import { assertExists, assertInstanceOf } from '@blockeditor/global/utils';
import { Slice, Text, type Y } from '@blockeditor/store';

import { toggleEmbedCardCreateModal } from '../../../_common/components/embed-card/modal/embed-card-create-modal.js';
import { toast } from '../../../_common/components/toast.js';
import { textConversionConfigs } from '../../../_common/configs/text-conversion.js';
import { textFormatConfigs } from '../../../_common/configs/text-format/config.js';
import {
  ArrowDownBigIcon,
  ArrowUpBigIcon,
  AttachmentIcon,
  BookmarkIcon,
  CopyIcon,
  DatabaseKanbanViewIcon20,
  DatabaseTableViewIcon20,
  DeleteIcon,
  DualLinkIcon,
  DuplicateIcon,
  FrameIcon,
  GroupingIcon,
  ImageIcon20,
  NewPageIcon,
  NowIcon,
  TodayIcon,
  TomorrowIcon,
  YesterdayIcon,
} from '../../../_common/icons/index.js';
import { REFERENCE_NODE } from '../../../_common/inline/presets/nodes/consts.js';
import {
  createDefaultPage,
  getImageFilesFromLocal,
  getInlineEditorByModel,
  matchFlavours,
  openFileOrFiles,
} from '../../../_common/utils/index.js';
import { clearMarksOnDiscontinuousInput } from '../../../_common/utils/inline-editor.js';
import { AttachmentService } from '../../../attachment-block/attachment-service.js';
import { addSiblingAttachmentBlock } from '../../../attachment-block/utils.js';
import type { DatabaseService } from '../../../database-block/database-service.js';
import { FigmaIcon } from '../../../embed-figma-block/styles.js';
import { GithubIcon } from '../../../embed-github-block/styles.js';
import { YoutubeIcon } from '../../../embed-youtube-block/styles.js';
import type { FrameBlockModel } from '../../../frame-block/index.js';
import { ImageService } from '../../../image-block/image-service.js';
import { addSiblingImageBlock } from '../../../image-block/utils.js';
import type { SurfaceBlockModel } from '../../../models.js';
import type { NoteBlockModel } from '../../../note-block/index.js';
import { onModelTextUpdated } from '../../../page-block/utils/index.js';
import { updateBlockElementType } from '../../../page-block/utils/operations/element/block-level.js';
import type { ParagraphBlockModel } from '../../../paragraph-block/index.js';
import { CanvasElementType } from '../../../surface-block/index.js';
import type { AffineLinkedDocWidget } from '../linked-doc/index.js';
import {
  formatDate,
  insertContent,
  insideDatabase,
  type SlashItem,
  type SlashMenuOptions,
  withRemoveEmptyLine,
} from './utils.js';

