import type { TextRangePoint, TextSelection } from '@blockeditor/block-std';
import { assertExists } from '@blockeditor/global/utils';
import type { BlockElement, EditorHost } from '@blockeditor/lit';
import {
  type BlockModel,
  type BlockSnapshot,
  type DeltaOperation,
  fromJSON,
  type JobMiddleware,
  type SliceSnapshot,
  type Text,
  Workspace,
} from '@blockeditor/store';

import { matchFlavours } from '../../../_common/utils/index.js';
import type { CodeBlockModel } from '../../../models.js';
import type { ParagraphBlockModel } from '../../../paragraph-block/index.js';

const findLast = (snapshot: BlockSnapshot): BlockSnapshot => {
  if (snapshot.children && snapshot.children.length > 0) {
    return findLast(snapshot.children[snapshot.children.length - 1]);
  }
  return snapshot;
};

class PointState {
  readonly block: BlockElement;
  readonly text: Text;
  readonly model: BlockModel;
  constructor(
    readonly std: EditorHost['std'],
    readonly point: TextRangePoint
  ) {
    this.block = this._blockFromPath(point.path);
    this.model = this.block.model;
    const text = this.model.text;
    assertExists(text);
    this.text = text;
  }

  private _blockFromPath = (path: string[]) => {
    const block = this.std.view.viewFromPath('block', path);
    assertExists(block);
    return block;
  };
}

class PasteTr {
  private readonly lastIndex: number;
  private readonly fromPointState: PointState;
  private readonly endPointState: PointState;
  private readonly to: TextRangePoint | null;
  private readonly firstSnapshot: BlockSnapshot;
  private lastSnapshot: BlockSnapshot;
  private readonly firstSnapshotIsPlainText: boolean;
  constructor(
    public readonly std: EditorHost['std'],
    public readonly text: TextSelection,
    public readonly snapshot: SliceSnapshot
  ) {
    const { from, to } = text;
    const end = to ?? from;

    this.to = to;

    this.fromPointState = new PointState(std, from);
    this.endPointState = new PointState(std, end);

    this.firstSnapshot = snapshot.content[0];
    this.lastSnapshot = findLast(snapshot.content[snapshot.content.length - 1]);
    if (
      this.firstSnapshot !== this.lastSnapshot &&
      this.lastSnapshot.props.text
    ) {
      const text = fromJSON(this.lastSnapshot.props.text) as Text;
      const doc = new Workspace.Y.Doc();
      const temp = doc.getMap('temp');
      temp.set('text', text.yText);
      this.lastIndex = text.length;
    } else {
      this.lastIndex = this.endPointState.text.length - end.index - end.length;
    }
    this.firstSnapshotIsPlainText =
      this.firstSnapshot.flavour === 'affine:paragraph' &&
      this.firstSnapshot.props.type === 'text';
  }

  canMerge = () => {
    const firstTextSnapshot = this._textFromSnapshot(this.firstSnapshot);
    const lastTextSnapshot = this._textFromSnapshot(this.lastSnapshot);
    return (
      firstTextSnapshot &&
      lastTextSnapshot &&
      ((this.fromPointState.text.length > 0 &&
        this.endPointState.text.length > 0) ||
        this.firstSnapshotIsPlainText)
    );
  };

  private _textFromSnapshot = (snapshot: BlockSnapshot) => {
    return snapshot.props.text as Record<'delta', DeltaOperation[]>;
  };

  private _getDeltas = () => {
    const firstTextSnapshot = this._textFromSnapshot(this.firstSnapshot);
    const lastTextSnapshot = this._textFromSnapshot(this.lastSnapshot);
    const fromDelta = this.fromPointState.text.sliceToDelta(
      0,
      this.fromPointState.point.index
    );
    const toDelta = this.endPointState.text.sliceToDelta(
      this.endPointState.point.index + this.endPointState.point.length,
      this.endPointState.text.length
    );
    const firstDelta = firstTextSnapshot.delta;
    const lastDelta = lastTextSnapshot.delta;
    return {
      firstTextSnapshot,
      lastTextSnapshot,
      fromDelta,
      toDelta,
      firstDelta,
      lastDelta,
    };
  };

  private _mergeCode = () => {
    const { firstTextSnapshot, fromDelta, toDelta } = this._getDeltas();

    this.firstSnapshot.flavour = this.fromPointState.model.flavour;
    const toLanguage = (this.fromPointState.model as CodeBlockModel).language;
    if (toLanguage !== 'Plain Text') {
      this.firstSnapshot.props.language = toLanguage;
    }
    const deltas: DeltaOperation[] = [...fromDelta];
    let i = 0;
    for (const blockSnapshot of this.snapshot.content) {
      if (blockSnapshot.props.text) {
        const text = this._textFromSnapshot(blockSnapshot);
        if (i > 0) {
          deltas.push({ insert: '\n' });
        }
        deltas.push(...text.delta);
        i++;
      } else {
        break;
      }
    }
    firstTextSnapshot.delta = deltas.concat(toDelta);
    this.snapshot.content.splice(1, i);
    this.lastSnapshot = findLast(
      this.snapshot.content[this.snapshot.content.length - 1]
    );
  };

