
import { dropCursor } from "prosemirror-dropcursor";
import {
  customElement, h, useEffect, useHost, useRef, useState, useEvent,
} from "atomico";
import { gapCursor } from "prosemirror-gapcursor";
import { keymap } from "prosemirror-keymap";
import { baseKeymap } from "prosemirror-commands";
import { undoInputRule, inputRules } from "prosemirror-inputrules";
import { Schema, DOMParser, DOMSerializer } from "prosemirror-model";
import { undo, redo, history } from "prosemirror-history";
import javascript from "highlight.js/lib/languages/javascript";
import python from "highlight.js/lib/languages/python";
import scala from "highlight.js/lib/languages/scala";
import css from "highlight.js/lib/languages/css";

import { useStylesheet } from "@caedman/kit/use-stylesheet";
import { EditorView } from "prosemirror-view";
import {
  Plugin,
  EditorState, PluginKey, TextSelection,
} from "prosemirror-state";

import Title from "./Nodes/Title";
import {
  Doc, DocTitle, Paragraph, Text,
} from "./Nodes";
import {
  Emitter, camelCase, minMax, ExtensionManager,
} from "./Utils";

import { getNodeAttrs, markIsActive, nodeIsActive } from "../utils";
import {
  Blockquote,
  Bold,
  BulletList,
  Image,
  Heading, Italic, Link,
  ListItem,
  OrderedList, Strike,
  TodoItem,
  TodoList, Underline, HardBreak, CodeBlockHighlight, Placeholder, KunIconP, KunUrl,
} from "../extenstions";
import MenuBar from "./Plugins/MenuBar";
import FloatingMenu from "./Plugins/FloatingMenu";
import ImageView from "../views/image";
import TodoView from "../views/todo";

const iOptionNoTitle = {
  emptyDocument: {
    type: "doc",
    content: [
      {
        type: "paragraph",
        content: [
          {
            type: "text",
            text: "This is some inserted text. 👋",
          },
        ],
      },
    ],
  },
  parseOptions: {},
};

const iOptionTitle = {
  emptyDocument: {
    type: "doc",
    content: [{
      type: "title",
      content: [
        {
          type: "text",
          text: "xxxxxxxxx",
        },
      ],
    },
    {
      type: "paragraph",
      content: [
        {
          type: "text",
          text: "This is some inserted text. 👋",
        },
      ],
    },

    ],
  },
  parseOptions: {},
};


const createDocument = (schema, content, hasTitle: boolean) => {
  console.log("content", content);
  console.log("schema", schema);
  if (content == null) {
    return schema.nodeFromJSON(hasTitle ? iOptionTitle.emptyDocument : iOptionNoTitle.emptyDocument);
  }

  if (typeof content === "object") {
    try {
      return schema.nodeFromJSON(content);
    } catch (error) {
      console.log(error);
      console.log("xxxxxxxxxxxxxxxxxxxxxxxx1");
      console.log(error);
      console.warn("[tiptap warn]: Invalid content.", "Passed value:", content, "Error:", error);
      return schema.nodeFromJSON(hasTitle ? iOptionTitle.emptyDocument : iOptionNoTitle.emptyDocument);
    }
  }

  if (typeof content === "string") {
    const element = document.createElement("div");
    element.innerHTML = content.trim();

    return DOMParser.fromSchema(schema).parse(element, {});
  }

  return false;
};
export default class PEditor implements Emitter {
    _callbacks: unknown;

    _menu: {
        isActive: false;
        left: 0;
        bottom: 0;
    }

    set menu(menu) {
      this._menu = menu;
    }

    get menu() {
      return this._menu;
    }

    on(event, fn) {
      this._callbacks = this._callbacks || {};
      // Create namespace for this event
      if (!this._callbacks[event]) {
        this._callbacks[event] = [];
      }
      this._callbacks[event].push(fn);
      return this;
    }

    emit(event, ...args) {
      this._callbacks = this._callbacks || {};
      const callbacks = this._callbacks[event];

      if (callbacks) {
        callbacks.forEach((callback) => callback.apply(this, args));
      }

      return this;
    }


    // Remove event listener for given event.
    // If fn is not provided, all event listeners for that event will be removed.
    // If neither is provided, all event listeners will be removed.
    off(event, fn) {
      if (!arguments.length) {
        this._callbacks = {};
      } else {
        // event listeners for the given event
        const callbacks = this._callbacks ? this._callbacks[event] : null;
        if (callbacks) {
          if (fn) {
            this._callbacks[event] = callbacks.filter((cb) => cb !== fn); // remove specific handler
          } else {
            delete this._callbacks[event]; // remove all handlers
          }
        }
      }

      return this;
    }

