import * as React from "react";
import * as Mantine from "@mantine/core";
import * as Icons from "@tabler/icons";

import {D3OnionGraphData, D3GraphUpdater} from "../d3graph/reducers/onion_graph";
import {D3GraphEditable, UserEditCallback} from "../d3graph/d3graph_editable";

import {
  DeltaGraphState,
  fullDeltaId,
  deltaGraphReducer,
} from "../d3graph/reducers/delta_graph";

import {
  HistoryGraphState,
  initialHistoryGraph,
  fullVersionId,
  historyGraphReducer,
} from "../d3graph/reducers/history_graph";

import * as helpText from "./help_text";
import {MergeView} from "./merge_view";
import {GraphView, Renderer} from "./graph_view";

import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
import {InfoHoverCardOverlay} from "../info_hover_card";
import {OnionContext, OnionContextType} from "../onion_context";

import {Version, VersionRegistry, Embeddings} from "onion/version";
import {PrimitiveDelta, PrimitiveRegistry} from "onion/primitive_delta";
import {PrimitiveValue, UUID} from "onion/types";
import {CompositeDelta, CompositeLevel} from "onion/composite_delta";
import {GraphState} from "onion/graph_state"; 
import {Delta} from "onion/delta";
import {DeltaParser} from "onion/delta_parser";

export const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";

export interface VersionedModelState {
  version: Version; // the 'current version'
  graph: D3OnionGraphData; // the state what is displayed in the leftmost panel
  historyGraph: HistoryGraphState; // the state of what is displayed in the middle panel
  deltaGraphL1: DeltaGraphState; // the state of what is displayed in the rightmost panel
  deltaGraphL0: DeltaGraphState; // the state of what is displayed in the rightmost panel
}

interface VersionedModelCallbacks {
  onUserEdit?: UserEditCallback;
  onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void;
  onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void;
  onVersionClicked?: (Version) => void;
  onMerge?: (outputs: Version[]) => void;
}

