import { BlockSuiteError } from '@blocksuite/global/exceptions';

import type { Doc } from '../store/index.js';
import type { AssetsManager } from '../transformer/assets.js';
import type { DraftModel, Job, Slice } from '../transformer/index.js';
import type {
  BlockSnapshot,
  DocSnapshot,
  SliceSnapshot,
} from '../transformer/type.js';

import { ASTWalkerContext } from './context.js';

export type FromDocSnapshotPayload = {
  snapshot: DocSnapshot;
  assets?: AssetsManager;
};
export type FromBlockSnapshotPayload = {
  snapshot: BlockSnapshot;
  assets?: AssetsManager;
};
export type FromSliceSnapshotPayload = {
  snapshot: SliceSnapshot;
  assets?: AssetsManager;
};
export type FromDocSnapshotResult<Target> = {
  file: Target;
  assetsIds: string[];
};
export type FromBlockSnapshotResult<Target> = {
  file: Target;
  assetsIds: string[];
};
export type FromSliceSnapshotResult<Target> = {
  file: Target;
  assetsIds: string[];
};
export type ToDocSnapshotPayload<Target> = {
  file: Target;
  assets?: AssetsManager;
};
export type ToBlockSnapshotPayload<Target> = {
  file: Target;
  assets?: AssetsManager;
};
export type ToSliceSnapshotPayload<Target> = {
  file: Target;
  assets?: AssetsManager;
};

export function wrapFakeNote(snapshot: SliceSnapshot) {
  if (snapshot.content[0]?.flavour !== 'affine:note') {
    snapshot.content = [
      {
        type: 'block',
        id: '',
        flavour: 'affine:note',
        props: {},
        children: snapshot.content,
      },
    ];
  }
}

export abstract class BaseAdapter<AdapterTarget = unknown> {
  job: Job;

  get configs() {
    return this.job.adapterConfigs;
  }

  constructor(job: Job) {
    this.job = job;
  }

  async fromBlock(model: DraftModel) {
    try {
      const blockSnapshot = this.job.blockToSnapshot(model);
      if (!blockSnapshot) return;
      return await this.fromBlockSnapshot({
        snapshot: blockSnapshot,
        assets: this.job.assetsManager,
      });
    } catch (error) {
      console.error('Cannot convert block to snapshot');
      console.error(error);
      return;
    }
  }

  abstract fromBlockSnapshot(
    payload: FromBlockSnapshotPayload
  ):
    | Promise<FromBlockSnapshotResult<AdapterTarget>>
    | FromBlockSnapshotResult<AdapterTarget>;

  async fromDoc(doc: Doc) {
    try {
      const docSnapshot = this.job.docToSnapshot(doc);
      if (!docSnapshot) return;
      return await this.fromDocSnapshot({
        snapshot: docSnapshot,
        assets: this.job.assetsManager,
      });
    } catch (error) {
      console.error('Cannot convert doc to snapshot');
      console.error(error);
      return;
    }
  }

  abstract fromDocSnapshot(
    payload: FromDocSnapshotPayload
  ):
    | Promise<FromDocSnapshotResult<AdapterTarget>>
    | FromDocSnapshotResult<AdapterTarget>;

  async fromSlice(slice: Slice) {
    try {
      const sliceSnapshot = this.job.sliceToSnapshot(slice);
      if (!sliceSnapshot) return;
      wrapFakeNote(sliceSnapshot);
      return await this.fromSliceSnapshot({
        snapshot: sliceSnapshot,
        assets: this.job.assetsManager,
      });
    } catch (error) {
      console.error('Cannot convert slice to snapshot');
      console.error(error);
      return;
    }
  }

  abstract fromSliceSnapshot(
    payload: FromSliceSnapshotPayload
  ):
    | Promise<FromSliceSnapshotResult<AdapterTarget>>
    | FromSliceSnapshotResult<AdapterTarget>;

  async toBlock(
    payload: ToBlockSnapshotPayload<AdapterTarget>,
    doc: Doc,
    parent?: string,
    index?: number
  ) {
    try {
      const snapshot = await this.toBlockSnapshot(payload);
      if (!snapshot) return;
      return await this.job.snapshotToBlock(snapshot, doc, parent, index);
    } catch (error) {
      console.error('Cannot convert block snapshot to block');
      console.error(error);
      return;
    }
  }

