import { Action } from "redux";
import * as path from "path";
import {
  CanvasToolArtboardTitleClicked,
  NEW_FILE_ADDED,
  PC_LAYER_EDIT_LABEL_BLUR,
  CANVAS_TOOL_ARTBOARD_TITLE_CLICKED,
  PROJECT_LOADED,
  PC_LAYER_DOUBLE_CLICK,
  ProjectLoaded,
  SYNTHETIC_WINDOW_OPENED,
  CanvasToolOverlayMouseMoved,
  PROJECT_DIRECTORY_LOADED,
  ProjectDirectoryLoaded,
  FILE_NAVIGATOR_ITEM_CLICKED,
  FileNavigatorItemClicked,
  DOCUMENT_RENDERED,
  DocumentRendered,
  CANVAS_WHEEL,
  CANVAS_MOUSE_MOVED,
  CANVAS_MOUNTED,
  CANVAS_MOUSE_CLICKED,
  WrappedEvent,
  CanvasToolOverlayClicked,
  RESIZER_MOUSE_DOWN,
  ResizerMouseDown,
  ResizerMoved,
  RESIZER_MOVED,
  RESIZER_PATH_MOUSE_STOPPED_MOVING,
  RESIZER_STOPPED_MOVING,
  ResizerPathStoppedMoving,
  RESIZER_PATH_MOUSE_MOVED,
  ResizerPathMoved,
  SHORTCUT_ESCAPE_KEY_DOWN,
  INSERT_TOOL_FINISHED,
  InsertToolFinished,
  SHORTCUT_DELETE_KEY_DOWN,
  CANVAS_TOOL_WINDOW_BACKGROUND_CLICKED,
  SYNTHETIC_NODES_PASTED,
  SyntheticVisibleNodesPasted,
  FILE_NAVIGATOR_ITEM_DOUBLE_CLICKED,
  OPEN_FILE_ITEM_CLICKED,
  OPEN_FILE_ITEM_CLOSE_CLICKED,
  OpenFilesItemClick,
  SAVED_FILE,
  SavedFile,
  SAVED_ALL_FILES,
  RAW_CSS_TEXT_CHANGED,
  RawCSSTextChanged,
  PC_LAYER_MOUSE_OVER,
  PC_LAYER_MOUSE_OUT,
  PC_LAYER_CLICK,
  PC_LAYER_EXPAND_TOGGLE_CLICK,
  TreeLayerLabelChanged,
  TreeLayerClick,
  TreeLayerDroppedNode,
  TreeLayerExpandToggleClick,
  TreeLayerMouseOut,
  FILE_NAVIGATOR_TOGGLE_DIRECTORY_CLICKED,
  TreeLayerMouseOver,
  PC_LAYER_DROPPED_NODE,
  FILE_NAVIGATOR_NEW_FILE_CLICKED,
  FILE_NAVIGATOR_NEW_DIRECTORY_CLICKED,
  NewFileAdded,
  FILE_NAVIGATOR_DROPPED_ITEM,
  FileNavigatorDroppedItem,
  SHORTCUT_UNDO_KEY_DOWN,
  SHORTCUT_REDO_KEY_DOWN,
  SLOT_TOGGLE_CLICK,
  PC_LAYER_LABEL_CHANGED,
  NATIVE_NODE_TYPE_CHANGED,
  TEXT_VALUE_CHANGED,
  TextValueChanged,
  ElementTypeChanged,
  NativeNodeTypeChanged,
  SHORTCUT_QUICK_SEARCH_KEY_DOWN,
  QUICK_SEARCH_ITEM_CLICKED,
  QuickSearchItemClicked,
  QUICK_SEARCH_BACKGROUND_CLICK,
  NEW_VARIANT_NAME_ENTERED,
  NewVariantNameEntered,
  COMPONENT_VARIANT_NAME_DEFAULT_TOGGLE_CLICK,
  ComponentVariantNameDefaultToggleClick,
  COMPONENT_VARIANT_REMOVED,
  COMPONENT_VARIANT_NAME_CHANGED,
  ComponentVariantNameChanged,
  COMPONENT_VARIANT_NAME_CLICKED,
  ComponentVariantNameClicked,
  ELEMENT_VARIANT_TOGGLED,
  ElementVariantToggled,
  EDITOR_TAB_CLICKED,
  EditorTabClicked,
  CanvasWheel,
  SHORTCUT_ZOOM_IN_KEY_DOWN,
  SHORTCUT_ZOOM_OUT_KEY_DOWN,
  CanvasMounted,
  CANVAS_DROPPED_ITEM,
  CanvasDroppedItem,
  CANVAS_DRAGGED_OVER,
  SHORTCUT_CONVERT_TO_COMPONENT_KEY_DOWN,
  SHORTCUT_T_KEY_DOWN,
  SHORTCUT_R_KEY_DOWN,
  CanvasDraggingOver,
  ELEMENT_TYPE_CHANGED,
  CSS_PROPERTY_CHANGED,
  CSS_PROPERTY_CHANGE_COMPLETED,
  ATTRIBUTE_CHANGED,
  CSSPropertyChanged,
  FRAME_MODE_CHANGE_COMPLETE,
  FrameModeChangeComplete,
  TOOLBAR_TOOL_CLICKED,
  ToolbarToolClicked,
  EDITOR_TAB_CLOSE_BUTTON_CLICKED,
  SHORTCUT_SELECT_NEXT_TAB,
  SHORTCUT_SELECT_PREVIOUS_TAB,
  SHORTCUT_CLOSE_CURRENT_TAB,
  COMPONENT_PICKER_BACKGROUND_CLICK,
  ComponentPickerItemClick,
  COMPONENT_PICKER_ITEM_CLICK,
  SHORTCUT_C_KEY_DOWN
} from "../actions";
import {
  queueOpenFile,
  fsSandboxReducer,
  isImageUri,
  hasFileCacheItem,
  FS_SANDBOX_ITEM_LOADED,
  FSSandboxItemLoaded,
  isSvgUri
} from "fsbox";
import {
  RootState,
  setActiveFilePath,
  updateRootState,
  updateOpenFileCanvas,
  getCanvasMouseTargetNodeId,
  setSelectedSyntheticVisibleNodeIds,
  getSelectionBounds,
  getBoundedSelection,
  ToolType,
  setTool,
  persistRootState,
  getOpenFile,
  addOpenFile,
  upsertOpenFile,
  removeTemporaryOpenFiles,
  setNextOpenFile,
  updateOpenFile,
  deselectRootProjectFiles,
  setHoveringSyntheticVisibleNodeIds,
  setRootStateSyntheticVisibleNodeExpanded,
  setSelectedFileNodeIds,
  InsertFileType,
  setInsertFile,
  undo,
  redo,
  openSyntheticVisibleNodeOriginFile,
  setRootStateSyntheticVisibleNodeLabelEditing,
  getEditorWithActiveFileUri,
  openEditorFileUri,
  openSecondEditor,
  getActiveEditorWindow,
  getEditorWindowWithFileUri,
  updateEditorWindow,
  getSyntheticWindowBounds,
  centerEditorCanvas,
  getCanvasMouseTargetNodeIdFromPoint,
  isSelectionMovable,
  SyntheticVisibleNodeMetadataKeys,
  selectInsertedSyntheticVisibleNodes,
  RegisteredComponent,
  closeFile,
  shiftActiveEditorTab
} from "../state";
import {
  PCSourceTagNames,
  PCVisibleNode,
  PCTextNode,
  PCElement,
  paperclipReducer,
  PC_SYNTHETIC_FRAME_RENDERED,
  SyntheticElement,
  createPCElement,
  createPCTextNode,
  getSyntheticSourceFrame,
  getSyntheticVisibleNodeRelativeBounds,
  getSyntheticVisibleNodeDocument,
  getSyntheticSourceNode,
  getSyntheticNodeById,
  SyntheticVisibleNode,
  getPCNodeDependency,
  updateSyntheticVisibleNodePosition,
  updateFrameBounds,
  updateSyntheticVisibleNodeBounds,
  persistInsertNode,
  persistChangeLabel,
  removeSyntheticVisibleNode,
  persistSyntheticVisibleNodeBounds,
  persistRemoveSyntheticVisibleNode,
  getSyntheticNodeSourceDependency,
  persistConvertNodeToComponent,
  PCModule,
  persistMoveSyntheticVisibleNode,
  persistAppendPCClips,
  getPCNodeModule,
  persistChangeSyntheticTextNodeValue,
  persistRawCSSText,
  SyntheticTextNode,
  updatePCNodeMetadata,
  PCVisibleNodeMetadataKey,
  getSyntheticDocumentByDependencyUri,
  SyntheticBaseNode,
  getFrameSyntheticNode,
  SyntheticDocument,
  getFrameByContentNodeId,
  PC_DEPENDENCY_GRAPH_LOADED,
  PCDependencyGraphLoaded,
  SYNTHETIC_DOCUMENT_NODE_NAME,
  DEFAULT_FRAME_BOUNDS,
  isPaperclipUri,
  evaluateDependency,
  isSyntheticDocumentRoot,
  isSyntheticVisibleNode,
  persistChangeElementType,
  getSyntheticDocumentById,
  persistAddComponentController,
  persistCSSProperty,
  persistAttribute,
  getPCNode,
  updateSyntheticVisibleNode,
  persistSyntheticNodeMetadata,
  createPCComponentInstance
} from "paperclip";
import {
  getTreeNodePath,
  getTreeNodeFromPath,
  File,
  EMPTY_OBJECT,
  TreeNode,
  StructReference,
  roundBounds,
  scaleInnerBounds,
  moveBounds,
  keepBoundsAspectRatio,
  keepBoundsCenter,
  Bounded,
  Struct,
  Bounds,
  getBoundsSize,
  shiftBounds,
  flipPoint,
  diffArray,
  isDirectory,
  updateNestedNode,
  FileAttributeNames,
  Directory,
  getNestedTreeNodeById,
  isFile,
  arraySplice,
  getParentTreeNode,
  appendChildNode,
  removeNestedTreeNode,
  resizeBounds,
  updateNestedNodeTrail,
  boundsFromRect,
  centerTransformZoom,
  Translate,
  zoomBounds,
  getBoundsPoint,
  TreeMoveOffset,
  shiftPoint,
  Point,
  zoomPoint,
  cloneTreeNode,
  createTreeNode,
  FSItemNamespaces,
  FSItemTagNames,
  FSItem,
  getFileFromUri,
  createFile,
  stripProtocol,
  createDirectory,
  sortFSItems
} from "tandem-common";
import { difference, pull, clamp, merge } from "lodash";
import { select } from "redux-saga/effects";

