import {
  arraySplice,
  Directory,
  memoize,
  EMPTY_ARRAY,
  StructReference,
  Point,
  Translate,
  Bounds,
  pointIntersectsBounds,
  getSmallestBounds,
  mergeBounds,
  Bounded,
  Struct,
  getTreeNodeIdMap,
  getNestedTreeNodeById,
  boundsFromRect,
  getFileFromUri,
  File,
  updateNestedNode,
  FileAttributeNames,
  isDirectory,
  getParentTreeNode,
  TreeNode,
  getBoundsSize,
  centerTransformZoom,
  createZeroBounds,
  getTreeNodeHeight,
  flattenTreeNode,
  shiftBounds,
  shiftPoint,
  flipPoint,
  moveBounds,
  FSItem,
  FSItemNamespaces,
  getTreeNodePath,
  updateNestedNodeTrail,
  getTreeNodeFromPath
} from "tandem-common";
import {
  SyntheticVisibleNode,
  PCEditorState,
  getSyntheticDocumentByDependencyUri,
  getSyntheticSourceNode,
  getPCNodeDependency,
  findRootInstanceOfPCNode,
  getSyntheticNodeById,
  getSyntheticVisibleNodeDocument,
  updateSyntheticVisibleNode,
  Frame,
  getSyntheticDocumentDependencyUri,
  getSyntheticVisibleNodeRelativeBounds,
  updateDependencyGraph,
  updateSyntheticVisibleNodeMetadata,
  isSyntheticVisibleNodeMovable,
  isSyntheticVisibleNodeResizable,
  updateFrame,
  diffSyntheticNode,
  SyntheticOperationalTransformType,
  SyntheticInsertChildOperationalTransform,
  PCSourceTagNames,
  patchSyntheticNode,
  getSyntheticDocumentById,
  SyntheticDocument,
  updateSyntheticDocument,
  getFrameByContentNodeId,
  getFramesByDependencyUri,
  isPaperclipUri,
  PCVisibleNode
} from "paperclip";
import {
  CanvasToolOverlayMouseMoved,
  CanvasToolOverlayClicked
} from "../actions";
import { uniq, pull, values, clamp } from "lodash";
import { stat } from "fs";
import {
  replaceDependency,
  PCDependency,
  Dependency,
  DependencyGraph,
  getModifiedDependencies
} from "paperclip";
import { FSSandboxRootState } from "fsbox";

export enum ToolType {
  TEXT,
  POINTER,
  COMPONENT,
  ELEMENT
}

export enum FrameMode {
  PREVIEW = "preview",
  DESIGN = "design"
}

export const REGISTERED_COMPONENT = "REGISTERED_COMPONENT";

export enum SyntheticVisibleNodeMetadataKeys {
  EDITING_LABEL = "editingLabel",
  EXPANDED = "expanded"
}

export type RegisteredComponent = {
  uri?: string;
  tagName: string;
  template: TreeNode<any>;
};

export type Canvas = {
  backgroundColor: string;
  panning?: boolean;
  translate: Translate;
};

export enum InsertFileType {
  FILE,
  DIRECTORY
}

export type InsertFileInfo = {
  type: InsertFileType;
  directoryId: string;
};

export type DependencyHistory = {
  index: number;
  snapshots: Dependency<any>[];
};

export type GraphHistory = {
  [identifier: string]: DependencyHistory;
};

export type Editor = {
  canvas: Canvas;
};

export type EditorWindow = {
  tabUris: string[];
  activeFilePath?: string;
  mousePosition?: Point;
  movingOrResizing?: boolean;
  smooth?: boolean;
  secondarySelection?: boolean;
  fullScreen?: boolean;
  container?: HTMLElement;
};