  private _mergeSingle = () => {
    this.firstSnapshot.flavour = this.fromPointState.model.flavour;
    if (
      this.firstSnapshot.props.type &&
      (this.fromPointState.text.length > 0 || this.firstSnapshotIsPlainText)
    ) {
      this.firstSnapshot.props.type = (
        this.fromPointState.model as ParagraphBlockModel
      ).type;
    }
    const { firstTextSnapshot, fromDelta, toDelta, firstDelta } =
      this._getDeltas();

    firstTextSnapshot.delta = [...fromDelta, ...firstDelta, ...toDelta];
  };

  private _mergeMultiple = () => {
    this.firstSnapshot.flavour = this.fromPointState.model.flavour;
    if (
      this.firstSnapshot.props.type &&
      (this.fromPointState.text.length > 0 || this.firstSnapshotIsPlainText)
    ) {
      this.firstSnapshot.props.type = (
        this.fromPointState.model as ParagraphBlockModel
      ).type;
    }
    if (this.lastSnapshot.props.type && this.to) {
      this.lastSnapshot.flavour = this.endPointState.model.flavour;
      this.lastSnapshot.props.type = (
        this.endPointState.model as ParagraphBlockModel
      ).type;
    }

    const {
      firstTextSnapshot,
      lastTextSnapshot,
      fromDelta,
      toDelta,
      firstDelta,
      lastDelta,
    } = this._getDeltas();

    firstTextSnapshot.delta = [...fromDelta, ...firstDelta];
    lastTextSnapshot.delta = [...lastDelta, ...toDelta];
  };

  pasted = () => {
    const needCleanup = this.canMerge() || this.endPointState.text.length === 0;
    if (!needCleanup) {
      return;
    }

    const lastModel = this.std.page.getBlockById(this.lastSnapshot.id);
    assertExists(lastModel);

    if (!this.to) {
      this.std.page.deleteBlock(this.fromPointState.model, {
        bringChildrenTo: lastModel,
      });

      return;
    }

    this.std.page.deleteBlock(
      this.endPointState.model,
      lastModel
        ? {
            bringChildrenTo: lastModel,
          }
        : undefined
    );
    const parent = this.std.page.getParent(this.fromPointState.model);
    this.std.page.deleteBlock(
      this.fromPointState.model,
      parent
        ? {
            bringChildrenTo: parent,
          }
        : undefined
    );
  };

  focusPasted = () => {
    const host = this.std.host as EditorHost;
    const parentBlockElement = this.fromPointState.block.parentBlockElement;
    const lastModel = this.std.page.getBlockById(this.lastSnapshot.id);
    assertExists(lastModel);

    host.updateComplete
      .then(() => {
        const target = parentBlockElement.querySelector<BlockElement>(
          `[${host.blockIdAttr}="${lastModel.id}"]`
        );
        assertExists(target);
        if (!lastModel.text) {
          if (matchFlavours(lastModel, ['affine:image'])) {
            const selection = this.std.selection.create('image', {
              path: target.path,
            });
            this.std.selection.setGroup('note', [selection]);
            return;
          }
          const selection = this.std.selection.create('block', {
            path: target.path,
          });
          this.std.selection.setGroup('note', [selection]);
          return;
        }
        const selection = this.std.selection.create('text', {
          from: {
            path: target.path,
            index:
              this.firstSnapshot === this.lastSnapshot
                ? lastModel.text
                  ? lastModel.text.length - this.lastIndex
                  : 0
                : this.lastIndex,
            length: 0,
          },
          to: null,
        });
        this.std.selection.setGroup('note', [selection]);
      })
      .catch(console.error);
  };

  merge() {
    if (this.fromPointState.model.flavour === 'affine:code' && !this.to) {
      this._mergeCode();
      return;
    }

    if (this.firstSnapshot === this.lastSnapshot) {
      this._mergeSingle();
      return;
    }

    this._mergeMultiple();
  }
}

function flatNote(snapshot: SliceSnapshot) {
  if (snapshot.content[0]?.flavour === 'affine:note') {
    snapshot.content = snapshot.content[0].children;
  }
}

export const pasteMiddleware = (std: EditorHost['std']): JobMiddleware => {
  return ({ slots }) => {
    let tr: PasteTr | undefined;
    slots.beforeImport.on(payload => {
      if (payload.type === 'slice') {
        const { snapshot } = payload;
        flatNote(snapshot);

        const text = std.selection.find('text');
        if (!text) {
          return;
        }
        tr = new PasteTr(std, text, payload.snapshot);
        if (tr.canMerge()) {
          tr.merge();
        }
      }
    });
    slots.afterImport.on(payload => {
      if (tr && payload.type === 'slice') {
        tr.pasted();
        tr.focusPasted();
      }
    });
  };
};