const DEFAULT_RECT_COLOR = "#CCC";
const INSERT_TEXT_OFFSET = {
  left: -5,
  top: -10
};

const PANE_SENSITIVITY = process.platform === "win32" ? 0.1 : 1;
const ZOOM_SENSITIVITY = process.platform === "win32" ? 2500 : 250;
const MIN_ZOOM = 0.02;
const MAX_ZOOM = 6400 / 100;
const INITIAL_ZOOM_PADDING = 50;

export const rootReducer = (state: RootState, action: Action): RootState => {
  state = fsSandboxReducer(state, action);
  state = paperclipReducer(state, action);
  state = canvasReducer(state, action);
  state = shortcutReducer(state, action);
  state = clipboardReducer(state, action);

  switch (action.type) {
    case PROJECT_DIRECTORY_LOADED: {
      const { directory } = action as ProjectDirectoryLoaded;
      return updateRootState({ projectDirectory: directory }, state);
    }
    case FILE_NAVIGATOR_ITEM_CLICKED: {
      const { node } = action as FileNavigatorItemClicked;
      const uri = node.uri;
      state = setSelectedFileNodeIds(state, node.id);
      state = setFileExpanded(node, true, state);

      if (!isDirectory(node)) {
        state = maybeEvaluateFile(uri, state);
        state = setActiveFilePath(uri, state);
        return state;
      }

      return state;
    }
    case QUICK_SEARCH_ITEM_CLICKED: {
      const { file } = action as QuickSearchItemClicked;
      const uri = file.uri;
      state = maybeEvaluateFile(uri, state);
      state = setSelectedFileNodeIds(state, file.id);
      state = setActiveFilePath(uri, state);
      state = upsertOpenFile(uri, false, state);
      state = updateRootState({ showQuickSearch: false }, state);
      return state;
    }
    case QUICK_SEARCH_BACKGROUND_CLICK: {
      return (state = updateRootState({ showQuickSearch: false }, state));
    }
    case FILE_NAVIGATOR_TOGGLE_DIRECTORY_CLICKED: {
      const { node } = action as FileNavigatorItemClicked;
      state = setFileExpanded(node, !node.expanded, state);
      return state;
    }
    case FILE_NAVIGATOR_ITEM_DOUBLE_CLICKED: {
      const { node } = action as FileNavigatorItemClicked;
      const uri = node.uri;
      const file = getFileFromUri(uri, state.projectDirectory);
      if (isFile(file)) {
        state = upsertOpenFile(uri, false, state);
        state = openEditorFileUri(uri, state);
      }

      return state;
    }
    case FILE_NAVIGATOR_NEW_FILE_CLICKED: {
      return setInsertFile(InsertFileType.FILE, state);
    }
    case FILE_NAVIGATOR_NEW_DIRECTORY_CLICKED: {
      return setInsertFile(InsertFileType.DIRECTORY, state);
    }

    case CANVAS_MOUNTED: {
      const { fileUri, element } = action as CanvasMounted;
      if (!element) {
        return state;
      }

      const { width = 400, height = 300 } =
        element.getBoundingClientRect() || {};

      state = updateEditorWindow(
        {
          container: element
        },
        fileUri,
        state
      );

      return centerEditorCanvas(state, fileUri);
    }

    case FILE_NAVIGATOR_DROPPED_ITEM: {
      const { node, targetNode } = action as FileNavigatorDroppedItem;
      const parent: Directory = getParentTreeNode(
        node.id,
        state.projectDirectory
      );
      const parentUri = parent.uri;
      const nodeUri = node.uri;
      state = updateRootState(
        {
          projectDirectory: updateNestedNode(
            parent,
            state.projectDirectory,
            parent => removeNestedTreeNode(node, parent)
          )
        },
        state
      );

      const targetDir: Directory =
        targetNode.name !== FSItemTagNames.FILE
          ? targetNode
          : getParentTreeNode(targetNode.id, state.projectDirectory);
      const targetUri = targetDir.uri;
      state = updateRootState(
        {
          projectDirectory: updateNestedNode(
            targetDir,
            state.projectDirectory,
            targetNode => {
              return appendChildNode(
                {
                  ...node,
                  uri: nodeUri.replace(parentUri, targetUri)
                } as FSItem,
                targetNode
              );
            }
          )
        },
        state
      );

      return state;
    }
    case NEW_FILE_ADDED: {
      const { uri, fileType } = action as NewFileAdded;
      const directory = getFileFromUri(
        path.dirname(uri),
        state.projectDirectory
      );

      state = updateRootState(
        {
          insertFileInfo: null,
          projectDirectory: updateNestedNode(
            directory,
            state.projectDirectory,
            dir => {
              return {
                ...dir,
                children: sortFSItems([
                  ...dir.children,
                  fileType === FSItemTagNames.FILE
                    ? createFile(uri)
                    : createDirectory(uri)
                ])
              };
            }
          )
        },
        state
      );

      if (fileType === FSItemTagNames.FILE) {
        state = setActiveFilePath(uri, state);
        state = maybeEvaluateFile(uri, state);
      }
      return state;
    }

    case FS_SANDBOX_ITEM_LOADED: {
      const { uri, mimeType } = action as FSSandboxItemLoaded;
      // const pcState = paperclipReducer(state, action);

      const editor = getEditorWindowWithFileUri(uri, state);

      // TODO - move this to paperclip-tandem package
      if (editor && editor.activeFilePath === uri) {
        state = maybeEvaluateFile(uri, state);
      }

      return state;
    }
    case OPEN_FILE_ITEM_CLICKED: {
      const { uri, sourceEvent } = action as OpenFilesItemClick;
      if (getEditorWithActiveFileUri(uri, state)) {
        return state;
      }
      state = setNextOpenFile(
        removeTemporaryOpenFiles(
          sourceEvent.metaKey
            ? openSecondEditor(uri, state)
            : openEditorFileUri(uri, state)
        )
      );
      return state;
    }
    case SAVED_FILE: {
      const { uri } = action as SavedFile;
      return updateOpenFile({ newContent: null }, uri, state);
    }
    case SAVED_ALL_FILES: {
      return updateRootState(
        {
          openFiles: state.openFiles.map(openFile => ({
            ...openFile,
            newContent: null
          }))
        },
        state
      );
    }
    case ELEMENT_VARIANT_TOGGLED: {
      // const { newVariants } = action as ElementVariantToggled;
      // const sourceNode = getSyntheticSourceNode(
      //   state.selectedNodeIds[0],
      //   state.paperclip
      // );
      // state = persistRootState(
      //   browser =>
      //     persistSetElementVariants(
      //       newVariants,
      //       sourceNode.id,
      //       state.selectedComponentVariantName,
      //       browser
      //     ),
      //   state
      // );
      return state;
    }
    case NEW_VARIANT_NAME_ENTERED: {
      // const { value } = action as NewVariantNameEntered;
      // const sourceNode = getSyntheticSourceNode(
      //   state.selectedNodeIds[0],
      //   state.paperclip
      // ) as PCComponentNode;
      // state = persistRootState(
      //   browser => persistInsertNewComponentVariant(value, sourceNode, browser),
      //   state
      // );
      return state;
    }
    case COMPONENT_VARIANT_NAME_DEFAULT_TOGGLE_CLICK: {
      const { name, value } = action as ComponentVariantNameDefaultToggleClick;
      // const sourceComponent = getSyntheticVisibleNodeSourceComponent(
      //   state.selectedNodeIds[0],
      //   state.paperclip
      // );
      // state = persistRootState(
      //   browser =>
      //     persistComponentVariantChanged(
      //       { [PCSourceNamespaces.CORE]: { isDefault: value } },
      //       name,
      //       sourceComponent.id,
      //       browser
      //     ),
      //   state
      // );
      return state;
    }
    case COMPONENT_VARIANT_REMOVED: {
      const { name, value } = action as ComponentVariantNameDefaultToggleClick;
      // const sourceComponent = getSyntheticVisibleNodeSourceComponent(
      //   state.selectedNodeIds[0],
      //   state.paperclip
      // );
      // state = persistRootState(
      //   browser =>
      //     persistRemoveComponentVariant(name, sourceComponent.id, browser),
      //   state
      // );
      return state;
    }
    case COMPONENT_VARIANT_NAME_CLICKED: {
      const { name } = action as ComponentVariantNameClicked;
      state = updateRootState({ selectedComponentVariantName: name }, state);
      return state;
    }
    case COMPONENT_VARIANT_NAME_CHANGED: {
      const { oldName, newName } = action as ComponentVariantNameChanged;
      // const sourceComponentNode = getSyntheticVisibleNodeSourceComponent(
      //   state.selectedNodeIds[0],
      //   state.paperclip
      // );
      // state = persistRootState(
      //   browser =>
      //     persistComponentVariantChanged(
      //       {
      //         [PCSourceNamespaces.CORE]: { name: newName }
      //       },
      //       oldName,
      //       sourceComponentNode.id,
      //       browser
      //     ),
      //   state
      // );
      return state;
    }
    case PC_LAYER_MOUSE_OVER: {
      const { node } = action as TreeLayerMouseOver;
      state = setHoveringSyntheticVisibleNodeIds(state, node.id);
      return state;
    }
    case PC_LAYER_DOUBLE_CLICK: {
      const { node } = action as TreeLayerClick;
      state = setRootStateSyntheticVisibleNodeLabelEditing(
        node.id,
        true,
        state
      );
      return state;
    }
    case PC_LAYER_EDIT_LABEL_BLUR: {
      const { node } = action as TreeLayerClick;
      state = setRootStateSyntheticVisibleNodeLabelEditing(
        node.id,
        false,
        state
      );
      return state;
    }
    case PC_LAYER_LABEL_CHANGED: {
      const { label, node } = action as TreeLayerLabelChanged;

      state = setRootStateSyntheticVisibleNodeLabelEditing(
        node.id,
        false,
        state
      );
      state = persistRootState(
        browser =>
          persistChangeLabel(label, node as SyntheticVisibleNode, browser),
        state
      );
      return state;
    }
    case PC_LAYER_DROPPED_NODE: {
      const { node, targetNode, offset } = action as TreeLayerDroppedNode;

      const oldState = state;

      state = persistRootState(
        state =>
          persistMoveSyntheticVisibleNode(
            node as SyntheticVisibleNode,
            targetNode as SyntheticVisibleNode,
            offset,
            state
          ),
        state
      );

      const document = getSyntheticVisibleNodeDocument(
        targetNode.id,
        state.documents
      );
      const mutatedTarget =
        offset === TreeMoveOffset.APPEND || offset === TreeMoveOffset.PREPEND
          ? targetNode
          : getParentTreeNode(targetNode.id, document);

      state = selectInsertedSyntheticVisibleNodes(
        oldState,
        state,
        mutatedTarget
      );
      return state;
    }
    case PC_LAYER_MOUSE_OUT: {
      const { node } = action as TreeLayerMouseOut;
      state = setHoveringSyntheticVisibleNodeIds(state);
      return state;
    }
    case PC_LAYER_CLICK: {
      const { node, sourceEvent } = action as TreeLayerClick;
      if (sourceEvent.altKey) {
        // state = openSyntheticVisibleNodeOriginFile(node.id, state);
      } else {
        const doc = getSyntheticVisibleNodeDocument(node.id, state.documents);
        const dep = getSyntheticNodeSourceDependency(doc, state.graph);
        state = setActiveFilePath(dep.uri, state);
        state = setSelectedSyntheticVisibleNodeIds(
          state,
          ...(sourceEvent.shiftKey
            ? [...state.selectedNodeIds, node.id]
            : [node.id])
        );
      }
      return state;
    }
    case PC_LAYER_EXPAND_TOGGLE_CLICK: {
      const { node } = action as TreeLayerExpandToggleClick;
      state = setRootStateSyntheticVisibleNodeExpanded(
        node.id,
        !(node as SyntheticVisibleNode).metadata[
          SyntheticVisibleNodeMetadataKeys.EXPANDED
        ],
        state
      );
      return state;
    }
    case OPEN_FILE_ITEM_CLOSE_CLICKED: {
      // TODO - flag confirm remove state
      const { uri } = action as OpenFilesItemClick;
      return closeFile(uri, state);
    }
    case EDITOR_TAB_CLICKED: {
      const { uri } = action as EditorTabClicked;
      return openEditorFileUri(uri, state);
    }
    case EDITOR_TAB_CLOSE_BUTTON_CLICKED: {
      const { uri } = action as EditorTabClicked;
      return closeFile(uri, state);
    }
    case PC_DEPENDENCY_GRAPH_LOADED: {
      const { graph } = action as PCDependencyGraphLoaded;
      state = centerEditorCanvas(state, state.activeEditorFilePath);
      return state;
    }
  }
  return state;
};