// Basically everything we need to construct the React components for:
//  - Graph state (+ optionally, a Rountangle Editor)
//  - History graph (+undo/redo buttons)
//  - Delta graph
// , their state, and callbacks for updating their state.
export function newOnion({readonly, primitiveRegistry, versionRegistry}) {
  const graphState = new GraphState();
  const compositeLevel = new CompositeLevel();

  // SVG coordinates to be used when adding a new node
  let x = 0;
  let y = 0;

  // The "current version" is both part of the React state (for rendering undo/redo buttons) and a local variable here, such that we can get the current version (synchronously), even outside of a setState-callback.
  let currentVersion = versionRegistry.initialVersion;
  function getCurrentVersion() {
    return currentVersion;
  }

  function useOnion(overridenCallbacks: (any) => VersionedModelCallbacks) {
    const [version, setVersion] = React.useState<Version>(versionRegistry.initialVersion);
    const [graph, setGraph] = React.useState<D3OnionGraphData>(emptyGraph);
    const [historyGraph, setHistoryGraph] = React.useState<HistoryGraphState>(initialHistoryGraph(versionRegistry.initialVersion));
    const [deltaGraphL1, setDeltaGraphL1] = React.useState<DeltaGraphState>(emptyGraph);
    const [deltaGraphL0, setDeltaGraphL0] = React.useState<DeltaGraphState>(emptyGraph);

    // Reducer

    // Create and add a new version, and its deltas, without changing the current version
    const createVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer, embeddings: (Version) => Embeddings = () => new Map()) => {

      const parentVersion = versionRegistry.lookupOptional(parentHash);
      if (parentVersion !== undefined) {
        // The following is not very efficient, and it looks weird and hacky, but it works.
        // Cleaner looking solution would be to implement a function version.getGraphState() ...
        let prevVersion = currentVersion;
        gotoVersion(parentVersion);
        const dependencies = compositeLevel.findCompositeDependencies(deltas, graphState.composites)
        const composite = compositeLevel.createComposite(deltas, description, dependencies);
        gotoVersion(prevVersion); // go back

        const newVersion = versionRegistry.createVersion(parentVersion, composite, embeddings);

        setHistoryGraph(historyGraph => historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}));
        setDeltaGraphL1(deltaGraphL1 => composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1);
        setDeltaGraphL0(deltaGraphL0 => composite.deltas.reduce((graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}), deltaGraphL0));

        return newVersion;
      }
    };
    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion, embeddings: (Version) => Embeddings = () => new Map()): Version => {
      const newVersion = createVersion(deltas, description, parentVersion.hash, embeddings) as Version;
      gotoVersion(newVersion);
      return newVersion;
    };
    // Idempotent
    const appendVersions = (versions: Version[]) => {
      setHistoryGraph(historyGraph => {
        const versionsToAdd: Version[] = [];
        const addIfDontHaveYet = version => {
          if (!(historyGraph.nodes.some(n => n.obj === version) || versionsToAdd.includes(version))) {
            collectVersions(version);
          }
        }
        const collectVersions = version => {
          // first add child, then parent
          // this prevents infinite recursion when a version explicitly embeds itself.
          versionsToAdd.push(version);
          for (const [parent] of version.parents) {
            addIfDontHaveYet(parent);
          }
          for (const {version: guest} of version.embeddings.values()) {
            addIfDontHaveYet(guest);
          }
        }
        for (const v of versions) {
          collectVersions(v);
        }
        return versionsToAdd.reduceRight((historyGraph, version) => historyGraphReducer(historyGraph, {type: 'addVersion', version}), historyGraph);
      })
    }
    // // Idempotent
    // const appendDelta = (delta: CompositeDelta) => {
    //   setState(({deltaGraphL0, deltaGraphL1, ...rest}) => {
    //     return {
    //         deltaGraphL1: deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta, active: false}),
    //         deltaGraphL0: delta.reduce(
    //           (graph, delta) => deltaGraphReducer(deltaGraphL0, {type: 'addDelta', delta, active: false}),
    //           deltaGraphL0),
    //       ...rest,
    //     };
    //   });
    // }

    const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
      const d3Updater = new D3GraphUpdater(setGraph, x, y);
      graphState.unexec(deltaToUndo, d3Updater);

      setDeltaGraphL0(deltaGraphL0 => deltaToUndo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaInactive', delta}), deltaGraphL0));
      setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}));
    };
    const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
      const d3Updater = new D3GraphUpdater(setGraph, x, y);
      graphState.exec(deltaToRedo, d3Updater);

      setDeltaGraphL0(deltaGraphL0 => deltaToRedo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaActive', delta}), deltaGraphL0));
      setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}));
    };
    const undo = (parentVersion, deltaToUndo) => {
      undoWithoutUpdatingHistoryGraph(deltaToUndo);
      currentVersion = parentVersion;

      setVersion(prevVersion => {
        setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
              {type: 'highlightVersion', version: prevVersion, bold: false}),
              {type: 'highlightVersion', version: parentVersion, bold: true}));
        return parentVersion;
      });
    };
    const redo = (childVersion, deltaToRedo) => {
      redoWithoutUpdatingHistoryGraph(deltaToRedo);
      currentVersion = childVersion;

      setVersion(prevVersion => {
        setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
            {type: 'highlightVersion', version: prevVersion, bold: false}),
            {type: 'highlightVersion', version: childVersion, bold: true}));
        return childVersion;
      });
    };
    const gotoVersion = (chosenVersion: Version) => {
      const path = currentVersion.findPathTo(chosenVersion);
      if (path === undefined) {
        throw new Error("Could not find path to version!");
      }
      for (const [linkType, delta] of path) {
        if (linkType === 'p') {
          undoWithoutUpdatingHistoryGraph(delta);
        }
        else if (linkType === 'c') {
          redoWithoutUpdatingHistoryGraph(delta);
        }
      }
      currentVersion = chosenVersion;
      setVersion(prevVersion => {
        setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
            {type: 'highlightVersion', version: prevVersion, bold: false}),
            {type: 'highlightVersion', version: chosenVersion, bold: true}));
        return chosenVersion;
      });
    };

    const reducer = {
      createVersion,
      gotoVersion,
      createAndGotoNewVersion,
      appendVersions,
      undo,
      redo,
    };

    // Components

    const defaultCallbacks = {
      onUserEdit: createAndGotoNewVersion,
      onUndoClicked: undo,
      onRedoClicked: redo,
      onVersionClicked: gotoVersion,
      onMerge: appendVersions,
    };

    const callbacks = Object.assign({}, defaultCallbacks, overridenCallbacks(reducer));

    const graphStateComponent = readonly ? 
          <GraphView graphData={graph} help={helpText.graphEditorReadonly} mouseUpHandler={()=>{}} />
        : <InfoHoverCardOverlay contents={helpText.graphEditor}>
            <D3GraphEditable
              graph={graph}
              graphState={graphState}
              forces={defaultGraphForces}
              setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
              onUserEdit={callbacks.onUserEdit}
            />
          </InfoHoverCardOverlay>;

    const deltaComponentProps = {
      help: helpText.deltaGraph,
      defaultRenderer: ('graphviz' as Renderer),
      mouseUpHandler: (e, {x,y}, node) => {
        if (node) {
          alert(JSON.stringify(node.obj.serialize(), null, 2));
        }
      },
    };
    const deltaGraphL0Component = <GraphView<Delta,null> graphData={deltaGraphL0} {...deltaComponentProps} />;
    const deltaGraphL1Component = <GraphView<Delta,null> graphData={deltaGraphL1} {...deltaComponentProps} />;

    const historyComponentWithMerge = <MergeView
      history={historyGraph}
      forces={defaultGraphForces}
      versionRegistry={versionRegistry}
      onMerge={outputs => callbacks.onMerge?.(outputs)}
      onGoto={version => callbacks.onVersionClicked?.(version)}
      appendVersions={reducer.appendVersions}
      {...{primitiveRegistry, compositeLevel, createVersion}}
    />;

    const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
      <RountangleEditor
        graph={graph}
        graphState={graphState}
        onUserEdit={callbacks.onUserEdit}
      />
    </InfoHoverCardOverlay>;

    const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
      if (parentsOrChildren.length === 0) {
        return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
      }
      if (parentsOrChildren.length === 1) {
        return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
      }
      return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionProps={{duration:0}}>
          <Mantine.Menu.Target>
            <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
          </Mantine.Menu.Target>
          <Mantine.Menu.Dropdown>
            {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
              <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
          </Mantine.Menu.Dropdown>
        </Mantine.Menu>;

    }
    const undoButton = makeUndoOrRedoButton(version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
    const redoButton = makeUndoOrRedoButton(version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);

    const undoRedoButtons = <>
      {undoButton}
      <Mantine.Space w="sm"/>
      {redoButton}
    </>;

    const stackedUndoButtons = version.parents.map(([parentVersion,deltaToUndo]) => {
      return (
        <div key={fullVersionId(parentVersion)}>
          <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconChevronLeft size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
            UNDO {deltaToUndo.getDescription()}
          </Mantine.Button>
          <Mantine.Space h="xs"/>
        </div>
      );
    });
    const stackedRedoButtons = version.children.map(([childVersion,deltaToRedo]) => {
      return (
        <div key={fullVersionId(childVersion)}>
          <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconChevronRight size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
            REDO {deltaToRedo.getDescription()}
          </Mantine.Button>
          <Mantine.Space h="xs"/>
        </div>
      );
    });
    const stackedUndoRedoButtons = (
      <Mantine.SimpleGrid cols={2}>
        <div>{stackedUndoButtons}</div>
        <div>{stackedRedoButtons}</div>
      </Mantine.SimpleGrid>
    );
    const makeTabs = (defaultTab: string, tabs: string[]) => {
      return <Mantine.Tabs defaultValue={defaultTab} keepMounted={false}>
        <Mantine.Tabs.List grow>
          {tabs.map(tab => ({
            editor: <Mantine.Tabs.Tab key={tab} value={tab}>Editor</Mantine.Tabs.Tab>,
            state:  <Mantine.Tabs.Tab key={tab} value={tab}>State</Mantine.Tabs.Tab>,
            merge: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
            deltaL1: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L1)</Mantine.Tabs.Tab>,
            deltaL0: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L0)</Mantine.Tabs.Tab>,
          }[tab]))}
        </Mantine.Tabs.List>
        <Mantine.Tabs.Panel value="state">
          {graphStateComponent}
        </Mantine.Tabs.Panel>
        <Mantine.Tabs.Panel value="editor">
          {rountangleEditor}
        </Mantine.Tabs.Panel>
        <Mantine.Tabs.Panel value="deltaL1">
          {deltaGraphL1Component}
        </Mantine.Tabs.Panel>
        <Mantine.Tabs.Panel value="deltaL0">
          {deltaGraphL0Component}
        </Mantine.Tabs.Panel>
        <Mantine.Tabs.Panel value="merge">
          {historyComponentWithMerge}
        </Mantine.Tabs.Panel>
      </Mantine.Tabs>;
    }


    return {
      state: {
        version,
      },
      reducer,
      components: {
        graphStateComponent,
        rountangleEditor,
        deltaGraphL1Component,
        deltaGraphL0Component,
        historyComponentWithMerge,
        undoButton,
        redoButton,
        undoRedoButtons,
        stackedUndoRedoButtons,
        makeTabs,
      },
    };
  }

  return {
    graphState,
    useOnion,
  }
}