    name: string


    extensions: unknown =null

    events = [
      "init",
      "transaction",
      "update",
      "focus",
      "blur",
      "paste",
      "drop",
    ]

    schema: unknown;

    view: EditorView

    public get view_1() {
      return this.view;
    }

    selection = { from: 0, to: 0 }

    hasTitle = false;

    constructor(el: HTMLElement, updateV: Function, updateFloat: Function, defaultContent: string, hasTitle: boolean) {
      console.log("exeditor", hasTitle);
      const options = {
        onUpdate: (r) => updateV(r),
      };
      this.events.forEach((name) => {
        this.on(name, options[camelCase(`on ${name}`)] || (() => {}));
      });

      this.options = hasTitle ? iOptionTitle : iOptionNoTitle;
      const onUpdateFloat = (menu, updateFloat) => {
        const menu1 = this.menu ?? {} as any;
        console.log("p", menu1);
        console.log("menu", menu);
        // the second check ensures event is fired only once
        if (menu.isActive && menu1.isActive === false) {
          updateFloat({ name: "show", menu });
          // this.$emit('show', menu)
        } else if (!menu.isActive && menu1.isActive === true) {
          updateFloat({ name: "hide", menu });
        }
        this.menu = menu;
      };

      const extensionManager = new ExtensionManager([
        new KunUrl(),
        new HardBreak(),
        new Link(),
        new Text(),
        new Placeholder({
          showOnlyCurrent: false,
          emptyNodeText: (node) => {
            if (node.type.name === "title") {
              return "Give me a name";
            }
            return "Write something";
          },
        }),
        new Image(),
        new Paragraph(),
        new Blockquote(),
        new Heading({ levels: [2, 3] }),
        new BulletList(),
        new OrderedList(),
        new ListItem(),
        new Bold(),
        new Italic(),
        new Strike(),
        new Underline(),
        // new KunIconP(),
        new CodeBlockHighlight({
          languages: {
            scala,
            javascript,
            python,
            css,
          },
        }),
      ].concat(...(hasTitle ? [new DocTitle(), new Title()] : [new Doc()])), undefined);

      this.hasTitle = hasTitle;
      this.extensions = extensionManager.extensions;
      const createSchema = () => {
        console.log("nodes");
        console.log(
          extensionManager.nodes,
        );
        console.log("marks");
        console.log(
          extensionManager.marks,
        );
        return new Schema({
          topNode: "doc",
          nodes: extensionManager.nodes,
          marks: extensionManager.marks,
        });
      };
      const schema = createSchema();
      console.log("extensionManager.nodes", schema);
      this.schema = schema;
      const keyMap = extensionManager.keymaps({
        schema,
      });

      const iPut = extensionManager.inputRules({
        schema,
        excludedExtensions: [],
      });

      const pasteRules = extensionManager.pasteRules({
        schema,
        excludedExtensions: [],
      });

      const content = "<h1>dd</h1><p>This is just a boring paragraph</p>";
      const state = EditorState.create({
        schema,
        doc: createDocument(schema, defaultContent, hasTitle),
        plugins: [
          ...extensionManager.plugins,
          keymap(baseKeymap),
          ...keyMap,
          // gapCursor(),
          new Plugin({
            key: new PluginKey("editable"),
            props: {
              editable: () => true,
            },
          }),
          new Plugin({
            props: {
              attributes: {
                tabindex: 0,
              },
              handleDOMEvents: {
                focus: (view, event) => {
                  this.focus.bind(this)(view, event);
                },
                blur: (view, event) => {
                  this.blur.bind(this)(view, event);
                },
              },
            },
          }),
          // FloatingMenu({
          //   editor: this,
          //   element: elLink,
          //   onUpdate: (item) => onUpdateFloat(item, updateFloat),
          // }),
          MenuBar({
            element: el,
          }),
          // commands1,
          // pasteRules,
          ...pasteRules,
          inputRules({
            rules: iPut,
          }),
        ],
      });
      const view = new EditorView(el, {
        state,
        dispatchTransaction: this.dispatchTransaction.bind(this),
        nodeViews: {
          image(node) { return new ImageView(node); },
          todo_item(node, view, getPos) { return new TodoView(node, view, getPos); },
        },
      });
      this.view = view;
      this.extensions.view = this.view;
    }

    linkUrl: null

    linkMenuIsActive: false

    showLinkMenu(attrs) {
      this.linkUrl = attrs.href;
      this.linkMenuIsActive = true;
      // this.$nextTick(() => {
      //     this.$refs.linkInput.focus()
      // })
    }