export const canvasReducer = (state: RootState, action: Action) => {
  switch (action.type) {
    case RESIZER_MOVED: {
      const { point: newPoint } = action as ResizerMoved;
      state = updateEditorWindow(
        {
          movingOrResizing: true
        },
        state.activeEditorFilePath,
        state
      );

      if (isSelectionMovable(state)) {
        const selectionBounds = getSelectionBounds(state);
        const nodeId = state.selectedNodeIds[0];

        let movedBounds = moveBounds(selectionBounds, newPoint);

        for (const nodeId of state.selectedNodeIds) {
          const itemBounds = getSyntheticVisibleNodeRelativeBounds(
            getSyntheticNodeById(nodeId, state.documents),
            state.frames
          );
          const newBounds = roundBounds(
            scaleInnerBounds(itemBounds, selectionBounds, movedBounds)
          );

          state = updateSyntheticVisibleNodePosition(
            newBounds,
            getSyntheticNodeById(nodeId, state.documents),
            state
          );
        }
      }

      return state;
    }
    case RESIZER_MOUSE_DOWN: {
      const { sourceEvent } = action as ResizerMouseDown;
      if (sourceEvent.metaKey) {
        // state = openSyntheticVisibleNodeOriginFile(state.selectedNodeIds[0], state);
      }
      return state;
    }

    case COMPONENT_PICKER_BACKGROUND_CLICK: {
      return setTool(null, state);
    }

    case COMPONENT_PICKER_ITEM_CLICK: {
      const { component } = action as ComponentPickerItemClick;
      return {
        ...state,
        selectedComponentId: component.id
      };
    }

    case TOOLBAR_TOOL_CLICKED: {
      const { toolType } = action as ToolbarToolClicked;
      if (toolType === ToolType.POINTER) {
        state = setTool(null, state);
      } else {
        state = setTool(toolType, state);
      }
      return state;
    }

    case RESIZER_STOPPED_MOVING: {
      const { point } = action as ResizerMoved;
      const oldGraph = state.graph;

      if (isSelectionMovable(state)) {
        const selectionBounds = getSelectionBounds(state);
        state = persistRootState(state => {
          return state.selectedNodeIds.reduce((state, nodeId) => {
            return persistSyntheticVisibleNodeBounds(
              getSyntheticNodeById(nodeId, state.documents),
              state
            );
          }, state);
        }, state);
      }

      state = updateEditorWindow(
        {
          movingOrResizing: false
        },
        state.activeEditorFilePath,
        state
      );
      return state;
    }

    case CANVAS_WHEEL: {
      const {
        metaKey,
        ctrlKey,
        deltaX,
        deltaY,
        canvasHeight,
        canvasWidth
      } = action as CanvasWheel;
      const editorWindow = getActiveEditorWindow(state);
      const openFile = getOpenFile(editorWindow.activeFilePath, state);

      let translate = openFile.canvas.translate;

      if (metaKey || ctrlKey) {
        translate = centerTransformZoom(
          translate,
          boundsFromRect({
            width: canvasWidth,
            height: canvasHeight
          }),
          clamp(
            translate.zoom + translate.zoom * deltaY / ZOOM_SENSITIVITY,
            MIN_ZOOM,
            MAX_ZOOM
          ),
          editorWindow.mousePosition
        );
      } else {
        translate = {
          ...translate,
          left: translate.left - deltaX,
          top: translate.top - deltaY
        };
      }

      state = updateEditorWindow(
        { smooth: false },
        editorWindow.activeFilePath,
        state
      );

      state = updateOpenFileCanvas(
        {
          translate
        },
        editorWindow.activeFilePath,
        state
      );

      return state;
    }

    case CANVAS_DROPPED_ITEM: {
      let { item, point, editorUri } = action as CanvasDroppedItem;

      const targetNodeId = getCanvasMouseTargetNodeIdFromPoint(
        state,
        point,
        getDragFilter(item)
      );

      let sourceNode: PCVisibleNode;

      if (isFile(item)) {
        let src = path.relative(path.dirname(editorUri), item.uri);

        if (src.charAt(0) !== ".") {
          src = "./" + src;
        }

        if (isImageUri(item.uri)) {
          sourceNode = createPCElement(
            "img",
            {},
            {
              src
            }
          );
          if (isSvgUri(item.uri)) {
            sourceNode = createPCElement(
              "object",
              {},
              {
                data: src,
                type: "image/svg+xml"
              },
              [sourceNode]
            );
          }
        } else if (isJavaScriptFile(item.uri)) {
          return persistRootState(state => {
            return persistAddComponentController(
              (item as FSItem).uri,
              getSyntheticNodeById(targetNodeId, state.documents),
              state
            );
          }, state);
        }
      } else if (isSyntheticVisibleNode(item)) {
        sourceNode = getSyntheticSourceNode(item, state.graph) as PCVisibleNode;
      } else {
        sourceNode = cloneTreeNode((item as RegisteredComponent).template);
      }

      if (!sourceNode) {
        console.error(`Unrecognized dropped item.`);
        return state;
      }

      const targetId = getCanvasMouseTargetNodeIdFromPoint(
        state,
        point,
        node => node.name !== PCSourceTagNames.TEXT
      );
      let target: SyntheticVisibleNode | SyntheticDocument = targetId
        ? getSyntheticNodeById(targetId, state.documents)
        : getSyntheticDocumentByDependencyUri(
            editorUri,
            state.documents,
            state.graph
          );

      if (target.name === SYNTHETIC_DOCUMENT_NODE_NAME) {
        sourceNode = updatePCNodeMetadata(
          {
            [PCVisibleNodeMetadataKey.BOUNDS]: moveBounds(
              sourceNode.metadata[PCVisibleNodeMetadataKey.BOUNDS] ||
                DEFAULT_FRAME_BOUNDS,
              point
            )
          },
          sourceNode
        );
      }

      return persistRootState(
        browser =>
          persistInsertNode(sourceNode, target, TreeMoveOffset.APPEND, browser),
        state
      );
    }

    case SHORTCUT_ZOOM_IN_KEY_DOWN: {
      const editor = getActiveEditorWindow(state);
      const openFile = getOpenFile(editor.activeFilePath, state);
      state = setCanvasZoom(
        normalizeZoom(openFile.canvas.translate.zoom) * 2,
        false,
        editor.activeFilePath,
        state
      );
      return state;
    }

    case SHORTCUT_ZOOM_OUT_KEY_DOWN: {
      const editor = getActiveEditorWindow(state);
      const openFile = getOpenFile(editor.activeFilePath, state);
      state = setCanvasZoom(
        normalizeZoom(openFile.canvas.translate.zoom) / 2,
        false,
        editor.activeFilePath,
        state
      );
      return state;
    }

    case SHORTCUT_SELECT_NEXT_TAB: {
      return shiftActiveEditorTab(1, state);
    }
    case SHORTCUT_SELECT_PREVIOUS_TAB: {
      return shiftActiveEditorTab(-1, state);
    }
    case SHORTCUT_CLOSE_CURRENT_TAB: {
      return closeFile(state.activeEditorFilePath, state);
    }

    case CANVAS_MOUSE_MOVED: {
      const {
        sourceEvent: { pageX, pageY }
      } = action as WrappedEvent<React.MouseEvent<any>>;
      state = updateEditorWindow(
        { mousePosition: { left: pageX, top: pageY } },
        state.activeEditorFilePath,
        state
      );

      let targetNodeId: string;
      const editorWindow = getActiveEditorWindow(state);
      const openFile = getOpenFile(editorWindow.activeFilePath, state);

      if (!editorWindow.movingOrResizing) {
        targetNodeId = getCanvasMouseTargetNodeId(
          state,
          action as CanvasToolOverlayMouseMoved
        );
      }

      state = updateRootState(
        {
          hoveringNodeIds: targetNodeId ? [targetNodeId] : []
        },
        state
      );

      return state;
    }

    case CANVAS_DRAGGED_OVER: {
      const { item, offset } = action as CanvasDraggingOver;

      state = updateEditorWindow(
        { mousePosition: offset },
        state.activeEditorFilePath,
        state
      );

      // remove selection so that hovering state is visible
      state = setSelectedSyntheticVisibleNodeIds(state);

      // TODO - in the future, we'll probably want to be able to highlight hovered nodes as the user is moving an element around to indicate where
      // they can drop the element.

      let targetNodeId: string;
      const editor = getActiveEditorWindow(state);

      targetNodeId = getCanvasMouseTargetNodeIdFromPoint(
        state,
        offset,
        getDragFilter(item)
      );

      state = updateRootState(
        {
          hoveringNodeIds: targetNodeId ? [targetNodeId] : []
        },
        state
      );

      return state;
    }

    // TODO
    case CANVAS_MOUSE_CLICKED: {
      if (state.toolType != null) {
        return state;
      }

      state = deselectRootProjectFiles(state);

      const { sourceEvent } = action as CanvasToolOverlayClicked;
      if (/textarea|input/i.test((sourceEvent.target as Element).nodeName)) {
        return state;
      }

      // alt key opens up a new link
      const altKey = sourceEvent.altKey;

      const editorWindow = getActiveEditorWindow(state);
      const openFile = getOpenFile(editorWindow.activeFilePath, state);

      // do not allow selection while window is panning (scrolling)
      if (openFile.canvas.panning || editorWindow.movingOrResizing)
        return state;

      const targetNodeId = getCanvasMouseTargetNodeId(
        state,
        action as CanvasToolOverlayMouseMoved
      );

      if (!targetNodeId) {
        return setSelectedSyntheticVisibleNodeIds(state);
      }

      // if (altKey) {
      //   state = openSyntheticVisibleNodeOriginFile(targetNodeId, state);
      //   return state;
      // }

      if (!altKey) {
        state = handleArtboardSelectionFromAction(
          state,
          targetNodeId,
          action as CanvasToolOverlayMouseMoved
        );
        state = updateEditorWindow(
          {
            secondarySelection: false
          },
          editorWindow.activeFilePath,
          state
        );
        return state;
      }
      return state;
    }
    case RESIZER_PATH_MOUSE_MOVED: {
      state = updateEditorWindow(
        {
          movingOrResizing: true
        },
        state.activeEditorFilePath,
        state
      );

      // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist
      const newBounds = getResizeActionBounds(action as ResizerPathMoved);
      for (const nodeId of getBoundedSelection(state)) {
        state = updateSyntheticVisibleNodeBounds(
          getNewSyntheticVisibleNodeBounds(
            newBounds,
            getSyntheticNodeById(nodeId, state.documents),
            state
          ),
          getSyntheticNodeById(nodeId, state.documents),
          state
        );
      }

      return state;
    }
    case RESIZER_PATH_MOUSE_STOPPED_MOVING: {
      state = updateEditorWindow(
        {
          movingOrResizing: false
        },
        state.activeEditorFilePath,
        state
      );

      // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist
      const newBounds = getResizeActionBounds(
        action as ResizerPathStoppedMoving
      );

      state = persistRootState(state => {
        return state.selectedNodeIds.reduce(
          (state, nodeId) =>
            persistSyntheticVisibleNodeBounds(
              getSyntheticNodeById(nodeId, state.documents),
              state
            ),
          state
        );
      }, state);

      return state;
    }
    case RAW_CSS_TEXT_CHANGED: {
      const { value: cssText } = action as RawCSSTextChanged;
      state = persistRootState(browser => {
        return state.selectedNodeIds.reduce(
          (state, nodeId) =>
            persistRawCSSText(
              cssText,
              getSyntheticNodeById(nodeId, state.documents),
              state
            ),
          state
        );
      }, state);
      return state;
    }
    case CSS_PROPERTY_CHANGED: {
      const { name, value } = action as CSSPropertyChanged;
      state = state.selectedNodeIds.reduce((state, nodeId) => {
        return updateSyntheticVisibleNode(
          getSyntheticNodeById(nodeId, state.documents),
          state,
          node => {
            return {
              ...node,
              style: {
                ...node.style,
                [name]: value
              }
            };
          }
        );
      }, state);
      return state;
    }

    case FRAME_MODE_CHANGE_COMPLETE: {
      const { frame, mode } = action as FrameModeChangeComplete;
      state = persistRootState(state => {
        return persistSyntheticNodeMetadata(
          { mode },
          getSyntheticNodeById(frame.contentNodeId, state.documents),
          state
        );
      }, state);
      return state;
    }

    case CSS_PROPERTY_CHANGE_COMPLETED: {
      const { name, value } = action as CSSPropertyChanged;
      state = persistRootState(browser => {
        return state.selectedNodeIds.reduce(
          (state, nodeId) =>
            persistCSSProperty(
              name,
              value,
              getSyntheticNodeById(nodeId, state.documents),
              state
            ),
          state
        );
      }, state);
      return state;
    }

    case ATTRIBUTE_CHANGED: {
      const { name, value } = action as CSSPropertyChanged;
      state = persistRootState(browser => {
        return state.selectedNodeIds.reduce(
          (state, nodeId) =>
            persistAttribute(
              name,
              value,
              getSyntheticNodeById(nodeId, state.documents) as SyntheticElement,
              state
            ),
          state
        );
      }, state);
      return state;
    }
    case SLOT_TOGGLE_CLICK: {
      // state = persistRootState(browser => {
      //   return persistToggleSlotContainer(
      //     getSyntheticSourceNode(state.selectedNodeIds[0], state.paperclip).id,
      //     browser
      //   );
      // }, state);
      return state;
    }
    case NATIVE_NODE_TYPE_CHANGED: {
      const { nativeType } = action as NativeNodeTypeChanged;
      // state = persistRootState(browser => {
      //   return persistChangeNodeType(
      //     nativeType,
      //     getSyntheticSourceNode(
      //       state.selectedNodeIds[0],
      //       state.paperclip
      //     ) as PCElement,
      //     browser
      //   );
      // }, state);
      return state;
    }
    case TEXT_VALUE_CHANGED: {
      const { value } = action as TextValueChanged;
      state = persistRootState(state => {
        return persistChangeSyntheticTextNodeValue(
          value,
          getSyntheticNodeById(
            state.selectedNodeIds[0],
            state.documents
          ) as SyntheticTextNode,
          state
        );
      }, state);
      return state;
    }
    case ELEMENT_TYPE_CHANGED: {
      const { value } = action as ElementTypeChanged;
      state = persistRootState(state => {
        return persistChangeElementType(
          value,
          getSyntheticNodeById(
            state.selectedNodeIds[0],
            state.documents
          ) as SyntheticElement,
          state
        );
      }, state);
      return state;
    }
    case CANVAS_TOOL_ARTBOARD_TITLE_CLICKED: {
      const { frame, sourceEvent } = action as CanvasToolArtboardTitleClicked;
      sourceEvent.stopPropagation();
      const contentNode = getFrameSyntheticNode(frame, state.documents);
      state = updateEditorWindow(
        { smooth: false },
        getPCNodeDependency(
          getSyntheticSourceNode(contentNode, state.graph).id,
          state.graph
        ).uri,
        state
      );
      return handleArtboardSelectionFromAction(
        state,
        frame.contentNodeId,
        action as CanvasToolArtboardTitleClicked
      );
    }
    case CANVAS_TOOL_WINDOW_BACKGROUND_CLICKED: {
      return setSelectedSyntheticVisibleNodeIds(state);
    }
    case INSERT_TOOL_FINISHED: {
      let { point, fileUri } = action as InsertToolFinished;
      const editor = getEditorWithActiveFileUri(fileUri, state);

      const toolType = state.toolType;

      switch (toolType) {
        case ToolType.COMPONENT: {
          const componentId = state.selectedComponentId;
          state = { ...state, selectedComponentId: null };
          const component = getPCNode(componentId, state.graph);

          return persistInsertNodeFromPoint(
            createPCComponentInstance(
              componentId,
              [],
              null,
              null,
              null,
              component.metadata
            ),
            fileUri,
            point,
            state
          );
        }
        case ToolType.ELEMENT: {
          return persistInsertNodeFromPoint(
            createPCElement(
              "div",
              { "box-sizing": "border-box" },
              null,
              null,
              "Element"
            ),
            fileUri,
            point,
            state
          );
        }

        case ToolType.TEXT: {
          return persistInsertNodeFromPoint(
            createPCTextNode("Click to edit", "Text"),
            fileUri,
            point,
            state
          );
        }
      }
    }
  }

  return state;
};