  abstract toBlockSnapshot(
    payload: ToBlockSnapshotPayload<AdapterTarget>
  ): Promise<BlockSnapshot> | BlockSnapshot;

  async toDoc(payload: ToDocSnapshotPayload<AdapterTarget>) {
    try {
      const snapshot = await this.toDocSnapshot(payload);
      if (!snapshot) return;
      return await this.job.snapshotToDoc(snapshot);
    } catch (error) {
      console.error('Cannot convert doc snapshot to doc');
      console.error(error);
      return;
    }
  }

  abstract toDocSnapshot(
    payload: ToDocSnapshotPayload<AdapterTarget>
  ): Promise<DocSnapshot> | DocSnapshot;

  async toSlice(
    payload: ToSliceSnapshotPayload<AdapterTarget>,
    doc: Doc,
    parent?: string,
    index?: number
  ) {
    try {
      const snapshot = await this.toSliceSnapshot(payload);
      if (!snapshot) return;
      return await this.job.snapshotToSlice(snapshot, doc, parent, index);
    } catch (error) {
      console.error('Cannot convert slice snapshot to slice');
      console.error(error);
      return;
    }
  }

  abstract toSliceSnapshot(
    payload: ToSliceSnapshotPayload<AdapterTarget>
  ): Promise<SliceSnapshot | null> | SliceSnapshot | null;
}

type Keyof<T> = T extends unknown ? keyof T : never;

type WalkerFn<ONode extends object, TNode extends object> = (
  o: NodeProps<ONode>,
  context: ASTWalkerContext<TNode>
) => Promise<void> | void;

export type NodeProps<Node extends object> = {
  node: Node;
  next?: Node | null;
  parent: NodeProps<Node> | null;
  prop: Keyof<Node> | null;
  index: number | null;
};

// Ported from https://github.com/Rich-Harris/estree-walker MIT License
export class ASTWalker<ONode extends object, TNode extends object | never> {
  private _enter: WalkerFn<ONode, TNode> | undefined;

  private _isONode!: (node: unknown) => node is ONode;

  private _leave: WalkerFn<ONode, TNode> | undefined;

  private _visit = async (o: NodeProps<ONode>) => {
    if (!o.node) return;
    this.context._skipChildrenNum = 0;
    this.context._skip = false;

    if (this._enter) {
      await this._enter(o, this.context);
    }

    if (this.context._skip) {
      return;
    }

    for (const key in o.node) {
      const value = o.node[key];

      if (value && typeof value === 'object') {
        if (Array.isArray(value)) {
          for (
            let i = this.context._skipChildrenNum;
            i < value.length;
            i += 1
          ) {
            const item = value[i];
            if (
              item !== null &&
              typeof item === 'object' &&
              this._isONode(item)
            ) {
              const nextItem = value[i + 1] ?? null;
              await this._visit({
                node: item,
                next: nextItem,
                parent: o,
                prop: key as unknown as Keyof<ONode>,
                index: i,
              });
            }
          }
        } else if (
          this.context._skipChildrenNum === 0 &&
          this._isONode(value)
        ) {
          await this._visit({
            node: value,
            next: null,
            parent: o,
            prop: key as unknown as Keyof<ONode>,
            index: null,
          });
        }
      }
    }

    if (this._leave) {
      await this._leave(o, this.context);
    }
  };

  private context: ASTWalkerContext<TNode>;

  setEnter = (fn: WalkerFn<ONode, TNode>) => {
    this._enter = fn;
  };

  setLeave = (fn: WalkerFn<ONode, TNode>) => {
    this._leave = fn;
  };

  setONodeTypeGuard = (fn: (node: unknown) => node is ONode) => {
    this._isONode = fn;
  };

  walk = async (oNode: ONode, tNode: TNode) => {
    this.context.openNode(tNode);
    await this._visit({ node: oNode, parent: null, prop: null, index: null });
    if (this.context.stack.length !== 1) {
      throw new BlockSuiteError(1, 'There are unclosed nodes');
    }
    return this.context.currentNode();
  };

  walkONode = async (oNode: ONode) => {
    await this._visit({ node: oNode, parent: null, prop: null, index: null });
  };

  constructor() {
    this.context = new ASTWalkerContext<TNode>();
  }
}