export type RootState = {
  editorWindows: EditorWindow[];
  mount: Element;
  openFiles: OpenFile[];
  toolType?: ToolType;
  activeEditorFilePath?: string;

  // TODO - should be actual instances for type safety
  hoveringNodeIds: string[];

  // TODO - this should be actual node instances
  selectedNodeIds: string[];
  selectedFileNodeIds: string[];
  selectedComponentVariantName?: string;
  projectDirectory?: Directory;
  insertFileInfo?: InsertFileInfo;
  history: GraphHistory;
  showQuickSearch?: boolean;
  selectedComponentId: string;
} & PCEditorState &
  FSSandboxRootState;

// TODO - change this to Editor
export type OpenFile = {
  temporary: boolean;
  newContent?: Buffer;
  uri: string;
  canvas: Canvas;
};

export const updateRootState = (
  properties: Partial<RootState>,
  root: RootState
) => ({
  ...root,
  ...properties
});

export const deselectRootProjectFiles = (state: RootState) =>
  updateRootState(
    {
      selectedFileNodeIds: []
    },
    state
  );

export const persistRootState = (
  persistPaperclipState: (state: RootState) => RootState,
  state: RootState,
  newSelectionScope?: SyntheticVisibleNode | SyntheticDocument
) => {
  const oldState = state;
  const oldGraph = state.graph;
  state = keepActiveFileOpen(
    updateRootState(persistPaperclipState(state), state)
  );
  const modifiedDeps = getModifiedDependencies(state.graph, oldGraph);
  state = addHistory(state, modifiedDeps.map(dep => oldGraph[dep.uri]));
  state = modifiedDeps.reduce(
    (state, dep: Dependency<any>) => setOpenFileContent(dep, state),
    state
  );
  return state;
};

const getUpdatedSyntheticVisibleNodes = (
  newState: RootState,
  oldState: RootState,
  scope: SyntheticVisibleNode | SyntheticDocument
) => {
  const MAX_DEPTH = 0;
  const oldScope = getSyntheticNodeById(scope.id, oldState.documents);
  const newScope = getSyntheticNodeById(scope.id, newState.documents);

  let newSyntheticVisibleNodes: SyntheticVisibleNode[] = [];
  let model = oldScope;
  diffSyntheticNode(oldScope, newScope).forEach(ot => {
    const target = getTreeNodeFromPath(ot.nodePath, model);
    model = patchSyntheticNode([ot], model);

    if (ot.nodePath.length > MAX_DEPTH) {
      return;
    }

    // TODO - will need to check if new parent is not in an instance of a component.
    // Will also need to consider child overrides though.
    if (ot.type === SyntheticOperationalTransformType.INSERT_CHILD) {
      newSyntheticVisibleNodes.push(ot.child);
    } else if (
      ot.type === SyntheticOperationalTransformType.SET_PROPERTY &&
      ot.name === "source"
    ) {
      newSyntheticVisibleNodes.push(target);
    }
  });

  return uniq(newSyntheticVisibleNodes);
};

export const selectInsertedSyntheticVisibleNodes = (
  oldState: RootState,
  newState: RootState,
  scope: SyntheticVisibleNode | SyntheticDocument
) => {
  return setSelectedSyntheticVisibleNodeIds(
    newState,
    ...getUpdatedSyntheticVisibleNodes(newState, oldState, scope).map(
      node => node.id
    )
  );
};

const setOpenFileContent = (dep: Dependency<any>, state: RootState) =>
  updateOpenFile(
    {
      temporary: false,
      newContent: new Buffer(JSON.stringify(dep.content, null, 2), "utf8")
    },
    dep.uri,
    state
  );

const addHistory = (root: RootState, modifiedDeps: Dependency<any>[]) => {
  return modifiedDeps.reduce((state, dep) => {
    const history: DependencyHistory = state.history[dep.uri] || {
      index: 0,
      snapshots: EMPTY_ARRAY
    };

    const snapshots = [...history.snapshots.slice(0, history.index), dep];

    return updateRootState(
      {
        history: {
          [dep.uri]: {
            index: snapshots.length,
            snapshots
          }
        }
      },
      state
    );
  }, root);
};