    hideLinkMenu() {
      this.linkUrl = null;
      this.linkMenuIsActive = false;
    }

    setLinkUrl(command, url) {
      command({ href: url });
      this.hideLinkMenu();
    }

    options = {
    }
    // options = {
    //   emptyDocument: {
    //     type: "doc",
    //     content: [
    //       {
    //         type: "paragraph",
    //         content: [
    //           {
    //             type: "title",
    //             text: "填写标题",
    //           },
    //           {
    //             type: "text",
    //             text: "请填写内容",
    //           },
    //         ],
    //       },
    //     ],
    //   },
    //   parseOptions: {},
    // }

    createDocument(content, parseOptions = this.options.parseOptions) {
      console.log("content");
      if (content == null) {
        return this.schema.nodeFromJSON(this.options.emptyDocument);
      }

      if (typeof content === "object") {
        try {
          return this.schema.nodeFromJSON(content);
        } catch (error) {
          console.warn("[tiptap warn]: Invalid content.", "Passed value:", content, "Error:", error);
          return this.schema.nodeFromJSON(this.options.emptyDocument);
        }
      }

      if (typeof content === "string") {
        const element = document.createElement("div");
        element.innerHTML = content.trim();

        return DOMParser.fromSchema(this.schema).parse(element, parseOptions);
      }

      return false;
    }


    dispatchTransaction(transaction) {
      const newState = this.view.state.apply(transaction);
      this.view.updateState(newState);

      this.selection = {
        from: this.view.state.selection.from,
        to: this.view.state.selection.to,
      };
      this.setActiveNodesAndMarks.bind(this)();

      this.emit("transaction", {
        getHTML: this.getHTML.bind(this),
        getJSON: this.getJSON.bind(this),
        state: this.view.state,
        transaction,
      });

      if (!transaction.docChanged || transaction.getMeta("preventUpdate")) {
        return;
      }

      this.emitUpdate(transaction);
    }

    emitUpdate(transaction) {
      const active = this.getActiveMap();
      console.log("isActiveA", active);
      this.emit("update", {
        getHTML: this.getHTML.bind(this),
        getJSON: this.getJSON.bind(this),
        state: this.view.state,
        active,
        transaction,
      });
    }

    resolveSelection(position = null) {
      if (this.selection && position === null) {
        return this.selection;
      }

      if (position === "start" || position === true) {
        return {
          from: 0,
          to: 0,
        };
      }

      if (position === "end") {
        const { doc } = this.view.state;
        return {
          from: doc.content.size,
          to: doc.content.size,
        };
      }

      return {
        from: position,
        to: position,
      };
    }

    focus(position = null) {
      if ((this.view.focused && position === null) || position === false) {
        return;
      }

      const { from, to } = this.resolveSelection(position);

      this.setSelection(from, to);
      setTimeout(() => this.view.focus(), 10);
    }

    setSelection(from = 0, to = 0) {
      const { doc, tr } = this.view.state;
      const resolvedFrom = minMax(from, 0, doc.content.size);
      const resolvedEnd = minMax(to, 0, doc.content.size);
      const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd);
      const transaction = tr.setSelection(selection);