const isJavaScriptFile = (file: string) => /(ts|js)x?$/.test(file);

const INSERT_ARTBOARD_WIDTH = 100;
const INSERT_ARTBOARD_HEIGHT = 100;

const persistInsertNodeFromPoint = (
  node: PCVisibleNode,
  fileUri: string,
  point: Point,
  state: RootState
) => {
  const oldState = state;
  const targetNodeId = getCanvasMouseTargetNodeIdFromPoint(state, point);
  let targetNode: SyntheticVisibleNode | SyntheticDocument =
    targetNodeId && getSyntheticNodeById(targetNodeId, state.documents);

  if (!targetNode) {
    const newPoint = shiftPoint(
      normalizePoint(getOpenFile(fileUri, state).canvas.translate, point),
      {
        left: -(INSERT_ARTBOARD_WIDTH / 2),
        top: -(INSERT_ARTBOARD_HEIGHT / 2)
      }
    );

    let bounds = {
      left: 0,
      top: 0,
      right: INSERT_ARTBOARD_WIDTH,
      bottom: INSERT_ARTBOARD_HEIGHT,
      ...(node.metadata[PCVisibleNodeMetadataKey.BOUNDS] || {})
    };

    bounds = moveBounds(bounds, newPoint);

    node = updatePCNodeMetadata(
      {
        [PCVisibleNodeMetadataKey.BOUNDS]: bounds
      },
      node
    );

    targetNode = getSyntheticDocumentByDependencyUri(
      fileUri,
      state.documents,
      state.graph
    );
  }

  state = persistRootState(
    browser => {
      return persistInsertNode(node, targetNode, TreeMoveOffset.APPEND, state);
    },
    state,
    targetNode
  );

  state = setTool(null, state);
  state = selectInsertedSyntheticVisibleNodes(oldState, state, targetNode);

  return state;
};