const moveDependencyRecordHistory = (
  uri: string,
  pos: number,
  root: RootState
): RootState => {
  const record = root.history[uri];
  if (!record) {
    return root;
  }

  const index = Math.max(
    0,
    Math.min(record.snapshots.length, record.index + pos)
  );

  // if index exceeds snapshot count, then we're at the end.
  const dep = record.snapshots[index] || root.graph[uri];

  root = updateRootState(
    {
      history: {
        [uri]: {
          ...record,
          index
        }
      },
      selectedFileNodeIds: [],
      selectedNodeIds: [],
      hoveringNodeIds: []
    },
    root
  );

  root = setOpenFileContent(dep, root);
  root = replaceDependency(dep, root);
  return root;
};

const DEFAULT_CANVAS: Canvas = {
  backgroundColor: "#EEE",
  translate: {
    left: 0,
    top: 0,
    zoom: 1
  }
};

export const undo = (root: RootState) =>
  root.editorWindows.reduce(
    (state, editor) =>
      moveDependencyRecordHistory(editor.activeFilePath, -1, root),
    root
  );
export const redo = (root: RootState) =>
  root.editorWindows.reduce(
    (state, editor) =>
      moveDependencyRecordHistory(editor.activeFilePath, 1, root),
    root
  );

export const getOpenFile = (uri: string, state: RootState) =>
  state.openFiles.find(openFile => openFile.uri === uri);

export const getOpenFilesWithContent = (state: RootState) =>
  state.openFiles.filter(openFile => openFile.newContent);

export const updateOpenFileContent = (
  uri: string,
  newContent: Buffer,
  state: RootState
) => {
  return updateOpenFile(
    {
      temporary: false,
      newContent
    },
    uri,
    state
  );
};

export const getActiveEditorWindow = (state: RootState) =>
  getEditorWithActiveFileUri(state.activeEditorFilePath, state);

export const updateOpenFile = (
  properties: Partial<OpenFile>,
  uri: string,
  state: RootState
) => {
  const file = getOpenFile(uri, state);

  if (!file) {
    state = addOpenFile(uri, false, state);
    return updateOpenFile(properties, uri, state);
  }

  const index = state.openFiles.indexOf(file);
  return updateRootState(
    {
      openFiles: arraySplice(state.openFiles, index, 1, {
        ...file,
        ...properties
      })
    },
    state
  );
};

export const upsertOpenFile = (
  uri: string,
  temporary: boolean,
  state: RootState
): RootState => {
  const file = getOpenFile(uri, state);
  if (file) {
    if (file.temporary !== temporary) {
      return updateOpenFile({ temporary }, uri, state);
    }
    return state;
  }

  return addOpenFile(uri, temporary, state);
};

export const getEditorWindowWithFileUri = (
  uri: string,
  state: RootState
): EditorWindow => {
  return state.editorWindows.find(window => window.tabUris.indexOf(uri) !== -1);
};

export const getEditorWithActiveFileUri = (
  uri: string,
  state: RootState
): EditorWindow => {
  return state.editorWindows.find(editor => editor.activeFilePath === uri);
};

export const openSecondEditor = (uri: string, state: RootState) => {
  const editor = getEditorWindowWithFileUri(uri, state);
  const i = state.editorWindows.indexOf(editor);
  if (i === 1) {
    return openEditorFileUri(uri, state);
  }

  if (i === 0 && editor.tabUris.length === 1) {
    return state;
  }

  const newTabUris = arraySplice(
    editor.tabUris,
    editor.tabUris.indexOf(uri),
    1
  );

  state = {
    ...state,
    editorWindows: arraySplice(state.editorWindows, i, 1, {
      ...editor,
      tabUris: newTabUris,
      activeFilePath:
        editor.activeFilePath === uri
          ? newTabUris[newTabUris.length - 1]
          : editor.activeFilePath
    })
  };

  if (state.editorWindows.length === 1) {
    state = {
      ...state,
      editorWindows: [
        ...state.editorWindows,

        { tabUris: [], activeFilePath: null }
      ]
    };
  }

  const secondEditor = state.editorWindows[1];
  return {
    ...state,
    editors: arraySplice(
      state.editorWindows,
      state.editorWindows.indexOf(secondEditor),
      1,
      {
        ...secondEditor,
        tabUris: [...secondEditor.tabUris, uri],
        activeFilePath: uri
      }
    )
  };
};