      this.view.dispatch(transaction);
    }

    blur() {
      this.view.dom.blur();
    }

    getSchemaJSON() {
      return JSON.parse(JSON.stringify({
        nodes: this.extensions.nodes,
        marks: this.extensions.marks,
      }));
    }

    getHTML() {
      const div = document.createElement("div");
      const fragment = DOMSerializer
        .fromSchema(this.schema)
        .serializeFragment(this.view.state.doc.content);

      div.appendChild(fragment);

      return div.innerHTML;
    }

    getJSON(): { [key: string]: any } {
      return this.view.state.doc.toJSON();
    }

    setContent(content = {}, emitUpdate = false, parseOptions = {}): void {
      console.log("setContent");
      const { doc, tr } = this.view.state;
      const document = this.createDocument(content, parseOptions);
      const selection = TextSelection.create(doc, 0, doc.content.size);
      const transaction = tr
        .setSelection(selection)
        .replaceSelectionWith(document, false)
        .setMeta("preventUpdate", !emitUpdate);

      this.view.dispatch(transaction);
    }

    clearContent(emitUpdate = false): void {
      this.setContent(this.options.emptyDocument, emitUpdate);
    }

    activeMarks = {}

    activeMarkAttrs = {}

    activeNodes ={}

    getMarkAttrs(type = null) {
      return this.activeMarkAttrs[type];
    }


    setActiveNodesAndMarks() {
      this.activeMarks = Object
        .entries(this.schema.marks)
        .reduce((marks, [name, mark]) => ({
          ...marks,
          [name]: (attrs = {}) => markIsActive(this.view.state, mark, attrs),
        }), {});

      this.activeMarkAttrs = Object
        .entries(this.schema.marks)
        .reduce((marks, [name, mark]) => ({
          ...marks,
          [name]: this.getMarkAttrs(this.view.state, mark),
        }), {});

      this.activeNodes = Object
        .entries(this.schema.nodes)
        .reduce((nodes, [name, node]) => ({
          ...nodes,
          [name]: (attrs = {}) => nodeIsActive(this.view.state, node, attrs),
        }), {});
    }

    getNodeAttrs1(type = null) {
      return {
        ...getNodeAttrs(this.view.state, this.schema.nodes[type]),
      };
    }

    isActiveA() {
      return Object
        .entries({
          ...this.activeMarks,
          ...this.activeNodes,
        })
        .reduce((types, [name, value]) => ({
          ...types,
          [name]: (attrs = {}) => value(attrs),
        }), {});
    }

    get isActive() {
      return Object
        .entries({
          ...this.activeMarks,
          ...this.activeNodes,
        })
        .reduce((types, [name, value]) => ({
          ...types,
          [name]: (attrs = {}) => value(attrs),
        }), {});
    }

    getActiveMap() {
      const r = Object
        .entries({
          ...this.activeMarks,
          ...this.activeNodes,
        })
        .reduce((types, [name, value]) => ({
          ...types,
          [name]: value(),
        }), {});
      return r;
    }

    get isActive() {
      return Object
        .entries({
          ...this.activeMarks,
          ...this.activeNodes,
        })
        .reduce((types, [name, value]) => ({
          ...types,
          [name]: (attrs = {}) => value(attrs),
        }), {});
    }

    focused: boolean

    public focus1(view, event) {
      this.focused = true;
      this.emit("focus", {
        event,
        state: this.view.state,
        view,
      });

      const transaction = this.view.state.tr.setMeta("focused", true);
      this.view.dispatch(transaction);
    }

    public blur1(view, event) {
      this.focused = false;
      this.emit("blur", {
        event,
        state: view.state,
        view,
      });

      const transaction = this.view.state.tr.setMeta("focused", false);
      this.view.dispatch(transaction);
    }


    public getC = this.getCommands.bind(this)

    insertHref(url) {
      console.log(`url${url}`);
      this.getC?.()?.link({ href: url });
    }

    insertIcon(url: string, title?: string) {
      console.log("insertIcon xxxxxxxxx", url);
      console.log("insertIcon xxxxxxxxx", title);
      const { $from } = this.view.state.selection;
      const index = $from.index();
      console.log(`index ${index}`);
      console.log("this.schema.nodes.iconp", this.schema.nodes.url);
      if ($from.parent.canReplaceWith(index, index, this.schema.nodes.url.create({
        title,
        url,
      }))) {
        return false;
      }
      const image = this.schema.nodes.url.create({ iconType: "comment",
        title,
        url,
      });
      if (this.view.dispatch) this.view.dispatch(this.view.state.tr.replaceSelectionWith(image));
    }

    insertImage(url) {
      const { $from } = this.view.state.selection;
      const index = $from.index();
      console.log("this.schema.nodes.image", this.schema.nodes.image);
      if ($from.parent.canReplaceWith(index, index, this.schema.nodes.image.create({ src: url }))) {
        return false;
      }
      const image = this.schema.nodes.image.create({ src: url });
      if (this.view.dispatch) this.view.dispatch(this.view.state.tr.replaceSelectionWith(image));
    }

    public getCommands() {
      const { schema } = this;
      return this.extensions
        .filter((extension) => extension.commands)
        .reduce((allCommands, extension) => {
          const { name, type } = extension;
          const commands = {};
          const value = extension.commands({
            schema,
            ...["node", "mark"].includes(type) ? {
              type: schema[`${type}s`][name],
            } : {},
          });

          const apply = (cb, attrs) => cb(attrs)(this.view.state, this.view.dispatch, this.view);
          const handle = (_name, _value) => {
            if (Array.isArray(_value)) {
              commands[_name] = (attrs) => _value.forEach((callback) => apply(callback, attrs));
            } else if (typeof _value === "function") {
              commands[_name] = (attrs) => apply(_value, attrs);
            }
          };

          if (typeof value === "object") {
            Object.entries(value).forEach(([commandName, commandValue]) => {
              handle(commandName, commandValue);
            });
          } else {
            handle(name, value);
          }

          return {
            ...allCommands,
            ...commands,
          };
        }, {});
    }
}