export const menuGroups: SlashMenuOptions['menus'] = [
  {
    name: 'Text',
    items: [
      ...textConversionConfigs
        .filter(i => i.flavour !== 'affine:list')
        .map<Omit<SlashItem, 'groupName'>>(({ name, icon, flavour, type }) => ({
          name,
          icon,
          showWhen: model => {
            if (!model.page.schema.flavourSchemaMap.has(flavour)) {
              return false;
            }

            if (['Quote', 'Code Block', 'Divider'].includes(name)) {
              return !insideDatabase(model);
            }
            return true;
          },
          action: ({ pageElement }) => {
            pageElement.host.std.command
              .pipe()
              .withHost()
              .tryAll(chain => [
                chain.getTextSelection(),
                chain.getBlockSelections(),
              ])
              .getSelectedBlocks({
                types: ['text', 'block'],
              })
              .inline(ctx => {
                const { selectedBlocks } = ctx;
                assertExists(selectedBlocks);

                const newModels = updateBlockElementType(
                  selectedBlocks,
                  flavour,
                  type
                );

                // Reset selection if the target is code block
                if (flavour === 'affine:code') {
                  if (newModels.length !== 1) {
                    throw new Error(
                      "Failed to reset selection! New model length isn't 1"
                    );
                  }
                  const codeModel = newModels[0];
                  onModelTextUpdated(pageElement.host, codeModel, richText => {
                    const inlineEditor = richText.inlineEditor;
                    assertExists(inlineEditor);
                    inlineEditor.focusEnd();
                  }).catch(console.error);
                }
              })
              .run();
          },
        })),
    ],
  },
  {
    name: 'Style',
    items: textFormatConfigs
      .filter(i => !['Link', 'Code'].includes(i.name))
      .map(({ name, icon, id }) => ({
        name,
        icon,
        action: ({ pageElement, model }) => {
          if (!model.text) {
            return;
          }
          const len = model.text.length;
          if (!len) {
            const inlineEditor = getInlineEditorByModel(
              pageElement.host,
              model
            );
            assertExists(
              inlineEditor,
              "Can't set style mark! Inline editor not found"
            );
            inlineEditor.setMarks({
              [id]: true,
            });
            clearMarksOnDiscontinuousInput(inlineEditor);
            return;
          }
          model.text.format(0, len, {
            [id]: true,
          });
        },
      })),
  },
  {
    name: 'List',
    items: textConversionConfigs
      .filter(i => i.flavour === 'affine:list')
      .map(({ name, icon, flavour, type }) => ({
        name,
        icon,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has(flavour)) {
            return false;
          }
          return true;
        },
        action: ({ pageElement }) => {
          pageElement.host.std.command
            .pipe()
            .withHost()
            .tryAll(chain => [
              chain.getTextSelection(),
              chain.getBlockSelections(),
            ])
            .getSelectedBlocks({
              types: ['text', 'block'],
            })
            .inline(ctx => {
              const { selectedBlocks } = ctx;
              assertExists(selectedBlocks);

              updateBlockElementType(selectedBlocks, flavour, type);
            })
            .run();
        },
      })),
  },

  {
    name: 'Pages',
    items: [
      {
        name: 'New Page',
        icon: NewPageIcon,
        action: async ({ pageElement, model }) => {
          const newPage = await createDefaultPage(pageElement.page.workspace);
          insertContent(pageElement.host, model, REFERENCE_NODE, {
            reference: {
              type: 'LinkedPage',
              pageId: newPage.id,
            },
          });
        },
      },
      {
        name: 'Link Page',
        alias: ['dual link'],
        icon: DualLinkIcon,
        showWhen: (_, pageElement) => {
          const linkedDocWidgetEle =
            pageElement.widgetElements['affine-linked-doc-widget'];
          if (!linkedDocWidgetEle) return false;
          if (!('showLinkedDoc' in linkedDocWidgetEle)) {
            console.warn(
              'You may not have correctly implemented the linkedDoc widget! "showLinkedDoc(model)" method not found on widget'
            );
            return false;
          }
          return true;
        },
        action: ({ model, pageElement }) => {
          const triggerKey = '@';
          insertContent(pageElement.host, model, triggerKey);
          assertExists(model.page.root);
          const widgetEle =
            pageElement.widgetElements['affine-linked-doc-widget'];
          assertExists(widgetEle);
          // We have checked the existence of showLinkedDoc method in the showWhen
          const linkedDocWidget = widgetEle as AffineLinkedDocWidget;
          // Wait for range to be updated
          setTimeout(() => {
            linkedDocWidget.showLinkedDoc(model, triggerKey);
          });
        },
      },
    ],
  },
  {
    name: 'Content & Media',
    items: [
      {
        name: 'Image',
        icon: ImageIcon20,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:image')) {
            return false;
          }
          if (insideDatabase(model)) {
            return false;
          }
          return true;
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parent = pageElement.page.getParent(model);
          if (!parent) {
            return;
          }

          const imageFiles = await getImageFilesFromLocal();

          const imageService = pageElement.host.spec.getService('affine:image');
          assertExists(imageService);
          assertInstanceOf(imageService, ImageService);
          const maxFileSize = imageService.maxFileSize;

          addSiblingImageBlock(
            pageElement.host,
            imageFiles,
            maxFileSize,
            model
          );
        }),
      },
      {
        name: 'Links',
        icon: BookmarkIcon,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:bookmark')) {
            return false;
          }
          return !insideDatabase(model);
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parentModel = pageElement.page.getParent(model);
          if (!parentModel) {
            return;
          }
          const index = parentModel.children.indexOf(model) + 1;
          await toggleEmbedCardCreateModal(
            pageElement.host,
            'Links',
            'The added link will be displayed as a card view.',
            { mode: 'page', parentModel, index }
          );
        }),
      },
      {
        name: 'File',
        icon: AttachmentIcon,
        alias: ['attachment'],
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:attachment'))
            return false;
          return !insideDatabase(model);
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const page = pageElement.page;
          const parent = page.getParent(model);
          if (!parent) return;

          const file = await openFileOrFiles();
          if (!file) return;

          const attachmentService =
            pageElement.host.spec.getService('affine:attachment');
          assertExists(attachmentService);
          assertInstanceOf(attachmentService, AttachmentService);
          const maxFileSize = attachmentService.maxFileSize;

          addSiblingAttachmentBlock(
            pageElement.host,
            file,
            maxFileSize,
            model
          ).catch(console.error);
        }),
      },
      {
        name: 'YouTube',
        icon: YoutubeIcon,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:embed-youtube')) {
            return false;
          }
          return !insideDatabase(model);
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parentModel = pageElement.page.getParent(model);
          if (!parentModel) {
            return;
          }
          const index = parentModel.children.indexOf(model) + 1;
          await toggleEmbedCardCreateModal(
            pageElement.host,
            'YouTube',
            'The added YouTube video link will be displayed as an embed view.',
            { mode: 'page', parentModel, index }
          );
        }),
      },
      {
        name: 'Figma',
        icon: FigmaIcon,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:embed-figma')) {
            return false;
          }
          return !insideDatabase(model);
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parentModel = pageElement.page.getParent(model);
          if (!parentModel) {
            return;
          }
          const index = parentModel.children.indexOf(model) + 1;
          await toggleEmbedCardCreateModal(
            pageElement.host,
            'Figma',
            'The added Figma link will be displayed as an embed view.',
            { mode: 'page', parentModel, index }
          );
        }),
      },
      {
        name: 'GitHub',
        icon: GithubIcon,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:embed-github')) {
            return false;
          }
          return !insideDatabase(model);
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parentModel = pageElement.page.getParent(model);
          if (!parentModel) {
            return;
          }
          const index = parentModel.children.indexOf(model) + 1;
          await toggleEmbedCardCreateModal(
            pageElement.host,
            'GitHub',
            'The added GitHub issue or pull request link will be displayed as a card view.',
            { mode: 'page', parentModel, index }
          );
        }),
      },
    ],
  },
  {
    name: 'Date & Time',
    items: [
      {
        name: 'Today',
        icon: TodayIcon,
        action: ({ pageElement, model }) => {
          const date = new Date();
          insertContent(pageElement.host, model, formatDate(date));
        },
      },
      {
        name: 'Tomorrow',
        icon: TomorrowIcon,
        action: ({ pageElement, model }) => {
          // yyyy-mm-dd
          const date = new Date();
          date.setDate(date.getDate() + 1);
          insertContent(pageElement.host, model, formatDate(date));
        },
      },
      {
        name: 'Yesterday',
        icon: YesterdayIcon,
        action: ({ pageElement, model }) => {
          const date = new Date();
          date.setDate(date.getDate() - 1);
          insertContent(pageElement.host, model, formatDate(date));
        },
      },
      {
        name: 'Now',
        icon: NowIcon,
        action: ({ pageElement, model }) => {
          // For example 7:13 pm
          // https://stackoverflow.com/questions/8888491/how-do-you-display-javascript-datetime-in-12-hour-am-pm-format
          const date = new Date();
          let hours = date.getHours();
          const minutes = date.getMinutes().toString().padStart(2, '0');
          const amOrPm = hours >= 12 ? 'pm' : 'am';
          hours = hours % 12;
          hours = hours ? hours : 12; // the hour '0' should be '12'
          const strTime = hours + ':' + minutes + ' ' + amOrPm;
          insertContent(pageElement.host, model, strTime);
        },
      },
    ],
  },
  {
    name: 'Database',
    items: [
      {
        name: 'Table View',
        alias: ['database'],
        icon: DatabaseTableViewIcon20,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:database')) {
            return false;
          }
          if (insideDatabase(model)) {
            // You can't add a database block inside another database block
            return false;
          }
          return true;
        },
        action: withRemoveEmptyLine(async ({ pageElement, model }) => {
          const parent = pageElement.page.getParent(model);
          assertExists(parent);
          const index = parent.children.indexOf(model);

          const id = pageElement.page.addBlock(
            'affine:database',
            {},
            pageElement.page.getParent(model),
            index + 1
          );
          const service = pageElement.std.spec.getService(
            'affine:database'
          ) as DatabaseService;
          service.initDatabaseBlock(
            pageElement.page,
            model,
            id,
            'table',
            false
          );
        }),
      },
      {
        name: 'Kanban View',
        alias: ['database'],
        disabled: false,
        icon: DatabaseKanbanViewIcon20,
        showWhen: model => {
          if (!model.page.schema.flavourSchemaMap.has('affine:database')) {
            return false;
          }
          if (insideDatabase(model)) {
            // You can't add a database block inside another database block
            return false;
          }
          return true;
        },
        action: withRemoveEmptyLine(async ({ model, pageElement }) => {
          const parent = pageElement.page.getParent(model);
          assertExists(parent);
          const index = parent.children.indexOf(model);

          const id = pageElement.page.addBlock(
            'affine:database',
            {},
            pageElement.page.getParent(model),
            index + 1
          );
          const service = pageElement.std.spec.getService(
            'affine:database'
          ) as DatabaseService;
          service.initDatabaseBlock(
            pageElement.page,
            model,
            id,
            'kanban',
            false
          );
        }),
      },
    ],
  },
  {
    name: 'Frames',
    items: options => {
      const frameModels = options.pageElement.page.getBlockByFlavour(
        'affine:frame'
      ) as FrameBlockModel[];

      return frameModels.map(frameModel => {
        return {
          name: 'Frame: ' + frameModel.title,
          icon: FrameIcon,
          action: async ({ pageElement, model }) => {
            const { page } = pageElement;
            const noteModel = page.getParent(model) as NoteBlockModel;
            const insertIdx = noteModel.children.indexOf(model);
            const surfaceRefProps = {
              flavour: 'affine:surface-ref',
              reference: frameModel.id,
              refFlavour: 'affine:frame',
            };

            page.addSiblingBlocks(
              model,
              [surfaceRefProps],
              insertIdx === 0 ? 'before' : 'after'
            );

            if (
              matchFlavours(model, ['affine:paragraph']) &&
              model.text.length === 0
            ) {
              page.deleteBlock(model);
            }
          },
        };
      });
    },
  },
  {
    name: 'Group',
    items: options => {
      const surfaceModel = (
        options.pageElement.page.getBlockByFlavour(
          'affine:surface'
        ) as SurfaceBlockModel[]
      )[0];

      if (!surfaceModel) return [];
      const groupElements = (<Array<Y.Map<string>>>(
        Array.from(surfaceModel.elements.getValue()?.values() ?? [])
      )).filter(element => element.get('type') === CanvasElementType.GROUP);

      return (
        groupElements.map(element => {
          return {
            name: 'Group: ' + element.get('title'),
            icon: GroupingIcon,
            action: async ({ pageElement, model }) => {
              const { page } = pageElement;
              const noteModel = page.getParent(model) as NoteBlockModel;
              const insertIdx = noteModel.children.indexOf(model);
              const surfaceRefProps = {
                flavour: 'affine:surface-ref',
                reference: element.get('id'),
                refFlavour: 'group',
              };

              page.addSiblingBlocks(
                model,
                [surfaceRefProps],
                insertIdx === 0 ? 'before' : 'after'
              );

              if (
                matchFlavours(model, ['affine:paragraph']) &&
                model.text.length === 0
              ) {
                page.deleteBlock(model);
              }
            },
          };
        }) ?? []
      );
    },
  },
  {
    name: 'Actions',
    items: [
      {
        name: 'Move Up',
        icon: ArrowUpBigIcon,
        action: async ({ pageElement, model }) => {
          const page = pageElement.page;
          const previousSiblingModel = page.getPreviousSibling(model);
          if (!previousSiblingModel) return;

          const parentModel = page.getParent(previousSiblingModel);
          if (!parentModel) return;

          page.moveBlocks([model], parentModel, previousSiblingModel, true);
        },
      },
      {
        name: 'Move Down',
        icon: ArrowDownBigIcon,
        action: async ({ pageElement, model }) => {
          const page = pageElement.page;
          const nextSiblingModel = page.getNextSibling(model);
          if (!nextSiblingModel) return;

          const parentModel = page.getParent(nextSiblingModel);
          if (!parentModel) return;

          page.moveBlocks([model], parentModel, nextSiblingModel, false);
        },
      },
      {
        name: 'Copy',
        icon: CopyIcon,
        action: async ({ pageElement, model }) => {
          const slice = Slice.fromModels(pageElement.std.page, [model]);

          pageElement.std.clipboard
            .copy(slice)
            .then(() => {
              toast(pageElement.host, 'Copied to clipboard');
            })
            .catch(e => {
              console.error(e);
            });
        },
      },
      // {
      //   name: 'Paste',
      //   icon: PasteIcon,
      //   action: async ({ model }) => {
      //     const copiedText = await navigator.clipboard.readText();
      //     console.log('copiedText', copiedText);
      //     insertContent(model, copiedText);
      //   },
      // },
      {
        name: 'Duplicate',
        icon: DuplicateIcon,
        action: ({ pageElement, model }) => {
          if (!model.text || !(model.text instanceof Text)) {
            throw new Error("Can't duplicate a block without text");
          }
          const parent = pageElement.page.getParent(model);
          if (!parent) {
            throw new Error('Failed to duplicate block! Parent not found');
          }
          const index = parent.children.indexOf(model);

          // TODO add clone model util
          pageElement.page.addBlock(
            model.flavour,
            {
              type: (model as ParagraphBlockModel).type,
              text: pageElement.page.Text.fromDelta(model.text.toDelta()),
              // @ts-expect-error
              checked: model.checked,
            },
            pageElement.page.getParent(model),
            index
          );
        },
      },
      {
        name: 'Delete',
        alias: ['remove'],
        icon: DeleteIcon,
        action: ({ pageElement, model }) => {
          pageElement.page.deleteBlock(model);
        },
      },
    ],
  },
];