export const getSyntheticWindowBounds = memoize(
  (uri: string, state: RootState) => {
    const frames = getFramesByDependencyUri(
      uri,
      state.frames,
      state.documents,
      state.graph
    );
    if (!window) return createZeroBounds();
    return mergeBounds(...(frames || EMPTY_ARRAY).map(frame => frame.bounds));
  }
);

export const isImageMimetype = (mimeType: string) => /^image\//.test(mimeType);

export const openEditorFileUri = (uri: string, state: RootState): RootState => {
  const editor =
    getEditorWindowWithFileUri(uri, state) || state.editorWindows[0];

  return {
    ...state,
    hoveringNodeIds: [],
    selectedNodeIds: [],
    activeEditorFilePath: uri,
    editorWindows: editor
      ? arraySplice(
          state.editorWindows,
          state.editorWindows.indexOf(editor),
          1,
          {
            ...editor,
            tabUris:
              editor.tabUris.indexOf(uri) === -1
                ? [...editor.tabUris, uri]
                : editor.tabUris,
            activeFilePath: uri
          }
        )
      : [
          {
            tabUris: [uri],
            activeFilePath: uri
          }
        ]
  };
};

const queuePreview = (uri: string, state: RootState): RootState => {
  return state;
};

export const shiftActiveEditorTab = (
  delta: number,
  state: RootState
): RootState => {
  const editor = getActiveEditorWindow(state);

  // nothing open
  if (!editor) {
    return state;
  }
  const index = editor.tabUris.indexOf(editor.activeFilePath);
  let newIndex = index + delta;
  if (newIndex < 0) {
    newIndex = editor.tabUris.length + delta;
  } else if (newIndex >= editor.tabUris.length) {
    newIndex = -1 + delta;
  }
  newIndex = clamp(newIndex, 0, editor.tabUris.length - 1);

  return openEditorFileUri(editor.tabUris[newIndex], state);
};

const removeEditorWindow = (
  { activeFilePath }: EditorWindow,
  state: RootState
): RootState => {
  const editor = getEditorWithActiveFileUri(activeFilePath, state);
  return {
    ...state,
    editorWindows: arraySplice(
      state.editorWindows,
      state.editorWindows.indexOf(editor),
      1
    )
  };
};

export const closeFile = (uri: string, state: RootState): RootState => {
  const editorWindow = getEditorWindowWithFileUri(uri, state);

  if (editorWindow.tabUris.length === 1) {
    state = removeEditorWindow(editorWindow, state);
  } else {
    state = updateEditorWindow(
      {
        tabUris: editorWindow.tabUris.filter(furi => furi !== uri)
      },
      uri,
      state
    );
  }

  state = updateRootState(
    {
      openFiles: state.openFiles.filter(openFile => openFile.uri !== uri)
    },
    state
  );

  state = setNextOpenFile(state);

  return state;
};

export const setNextOpenFile = (state: RootState): RootState => {
  const hasOpenFile = state.openFiles.find(openFile =>
    Boolean(getEditorWithActiveFileUri(openFile.uri, state))
  );

  if (hasOpenFile) {
    return state;
  }
  state = {
    ...state,
    hoveringNodeIds: [],
    selectedNodeIds: []
  };

  if (state.openFiles.length) {
    state = openEditorFileUri(state.openFiles[0].uri, state);
  }

  return state;
};

export const removeTemporaryOpenFiles = (state: RootState) => {
  return {
    ...state,
    openFiles: state.openFiles.filter(openFile => !openFile.temporary)
  };
};

