import {
  Mark as ProseMirrorMark,
  Node as ProseMirrorNode,
  NodeType,
  ParseOptions,
} from '@tiptap/pm/model'
import { EditorState, Transaction } from '@tiptap/pm/state'
import {
  Decoration, EditorProps, EditorView, NodeView,
} from '@tiptap/pm/view'

import { Editor } from './Editor.js'
import { Extension } from './Extension.js'
import {
  Commands, ExtensionConfig, MarkConfig, NodeConfig,
} from './index.js'
import { Mark } from './Mark.js'
import { Node } from './Node.js'

export type AnyConfig = ExtensionConfig | NodeConfig | MarkConfig;
export type AnyExtension = Extension | Node | Mark;
export type Extensions = AnyExtension[];

export type ParentConfig<T> = Partial<{
  [P in keyof T]: Required<T>[P] extends (...args: any) => any
    ? (...args: Parameters<Required<T>[P]>) => ReturnType<Required<T>[P]>
    : T[P];
}>;

export type Primitive = null | undefined | string | number | boolean | symbol | bigint;

export type RemoveThis<T> = T extends (...args: any) => any
  ? (...args: Parameters<T>) => ReturnType<T>
  : T;

export type MaybeReturnType<T> = T extends (...args: any) => any ? ReturnType<T> : T;

export type MaybeThisParameterType<T> = Exclude<T, Primitive> extends (...args: any) => any
  ? ThisParameterType<Exclude<T, Primitive>>
  : any;

export interface EditorEvents {
  beforeCreate: { editor: Editor };
  create: { editor: Editor };
  contentError: {
    editor: Editor;
    error: Error;
    /**
     * If called, will re-initialize the editor with the collaboration extension removed.
     * This will prevent syncing back deletions of content not present in the current schema.
     */
    disableCollaboration: () => void;
  };
  update: { editor: Editor; transaction: Transaction };
  selectionUpdate: { editor: Editor; transaction: Transaction };
  beforeTransaction: { editor: Editor; transaction: Transaction; nextState: EditorState };
  transaction: { editor: Editor; transaction: Transaction };
  focus: { editor: Editor; event: FocusEvent; transaction: Transaction };
  blur: { editor: Editor; event: FocusEvent; transaction: Transaction };
  destroy: void;
}

export type EnableRules = (AnyExtension | string)[] | boolean;

export interface EditorOptions {
  element: Element;
  content: Content;
  extensions: Extensions;
  injectCSS: boolean;
  injectNonce: string | undefined;
  autofocus: FocusPosition;
  editable: boolean;
  editorProps: EditorProps;
  parseOptions: ParseOptions;
  coreExtensionOptions?: {
    clipboardTextSerializer?: {
      blockSeparator?: string;
    };
  };
  enableInputRules: EnableRules;
  enablePasteRules: EnableRules;
  enableCoreExtensions: boolean;
  /**
   * If `true`, the editor will check the content for errors on initialization.
   * Emitting the `contentError` event if the content is invalid.
   * Which can be used to show a warning or error message to the user.
   * @default false
   */
  enableContentCheck: boolean;
  onBeforeCreate: (props: EditorEvents['beforeCreate']) => void;
  onCreate: (props: EditorEvents['create']) => void;
  /**
   * Called when the editor encounters an error while parsing the content.
   * Only enabled if `enableContentCheck` is `true`.
   */
  onContentError: (props: EditorEvents['contentError']) => void;
  onUpdate: (props: EditorEvents['update']) => void;
  onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void;
  onTransaction: (props: EditorEvents['transaction']) => void;
  onFocus: (props: EditorEvents['focus']) => void;
  onBlur: (props: EditorEvents['blur']) => void;
  onDestroy: (props: EditorEvents['destroy']) => void;
}

export type HTMLContent = string;

export type JSONContent = {
  type?: string;
  attrs?: Record<string, any>;
  content?: JSONContent[];
  marks?: {
    type: string;
    attrs?: Record<string, any>;
    [key: string]: any;
  }[];
  text?: string;
  [key: string]: any;
};