const getDragFilter = (item: any) => {
  let filter = (node: SyntheticVisibleNode) =>
    node.name !== PCSourceTagNames.TEXT;

  if (isFile(item) && isJavaScriptFile(item.uri)) {
    filter = (node: SyntheticVisibleNode) => {
      return (
        node.isContentNode &&
        node.isCreatedFromComponent &&
        !node.isComponentInstance
      );
    };
  }

  return filter;
};

const setFileExpanded = (node: FSItem, value: boolean, state: RootState) => {
  state = updateRootState(
    {
      projectDirectory: updateNestedNode(
        node,
        state.projectDirectory,
        (node: FSItem) => ({
          ...node,
          expanded: value
        })
      )
    },
    state
  );
  return state;
};

const getNewSyntheticVisibleNodeBounds = (
  newBounds: Bounds,
  node: SyntheticVisibleNode,
  state: RootState
) => {
  const currentBounds = getSelectionBounds(state);
  const innerBounds = getSyntheticVisibleNodeRelativeBounds(node, state.frames);
  return scaleInnerBounds(innerBounds, currentBounds, newBounds);
};

const getResizeActionBounds = (action: ResizerPathMoved | ResizerMoved) => {
  let {
    anchor,
    originalBounds,
    newBounds,
    sourceEvent
  } = action as ResizerPathMoved;

  const keepAspectRatio = sourceEvent.shiftKey;
  const keepCenter = sourceEvent.altKey;

  if (keepCenter) {
    // TODO - need to test. this might not work
    newBounds = keepBoundsCenter(newBounds, originalBounds, anchor);
  }

  if (keepAspectRatio) {
    newBounds = keepBoundsAspectRatio(
      newBounds,
      originalBounds,
      anchor,
      keepCenter ? { left: 0.5, top: 0.5 } : anchor
    );
  }

  return newBounds;
};