export const openSyntheticVisibleNodeOriginFile = (
  node: SyntheticVisibleNode,
  state: RootState
) => {
  const sourceNode = getSyntheticSourceNode(
    node as SyntheticVisibleNode,
    state.graph
  ) as PCVisibleNode;

  const uri = getPCNodeDependency(sourceNode.id, state.graph).uri;
  state = openEditorFileUri(uri, state);
  const instance = findRootInstanceOfPCNode(sourceNode, state.documents);
  state = setActiveFilePath(uri, state);
  state = setSelectedSyntheticVisibleNodeIds(state, instance.id);
  return state;
};

export const addOpenFile = (
  uri: string,
  temporary: boolean,
  state: RootState
): RootState => {
  const file = getOpenFile(uri, state);
  if (file) {
    return state;
  }

  state = removeTemporaryOpenFiles(state);

  return {
    ...state,
    openFiles: [
      ...state.openFiles,
      {
        uri,
        temporary,
        canvas: DEFAULT_CANVAS
      }
    ]
  };
};

// export const getInsertedWindowElementIds = (
//   oldWindow: SyntheticWindow,
//   targetFrameId: string,
//   newBrowser: PCEditorState
// ): string[] => {
//   const elementIds = oldWindow.documents
//     .filter(document => !targetFrameId || document.id === targetFrameId)
//     .reduce((nodeIds, oldFrame) => {
//       return [
//         ...nodeIds,
//         ...getInsertedFrameElementIds(oldFrame, newBrowser)
//       ];
//     }, []);
//   const newWindow = newBrowser.windows.find(
//     window => window.location === oldWindow.location
//   );
//   return [
//     ...elementIds,
//     ...newWindow.documents
//       .filter(document => {
//         const isInserted =
//           oldWindow.documents.find(oldFrame => {
//             return oldFrame.id === document.id;
//           }) == null;
//         return isInserted;
//       })
//       .map(document => document.root.id)
//   ];
// };

// export const getInsertedFrameElementIds = (
//   oldFrame: Frame,
//   newBrowser: PCEditorState
// ): string[] => {
//   const newFrame = getFrameById(oldFrame.id, newBrowser);
//   if (!newFrame) {
//     return [];
//   }
//   const oldIds = Object.keys(oldFrame.nativeNodeMap);
//   const newIds = Object.keys(newFrame.nativeNodeMap);
//   return pull(newIds, ...oldIds);
// };

export const keepActiveFileOpen = (state: RootState): RootState => {
  return {
    ...state,
    openFiles: state.openFiles.map(openFile => ({
      ...openFile,
      temporary: false
    }))
  };
};

// export const updateRootStateSyntheticWindowFrame = (
//   documentId: string,
//   properties: Partial<Frame>,
//   root: RootState
// ) => {
//   const window = getFrameWindow(documentId, root);
//   const document = getFrameById(documentId, root);
//   return updateRootState(
//     {
//       browser: updateSyntheticWindow(
//         window.location,
//         {
//           documents: arraySplice(
//             window.documents,
//             window.documents.indexOf(document),
//             1,
//             {
//               ...document,
//               ...properties
//             }
//           )
//         },
//         root
//       )
//     },
//     root
//   );
// };

export const setRootStateSyntheticVisibleNodeExpanded = (
  nodeId: string,
  value: boolean,
  state: RootState
) => {
  const node = getSyntheticNodeById(nodeId, state.documents);
  const document = getSyntheticVisibleNodeDocument(node.id, state.documents);

  state = updateSyntheticDocument(
    setSyntheticVisibleNodeExpanded(node, value, document),
    document,
    state
  );

  return state;
};

const setSyntheticVisibleNodeExpanded = (
  node: SyntheticVisibleNode,
  value: boolean,
  document: SyntheticDocument
): SyntheticVisibleNode => {
  const path = getTreeNodePath(node.id, document);
  const updater = (node: SyntheticVisibleNode) => {
    return {
      ...node,
      metadata: {
        ...node.metadata,
        [SyntheticVisibleNodeMetadataKeys.EXPANDED]: value
      }
    };
  };
  return (value
    ? updateNestedNodeTrail(path, document, updater)
    : updateNestedNode(node, document, updater)) as SyntheticVisibleNode;
};