export type Content = HTMLContent | JSONContent | JSONContent[] | null;

export type CommandProps = {
  editor: Editor;
  tr: Transaction;
  commands: SingleCommands;
  can: () => CanCommands;
  chain: () => ChainedCommands;
  state: EditorState;
  view: EditorView;
  dispatch: ((args?: any) => any) | undefined;
};

export type Command = (props: CommandProps) => boolean;

export type CommandSpec = (...args: any[]) => Command;

export type KeyboardShortcutCommand = (props: { editor: Editor }) => boolean;

export type Attribute = {
  default?: any;
  rendered?: boolean;
  renderHTML?: ((attributes: Record<string, any>) => Record<string, any> | null) | null;
  parseHTML?: ((element: HTMLElement) => any | null) | null;
  keepOnSplit?: boolean;
  isRequired?: boolean;
};

export type Attributes = {
  [key: string]: Attribute;
};

export type ExtensionAttribute = {
  type: string;
  name: string;
  attribute: Required<Attribute>;
};

export type GlobalAttributes = {
  /**
   * The node & mark types this attribute should be applied to.
   */
  types: string[];
  /**
   * The attributes to add to the node or mark types.
   */
  attributes: Record<string, Attribute | undefined>;
}[];

export type PickValue<T, K extends keyof T> = T[K];

export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

export type Diff<T extends keyof any, U extends keyof any> = ({ [P in T]: P } & {
  [P in U]: never;
} & { [x: string]: never })[T];

export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;

export type ValuesOf<T> = T[keyof T];

export type KeysWithTypeOf<T, Type> = { [P in keyof T]: T[P] extends Type ? P : never }[keyof T];

export type DecorationWithType = Decoration & {
  type: NodeType;
};

export type NodeViewProps = {
  editor: Editor;
  node: ProseMirrorNode;
  decorations: DecorationWithType[];
  selected: boolean;
  extension: Node;
  getPos: () => number;
  updateAttributes: (attributes: Record<string, any>) => void;
  deleteNode: () => void;
};

export interface NodeViewRendererOptions {
  stopEvent: ((props: { event: Event }) => boolean) | null;
  ignoreMutation:
    | ((props: { mutation: MutationRecord | { type: 'selection'; target: Element } }) => boolean)
    | null;
  contentDOMElementTag: string;
}

export type NodeViewRendererProps = {
  editor: Editor;
  node: ProseMirrorNode;
  getPos: (() => number) | boolean;
  HTMLAttributes: Record<string, any>;
  decorations: Decoration[];
  extension: Node;
};

export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView | {};

export type AnyCommands = Record<string, (...args: any[]) => Command>;

export type UnionCommands<T = Command> = UnionToIntersection<
  ValuesOf<Pick<Commands<T>, KeysWithTypeOf<Commands<T>, {}>>>
>;

export type RawCommands = {
  [Item in keyof UnionCommands]: UnionCommands<Command>[Item];
};

export type SingleCommands = {
  [Item in keyof UnionCommands]: UnionCommands<boolean>[Item];
};

export type ChainedCommands = {
  [Item in keyof UnionCommands]: UnionCommands<ChainedCommands>[Item];
} & {
  run: () => boolean;
};

export type CanCommands = SingleCommands & { chain: () => ChainedCommands };

export type FocusPosition = 'start' | 'end' | 'all' | number | boolean | null;

export type Range = {
  from: number;
  to: number;
};

export type NodeRange = {
  node: ProseMirrorNode;
  from: number;
  to: number;
};

export type MarkRange = {
  mark: ProseMirrorMark;
  from: number;
  to: number;
};

export type Predicate = (node: ProseMirrorNode) => boolean;

export type NodeWithPos = {
  node: ProseMirrorNode;
  pos: number;
};

export type TextSerializer = (props: {
  node: ProseMirrorNode;
  pos: number;
  parent: ProseMirrorNode;
  index: number;
  range: Range;
}) => string;

export type ExtendedRegExpMatchArray = RegExpMatchArray & {
  data?: Record<string, any>;
};

export type Dispatch = ((args?: any) => any) | undefined;
