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 {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
import {InfoHoverCardOverlay} from "../info_hover_card";

import {embed, Version, VersionRegistry} 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";

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 newVersionedModel({readonly}) {
  const versionRegistry = new 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;


  const initialState: VersionedModelState = {
    version: versionRegistry.initialVersion,
    graph: emptyGraph,
    historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
    deltaGraphL1: emptyGraph,
    deltaGraphL0: emptyGraph,
  }

  // 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;
  }

  // This function may only be called from a functional React component.
  // Given setState callback, returns:
  //  - Callback functions for updating the state
  //  - A callback that constructs all React components (to be used in React render function)
  function getReducer(setState) {

    // Create and add a new version, and its deltas, without changing the current version
    const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
      const composite = compositeLevel.createComposite(deltas, description);

      const parentVersion = versionRegistry.lookupOptional(parentHash);
      if (parentVersion !== undefined) {
        const newVersion = versionRegistry.createVersion(parentVersion, composite);

        setState(({historyGraph, deltaGraphL1, deltaGraphL0, ...rest}) => {
          return {
            // add new version to history graph:
            historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}),
            // add the composite delta to the L1-graph + highlight it as 'active':
            deltaGraphL1: composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1, // never add an empty composite
            // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
            deltaGraphL0: composite.deltas.reduce(
              (graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}),
              deltaGraphL0),
            ...rest,
          };
        });

        return newVersion;
      }
    };
    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion): Version => {
      const newVersion = addDeltasAndVersion(deltas, description, parentVersion.hash) as Version;
      gotoVersion(newVersion);
      return newVersion;
    };
    const appendVersion = (version: Version) => {
      setState(({historyGraph, ...rest}) => {
        return {
          // add new version to history graph:
          historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version}),
          ...rest,
        };
      });
    }

    // helper
    const setGraph = callback =>
      setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));

    const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
      const d3Updater = new D3GraphUpdater(setGraph, x, y);
      graphState.unexec(deltaToUndo, d3Updater);
      setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
        deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}),
        deltaGraphL0: deltaToUndo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaInactive', delta}),prevDeltaGraphL0),
        ...rest,
      }));
    };
    const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
      const d3Updater = new D3GraphUpdater(setGraph, x, y);
      graphState.exec(deltaToRedo, d3Updater);
      setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
        deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}),
        deltaGraphL0: deltaToRedo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaActive', delta}),
          prevDeltaGraphL0),
        ...rest,
      }));
    };
    const undo = (parentVersion, deltaToUndo) => {
      undoWithoutUpdatingHistoryGraph(deltaToUndo);
      currentVersion = parentVersion;
      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
        version: parentVersion,
        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
            {type: 'highlightVersion', version: prevVersion, bold: false}),
            {type: 'highlightVersion', version: parentVersion, bold: true}),
        ...rest,
      }));
    };
    const redo = (childVersion, deltaToRedo) => {
      redoWithoutUpdatingHistoryGraph(deltaToRedo);
      currentVersion = childVersion;
      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
        version: childVersion,
        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
          {type: 'highlightVersion', version: prevVersion, bold: false}),
          {type: 'highlightVersion', version: childVersion, bold: true}),
        ...rest,
      }));
    };
    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;
      setState(({historyGraph, version: oldVersion, ...rest}) => ({
        version: chosenVersion,
        historyGraph: historyGraphReducer(historyGraphReducer(historyGraph,
          {type: 'highlightVersion', version: oldVersion, bold: false}),
          {type: 'highlightVersion', version: chosenVersion, bold: true}),
        ...rest,
      }));
    };

    return {
      addDeltasAndVersion,
      gotoVersion,
      createAndGotoNewVersion,
      appendVersion,
      undo,
      redo,
    };
  }

  function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) {
    const graphStateComponent = <InfoHoverCardOverlay
      contents={readonly ? helpText.graphEditorReadonly : helpText.graphEditor}>
        {readonly ? 
          <D3Graph graph={state.graph} forces={defaultGraphForces} />
        : <D3GraphEditable
            graph={state.graph}
            graphState={graphState}
            forces={defaultGraphForces}
            setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
            onUserEdit={callbacks.onUserEdit}
          />}
    </InfoHoverCardOverlay>;

    const deltaGraphL1Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
      <D3Graph graph={state.deltaGraphL1} forces={defaultGraphForces} />
    </InfoHoverCardOverlay>;

    const deltaGraphL0Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
      <D3Graph graph={state.deltaGraphL0} forces={defaultGraphForces} />
    </InfoHoverCardOverlay>;

    const historyComponent = <InfoHoverCardOverlay contents={helpText.historyGraph}>
      <D3Graph graph={state.historyGraph} forces={defaultGraphForces}
        mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />
    </InfoHoverCardOverlay>;

    const historyComponentWithMerge = <MergeView
      history={state.historyGraph}
      forces={defaultGraphForces}
      versionRegistry={versionRegistry}
      onMerge={outputs => callbacks.onMerge?.(outputs)}
      onGoto={version => callbacks.onVersionClicked?.(version)} />

    const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
      <RountangleEditor
        graph={state.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} transitionDuration={0}>
          <Mantine.Menu.Target>
            <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
          </Mantine.Menu.Target>
          <Mantine.Menu.Dropdown>
            {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
            {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(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
    const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);

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

    const stackedUndoButtons = state.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 = state.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>,
            history: <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="history">
          {historyComponent}
        </Mantine.Tabs.Panel>
      </Mantine.Tabs>;
    }

    // React components:
    return {
      graphStateComponent,
      rountangleEditor,
      deltaGraphL1Component,
      deltaGraphL0Component,
      historyComponent,
      historyComponentWithMerge,
      undoButton,
      redoButton,
      undoRedoButtons,
      stackedUndoRedoButtons,
      makeTabs,
    };
  }

  // State, reducers, etc.
  return {
    initialState,
    graphState,
    versionRegistry,
    getCurrentVersion,
    getReducer,
    getReactComponents,
  };
}