export const setRootStateSyntheticVisibleNodeLabelEditing = (
  nodeId: string,
  value: boolean,
  state: RootState
) => {
  const node = getSyntheticNodeById(nodeId, state.documents);
  const document = getSyntheticVisibleNodeDocument(node.id, state.documents);
  state = updateSyntheticDocument(
    updateSyntheticVisibleNodeMetadata(
      {
        [SyntheticVisibleNodeMetadataKeys.EDITING_LABEL]: value
      },
      node,
      document
    ),
    document,
    state
  );
  return state;
};

export const setRootStateFileNodeExpanded = (
  nodeId: string,
  value: boolean,
  state: RootState
) => {
  return updateRootState(
    {
      projectDirectory: updateNestedNode(
        getNestedTreeNodeById(nodeId, state.projectDirectory),
        state.projectDirectory,
        (child: FSItem) => ({
          ...child,
          expanded: value
        })
      )
    },
    state
  );
};

export const updateEditorWindow = (
  properties: Partial<EditorWindow>,
  uri: string,
  root: RootState
) => {
  const window = getEditorWindowWithFileUri(uri, root);
  const i = root.editorWindows.indexOf(window);
  return updateRootState(
    {
      editorWindows: arraySplice(root.editorWindows, i, 1, {
        ...window,
        ...properties
      })
    },
    root
  );
};

const INITIAL_ZOOM_PADDING = 50;

export const centerEditorCanvas = (
  state: RootState,
  editorFileUri: string,
  innerBounds?: Bounds,
  smooth: boolean = false,
  zoomOrZoomToFit: boolean | number = true
) => {
  if (!innerBounds) {
    const frames = getFramesByDependencyUri(
      editorFileUri,
      state.frames,
      state.documents,
      state.graph
    );

    if (!frames.length) {
      return state;
    }

    innerBounds = getSyntheticWindowBounds(editorFileUri, state);
  }

  // no windows loaded
  if (
    innerBounds.left +
      innerBounds.right +
      innerBounds.top +
      innerBounds.bottom ===
    0
  ) {
    console.warn(` Cannot center when bounds has no size`);
    return updateOpenFileCanvas(
      {
        translate: { left: 0, top: 0, zoom: 1 }
      },
      editorFileUri,
      state
    );
  }

  const editorWindow = getEditorWindowWithFileUri(editorFileUri, state);
  const openFile = getOpenFile(editorFileUri, state);
  const { container } = editorWindow;

  if (!container) {
    console.warn("cannot center canvas without a container");
    return state;
  }

  const {
    canvas: { translate }
  } = openFile;

  const { width, height } = container.getBoundingClientRect();

  const innerSize = getBoundsSize(innerBounds);

  const centered = {
    left: -innerBounds.left + width / 2 - innerSize.width / 2,
    top: -innerBounds.top + height / 2 - innerSize.height / 2
  };

  const scale =
    typeof zoomOrZoomToFit === "boolean"
      ? Math.min(
          (width - INITIAL_ZOOM_PADDING) / innerSize.width,
          (height - INITIAL_ZOOM_PADDING) / innerSize.height
        )
      : typeof zoomOrZoomToFit === "number"
        ? zoomOrZoomToFit
        : translate.zoom;

  state = updateEditorWindow(
    {
      smooth
    },
    editorFileUri,
    state
  );

  state = updateOpenFileCanvas(
    {
      translate: centerTransformZoom(
        {
          ...centered,
          zoom: 1
        },
        { left: 0, top: 0, right: width, bottom: height },
        Math.min(scale, 1)
      )
    },
    editorFileUri,
    state
  );

  return state;
};