const isInputSelected = (state: RootState) => {
  // ick -- this needs to be moved into a saga
  return (
    document.activeElement &&
    /textarea|input|button/i.test(document.activeElement.tagName)
  );
};

const shortcutReducer = (state: RootState, action: Action): RootState => {
  switch (action.type) {
    case SHORTCUT_QUICK_SEARCH_KEY_DOWN: {
      return isInputSelected(state)
        ? state
        : updateRootState(
            {
              showQuickSearch: !state.showQuickSearch
            },
            state
          );
    }
    case SHORTCUT_UNDO_KEY_DOWN: {
      return undo(state);
    }
    case SHORTCUT_REDO_KEY_DOWN: {
      return redo(state);
    }
    case SHORTCUT_T_KEY_DOWN: {
      return isInputSelected(state) ? state : setTool(ToolType.TEXT, state);
    }
    case SHORTCUT_R_KEY_DOWN: {
      return isInputSelected(state) ? state : setTool(ToolType.ELEMENT, state);
    }
    case SHORTCUT_C_KEY_DOWN: {
      return isInputSelected(state)
        ? state
        : setTool(ToolType.COMPONENT, state);
    }
    case SHORTCUT_CONVERT_TO_COMPONENT_KEY_DOWN: {
      // TODO - should be able to conver all selected nodes to components
      if (state.selectedNodeIds.length > 1) {
        return state;
      }

      const oldState = state;

      state = persistRootState(
        state =>
          persistConvertNodeToComponent(
            getSyntheticNodeById(state.selectedNodeIds[0], state.documents),
            state
          ),
        state
      );

      state = selectInsertedSyntheticVisibleNodes(
        oldState,
        state,
        getSyntheticDocumentByDependencyUri(
          state.activeEditorFilePath,
          state.documents,
          state.graph
        )
      );
      return state;
    }
    case SHORTCUT_ESCAPE_KEY_DOWN: {
      if (isInputSelected(state)) {
        return state;
      }
      if (state.toolType != null) {
        return setTool(null, state);
      } else {
        state = setSelectedSyntheticVisibleNodeIds(state);
        state = setSelectedFileNodeIds(state);
        state = updateRootState({ insertFileInfo: null }, state);
        return state;
      }
    }
    case SHORTCUT_DELETE_KEY_DOWN: {
      if (isInputSelected(state)) {
        return state;
      }

      return persistRootState(state => {
        const firstNode = getSyntheticNodeById(
          state.selectedNodeIds[0],
          state.documents
        );
        const document = getSyntheticVisibleNodeDocument(
          firstNode.id,
          state.documents
        );
        let parent = getParentTreeNode(firstNode.id, document);
        const index = parent.children.indexOf(firstNode);

        state = state.selectedNodeIds.reduce((state, nodeId) => {
          return persistRemoveSyntheticVisibleNode(
            getSyntheticNodeById(nodeId, state.documents),
            state
          );
        }, state);

        parent = getSyntheticNodeById(parent.id, state.documents);

        state = setSelectedSyntheticVisibleNodeIds(
          state,
          ...(parent.children.length
            ? [parent.children[Math.min(index, parent.children.length - 1)].id]
            : parent.name !== SYNTHETIC_DOCUMENT_NODE_NAME
              ? [parent.id]
              : [])
        );
        return state;
      }, state);
    }
  }
  return state;
};

