import { Editor, Location, Node, Path, Point, Transforms } from 'slate';
import invariant from 'tiny-invariant';
import * as Y from 'yjs';
import { CustomNode } from '../src/model';
import { YjsEditor } from '../src/plugin/yjs-editor';

export interface TestEditor extends YjsEditor {
  shouldCaptureYjsUpdates: boolean;
  capturedYjsUpdates: Uint8Array[];
}

export type TransformFunc = (e: Editor) => void;

export const TestEditor = {
  /**
   * Capture Yjs updates generated by this editor.
   */
  captureYjsUpdate: (e: TestEditor, update: Uint8Array): void => {
    if (!e.shouldCaptureYjsUpdates) return;
    e.capturedYjsUpdates.push(update);
  },

  /**
   * Return captured Yjs updates.
   */
  getCapturedYjsUpdates: (e: TestEditor): Uint8Array[] => {
    const result = e.capturedYjsUpdates;
    e.capturedYjsUpdates = [];
    return result;
  },

  /**
   * Apply one Yjs update to Yjs.
   */
  applyYjsUpdateToYjs: (e: TestEditor, update: Uint8Array): void => {
    e.shouldCaptureYjsUpdates = false;
    invariant(e.sharedType.doc, 'Shared type should be bound to a document');
    Y.applyUpdate(e.sharedType.doc, update);
    e.shouldCaptureYjsUpdates = true;
  },

  /**
   * Apply multiple Yjs updates to Yjs.
   */
  applyYjsUpdatesToYjs: (e: TestEditor, updates: Uint8Array[]): void => {
    updates.forEach((update) => {
      TestEditor.applyYjsUpdateToYjs(e, update);
    });
  },

  /**
   * Apply one TransformFunc to slate.
   */
  applyTransform: (e: TestEditor, transform: TransformFunc): void => {
    transform(e);
  },

  /**
   * Apply multiple TransformFuncs to slate.
   */
  applyTransforms: (e: TestEditor, transforms: TransformFunc[]): void => {
    transforms.forEach((transform) => {
      TestEditor.applyTransform(e, transform);
    });
  },

  makeInsertText: (text: string, at: Location): TransformFunc => {
    return (e: Editor) => {
      Transforms.insertText(e, text, { at });
    };
  },

  makeRemoveCharacters: (count: number, at: Location): TransformFunc => {
    return (e: Editor) => {
      Transforms.delete(e, { distance: count, at });
    };
  },

  makeInsertNodes: (nodes: (Node | CustomNode) | (Node | CustomNode)[], at: Location): TransformFunc => {
    return (e: Editor) => {
      Transforms.insertNodes(e, nodes as Node, { at });
    };
  },

  makeMergeNodes: (at: Path): TransformFunc => {
    return (e: Editor) => {
      Transforms.mergeNodes(e, { at });
    };
  },

  makeMoveNodes: (from: Path, to: Path): TransformFunc => {
    return (e: Editor) => {
      Transforms.moveNodes(e, { at: from, to });
    };
  },

  makeRemoveNodes: (at: Path): TransformFunc => {
    return (e: Editor) => {
      Transforms.removeNodes(e, { at });
    };
  },

  makeSetNodes: (at: Location, props: Partial<CustomNode>): TransformFunc => {
    return (e: Editor) => {
      Transforms.setNodes(e, props, { at });
    };
  },

  makeSplitNodes: (at: Location): TransformFunc => {
    return (e: Editor) => {
      Transforms.splitNodes(e, { at });
    };
  },

  makeSetSelection: (anchor: Point, focus: Point): TransformFunc => {
    return (e: Editor) => {
      Transforms.setSelection(e, { anchor, focus });
    };
  }
};

export function withTest<T extends YjsEditor>(editor: T): T & TestEditor {
  const e = editor as T & TestEditor;

  invariant(e.sharedType.doc, 'Shared type should be bound to a document');
  e.sharedType.doc.on('update', (updateMessage: Uint8Array) => {
    TestEditor.captureYjsUpdate(e, updateMessage);
  });

  e.shouldCaptureYjsUpdates = true;
  e.capturedYjsUpdates = [];

  return e;
}