export const setActiveFilePath = (
  newActiveFilePath: string,
  root: RootState
) => {
  if (getEditorWithActiveFileUri(newActiveFilePath, root)) {
    return root;
  }
  root = openEditorFileUri(newActiveFilePath, root);
  root = addOpenFile(newActiveFilePath, true, root);
  root = centerEditorCanvas(root, newActiveFilePath);
  return root;
};

export const updateOpenFileCanvas = (
  properties: Partial<Canvas>,
  uri: string,
  root: RootState
) => {
  const openFile = getOpenFile(uri, root);
  return updateOpenFile(
    {
      canvas: {
        ...openFile.canvas,
        ...properties
      }
    },
    uri,
    root
  );
};

export const setInsertFile = (type: InsertFileType, state: RootState) => {
  const file = getNestedTreeNodeById(
    state.selectedFileNodeIds[0] || state.projectDirectory.id,
    state.projectDirectory
  );
  return updateRootState(
    {
      insertFileInfo: {
        type,
        directoryId: isDirectory(file)
          ? file.id
          : getParentTreeNode(file.id, state.projectDirectory).id
      }
    },
    state
  );
};

export const setTool = (toolType: ToolType, root: RootState) => {
  if (!root.editorWindows.length) {
    return root;
  }
  root = { ...root, selectedComponentId: null };
  root = updateRootState({ toolType }, root);
  root = setSelectedSyntheticVisibleNodeIds(root);
  return root;
};

export const getActiveFrames = (root: RootState): Frame[] =>
  values(root.frames).filter(frame =>
    root.editorWindows.some(
      editor =>
        editor.activeFilePath ===
        getSyntheticDocumentDependencyUri(
          getSyntheticVisibleNodeDocument(frame.contentNodeId, root.documents),
          root.graph
        )
    )
  );

export const getCanvasTranslate = (canvas: Canvas) => canvas.translate;

export const getScaledMouseCanvasPosition = (
  state: RootState,
  point: Point
) => {
  const canvas = getOpenFile(state.activeEditorFilePath, state).canvas;
  const translate = getCanvasTranslate(canvas);

  const scaledPageX = (point.left - translate.left) / translate.zoom;
  const scaledPageY = (point.top - translate.top) / translate.zoom;
  return { left: scaledPageX, top: scaledPageY };
};

export const getCanvasMouseTargetNodeId = (
  state: RootState,
  event: CanvasToolOverlayMouseMoved | CanvasToolOverlayClicked,
  filter?: (node: TreeNode<any>) => boolean
): string => {
  return getCanvasMouseTargetNodeIdFromPoint(
    state,
    {
      left: event.sourceEvent.pageX,
      top: event.sourceEvent.pageY
    },
    filter
  );
};

export const getCanvasMouseTargetNodeIdFromPoint = (
  state: RootState,
  point: Point,
  filter?: (node: TreeNode<any>) => boolean
): string => {
  const editor = getActiveEditorWindow(state);
  const canvas = getOpenFile(editor.activeFilePath, state).canvas;
  const translate = getCanvasTranslate(canvas);
  const toolType = state.toolType;

  const scaledMousePos = getScaledMouseCanvasPosition(state, point);

  const frame = getFrameFromPoint(scaledMousePos, state);

  if (!frame) return null;
  const contentNode = getSyntheticNodeById(
    frame.contentNodeId,
    state.documents
  );

  const { left: scaledPageX, top: scaledPageY } = scaledMousePos;

  const deadZone = getSelectionBounds(state);

  const mouseX = scaledPageX - frame.bounds.left;
  const mouseY = scaledPageY - frame.bounds.top;

  const computedInfo = frame.computed || {};
  const intersectingBounds: Bounds[] = [];
  const intersectingBoundsMap = new Map<Bounds, string>();
  const mouseFramePoint = { left: mouseX, top: mouseY };
  for (const $id in computedInfo) {
    const { bounds } = computedInfo[$id];
    if (
      pointIntersectsBounds(mouseFramePoint, bounds) &&
      !pointIntersectsBounds(scaledMousePos, deadZone) &&
      (toolType == null ||
        getNestedTreeNodeById($id, contentNode).name !==
          PCSourceTagNames.TEXT) &&
      (!filter || filter(getNestedTreeNodeById($id, contentNode)))
    ) {
      intersectingBounds.push(bounds);
      intersectingBoundsMap.set(bounds, $id);
    }
  }

  if (!intersectingBounds.length) return null;
  const smallestBounds = getSmallestBounds(...intersectingBounds);
  return intersectingBoundsMap.get(smallestBounds);
};