const clipboardReducer = (state: RootState, action: Action) => {
  switch (action.type) {
    case SYNTHETIC_NODES_PASTED: {
      const { clips } = action as SyntheticVisibleNodesPasted;
      const oldState = state;

      let offset: TreeMoveOffset = TreeMoveOffset.AFTER;
      let targetNode: SyntheticVisibleNode | SyntheticDocument;
      let scopeNode: SyntheticVisibleNode | SyntheticDocument;

      if (state.selectedNodeIds.length) {
        const nodeId = state.selectedNodeIds[0];
        scopeNode = targetNode = getSyntheticNodeById(nodeId, state.documents);
        const clipsContainTarget = clips.some(
          clip => clip.node.id === targetNode.source.nodeId
        );

        // if selected node is the pasted element, then paste
        if (!clipsContainTarget) {
          offset = TreeMoveOffset.PREPEND;
        } else {
          scopeNode = getParentTreeNode(
            scopeNode.id,
            getSyntheticVisibleNodeDocument(scopeNode.id, state.documents)
          );
        }
      } else {
        offset = TreeMoveOffset.PREPEND;
        scopeNode = targetNode = getSyntheticDocumentByDependencyUri(
          state.activeEditorFilePath,
          state.documents,
          state.graph
        );
      }

      state = persistRootState(
        state => persistAppendPCClips(clips, targetNode, offset, state),
        state
      );

      if (scopeNode === targetNode) {
        state = selectInsertedSyntheticVisibleNodes(oldState, state, scopeNode);
      }

      return state;
    }
  }

  return state;
};