export const getCanvasMouseFrame = (
  state: RootState,
  event: CanvasToolOverlayMouseMoved | CanvasToolOverlayClicked
) => {
  return getFrameFromPoint(
    getScaledMouseCanvasPosition(state, {
      left: event.sourceEvent.pageX,
      top: event.sourceEvent.pageY
    }),
    state
  );
};

export const getFrameFromPoint = (point: Point, state: RootState) => {
  const activeFrames = getActiveFrames(state);
  if (!activeFrames.length) return null;
  for (let j = activeFrames.length; j--; ) {
    const frame = activeFrames[j];
    if (pointIntersectsBounds(point, frame.bounds)) {
      return frame;
    }
  }
};

export const setSelectedSyntheticVisibleNodeIds = (
  root: RootState,
  ...selectionIds: string[]
) => {
  const nodeIds = uniq([...selectionIds]).filter(Boolean);
  root = nodeIds.reduce(
    (state, nodeId) =>
      setRootStateSyntheticVisibleNodeExpanded(nodeId, true, root),
    root
  );
  root = updateRootState(
    {
      selectedNodeIds: nodeIds
    },
    root
  );
  return root;
};

export const setSelectedFileNodeIds = (
  root: RootState,
  ...selectionIds: string[]
) => {
  const nodeIds = uniq([...selectionIds]);
  root = nodeIds.reduce(
    (state, nodeId) => setRootStateFileNodeExpanded(nodeId, true, root),
    root
  );

  root = updateRootState(
    {
      selectedFileNodeIds: nodeIds
    },
    root
  );
  return root;
};

export const setHoveringSyntheticVisibleNodeIds = (
  root: RootState,
  ...selectionIds: string[]
) => {
  return updateRootState(
    {
      hoveringNodeIds: uniq([...selectionIds])
    },
    root
  );
};

// export const updateRootSyntheticPosition = (
//   position: Point,
//   nodeId: string,
//   root: RootState
// ) =>
//   updateRootState(
//     {
//       browser: updateSyntheticItemPosition(position, nodeId, root)
//     },
//     root
//   );

// export const updateRootSyntheticBounds = (
//   bounds: Bounds,
//   nodeId: string,
//   root: RootState
// ) =>
//   updateRootState(
//     {
//       browser: updateSyntheticItemBounds(bounds, nodeId, root)
//     },
//     root
//   );

export const getBoundedSelection = memoize((root: RootState): string[] =>
  root.selectedNodeIds.filter(nodeId =>
    getSyntheticVisibleNodeRelativeBounds(
      getSyntheticNodeById(nodeId, root.documents),
      root.frames
    )
  )
);

export const getSelectionBounds = memoize((root: RootState) =>
  mergeBounds(
    ...getBoundedSelection(root).map(nodeId =>
      getSyntheticVisibleNodeRelativeBounds(
        getSyntheticNodeById(nodeId, root.documents),
        root.frames
      )
    )
  )
);

export const isSelectionMovable = memoize((root: RootState) => {
  return !root.selectedNodeIds.some(nodeId => {
    const node = getSyntheticNodeById(nodeId, root.documents);
    return !isSyntheticVisibleNodeMovable(node);
  });
});

export const isSelectionResizable = memoize((root: RootState) => {
  return !root.selectedNodeIds.some(nodeId => {
    const node = getSyntheticNodeById(nodeId, root.documents);
    return !isSyntheticVisibleNodeResizable(node);
  });
});