const isDroppableNode = (node: SyntheticVisibleNode) => {
  return (
    node.name !== "text" &&
    !/input/.test(String((node as SyntheticElement).name))
  );
};

const maybeEvaluateFile = (uri: string, state: RootState) => {
  if (isPaperclipUri(uri) && hasFileCacheItem(uri, state)) {
    return evaluateDependency(uri, state);
  }
  return queueOpenFile(uri, state);
};

const handleArtboardSelectionFromAction = <
  T extends { sourceEvent: React.MouseEvent<any> }
>(
  state: RootState,
  nodeId: string,
  event: T
) => {
  const { sourceEvent } = event;
  state = setRootStateSyntheticVisibleNodeExpanded(nodeId, true, state);
  return setSelectedSyntheticVisibleNodeIds(state, nodeId);
};

const setCanvasZoom = (
  zoom: number,
  smooth: boolean = true,
  uri: string,
  state: RootState
) => {
  const editorWindow = getEditorWindowWithFileUri(uri, state);
  const openFile = getOpenFile(uri, state);
  return updateOpenFileCanvas(
    {
      translate: centerTransformZoom(
        openFile.canvas.translate,
        editorWindow.container.getBoundingClientRect(),
        clamp(zoom, MIN_ZOOM, MAX_ZOOM),
        editorWindow.mousePosition
      )
    },
    uri,
    state
  );
};

const normalizeBounds = (translate: Translate, bounds: Bounds) => {
  return zoomBounds(
    shiftBounds(bounds, {
      left: -translate.left,
      top: -translate.top
    }),
    1 / translate.zoom
  );
};

const normalizePoint = (translate: Translate, point: Point) => {
  return zoomPoint(
    shiftPoint(point, {
      left: -translate.left,
      top: -translate.top
    }),
    1 / translate.zoom
  );
};

const normalizeZoom = zoom => {
  return zoom < 1 ? 1 / Math.round(1 / zoom) : Math.round(zoom);
};
