import {
  Component,
  ElementRef,
  NgZone,
  OnDestroy,
  computed,
  effect,
  inject,
  input,
  output,
  viewChild
} from '@angular/core';
import { indentWithTab } from '@codemirror/commands';
import { css } from '@codemirror/lang-css';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { indentUnit, syntaxTree } from '@codemirror/language';
import {
  Diagnostic,
  lintGutter,
  linter
} from '@codemirror/lint';
import {
  EditorSelection,
  EditorState,
  Extension
} from '@codemirror/state';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, keymap } from '@codemirror/view';
import { basicSetup } from 'codemirror';
import { AppI18nService } from '../../../../../core/i18n';

import { formatPastedJsonText } from './theme-json-format.logic';

type ThemeCodeEditorLanguage = 'json' | 'css';

const ERROR_SQUIGGLE_BACKGROUND =
  'linear-gradient(45deg, transparent 65%, #fb7185 65%, #fb7185 80%, transparent 80%), '
  + 'linear-gradient(135deg, transparent 65%, #fb7185 65%, #fb7185 80%, transparent 80%)';
const THEME_JSON_EDITOR_THEME = EditorView.theme(
  {
    '&': {
      height: '100%',
      backgroundColor: 'transparent',
      color: '#e7eef9'
    },
    '&.cm-focused': {
      outline: 'none'
    },
    '.cm-scroller': {
      overflow: 'auto',
      fontFamily: "'IBM Plex Mono', 'JetBrains Mono', 'Fira Code', monospace",
      lineHeight: '1.55'
    },
    '.cm-content': {
      minHeight: '100%',
      padding: '1rem 0',
      caretColor: '#f8fafc'
    },
    '.cm-line': {
      padding: '0 1rem 0 0.5rem'
    },
    '.cm-cursor, .cm-dropCursor': {
      borderLeftColor: '#f8fafc'
    },
    '.cm-gutters': {
      minHeight: '100%',
      borderRight: '1px solid #2f405c',
      backgroundColor: '#172033',
      color: '#7b8aa5'
    },
    '.cm-activeLine': {
      backgroundColor: 'rgb(148 163 184 / 0.08)'
    },
    '.cm-activeLineGutter': {
      backgroundColor: 'rgb(148 163 184 / 0.12)',
      color: '#d7e2f2'
    },
    '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': {
      backgroundColor: 'rgb(96 165 250 / 0.22)'
    },
    '.cm-panels': {
      backgroundColor: '#111827',
      color: '#e5eefc',
      borderBottom: '1px solid #2f405c'
    },
    '.cm-searchMatch': {
      backgroundColor: 'rgb(250 204 21 / 0.18)',
      outline: '1px solid rgb(250 204 21 / 0.32)'
    },
    '.cm-searchMatch.cm-searchMatch-selected': {
      backgroundColor: 'rgb(250 204 21 / 0.28)'
    },
    '.cm-tooltip': {
      border: '1px solid #314158',
      backgroundColor: '#111827'
    },
    '.cm-tooltip-autocomplete ul li[aria-selected]': {
      backgroundColor: 'rgb(96 165 250 / 0.18)',
      color: '#f8fafc'
    },
    '.cm-diagnosticRange-error': {
      backgroundImage: ERROR_SQUIGGLE_BACKGROUND,
      backgroundPosition: '0 100%',
      backgroundRepeat: 'repeat-x',
      backgroundSize: '8px 3px',
      paddingBottom: '2px'
    },
    '.cm-lintRange-error': {
      backgroundImage: ERROR_SQUIGGLE_BACKGROUND,
      backgroundPosition: '0 100%',
      backgroundRepeat: 'repeat-x',
      backgroundSize: '8px 3px',
      paddingBottom: '2px'
    },
    '.cm-lintPoint-error:after': {
      borderBottomColor: '#fb7185'
    },
    '.cm-diagnostic': {
      fontFamily: 'Inter, system-ui, sans-serif'
    },
    '.cm-diagnostic-error': {
      borderLeftColor: '#fb7185'
    }
  },
  { dark: true }
);

function getLanguageExtensions(language: ThemeCodeEditorLanguage): Extension[] {
  if (language === 'css') {
    return [css(), linter(cssSyntaxLinter, { delay: 250 })];
  }

  return [json(), linter(jsonParseLinter(), { delay: 250 })];
}

function cssSyntaxLinter(view: EditorView): Diagnostic[] {
  const diagnostics: Diagnostic[] = [];
  const documentLength = view.state.doc.length;

  syntaxTree(view.state).iterate({
    enter: (node) => {
      if (!node.type.isError) {
        return;
      }

      diagnostics.push({
        from: node.from,
        to: Math.max(node.to, Math.min(node.from + 1, documentLength)),
        severity: 'error',
        source: 'CSS',
        message: 'CSS syntax error.'
      });
    }
  });

  return diagnostics;
}

@Component({
  selector: 'app-theme-json-code-editor',
  standalone: true,
  templateUrl: './theme-json-code-editor.component.html',
  styleUrl: './theme-json-code-editor.component.scss'
})
export class ThemeJsonCodeEditorComponent implements OnDestroy {
  readonly editorHostRef = viewChild<ElementRef<HTMLDivElement>>('editorHostRef');
  readonly value = input.required<string>();
  readonly fullscreen = input(false);
  readonly language = input<ThemeCodeEditorLanguage>('json');
  readonly valueChange = output<string>();

  readonly editorMinHeight = computed(() => (this.fullscreen() ? 'max(34rem, calc(100vh - 15rem))' : '28rem'));

  private readonly zone = inject(NgZone);
  private readonly appI18n = inject(AppI18nService);

  private editorView: EditorView | null = null;
  private isApplyingExternalValue = false;

  constructor() {
    effect(() => {
      const host = this.editorHostRef();

      if (!host || this.editorView) {
        return;
      }

      this.createEditor(host.nativeElement);
    });

    effect(() => {
      const nextValue = this.value();

      if (!this.editorView) {
        return;
      }

      const currentValue = this.editorView.state.doc.toString();

      if (currentValue === nextValue) {
        return;
      }

      this.isApplyingExternalValue = true;
      this.editorView.dispatch({
        changes: {
          from: 0,
          to: currentValue.length,
          insert: nextValue
        }
      });

      this.isApplyingExternalValue = false;
    });

    effect(() => {
      this.fullscreen();

      queueMicrotask(() => {
        this.editorView?.requestMeasure();
      });
    });
  }

  ngOnDestroy(): void {
    this.editorView?.destroy();
    this.editorView = null;
  }

  focus(): void {
    this.editorView?.focus();
  }

  focusRange(from: number, to = from): void {
    if (!this.editorView) {
      return;
    }

    const documentLength = this.editorView.state.doc.length;
    const selectionStart = Math.max(0, Math.min(from, documentLength));
    const selectionEnd = Math.max(selectionStart, Math.min(to, documentLength));

    this.editorView.dispatch({
      selection: EditorSelection.range(selectionStart, selectionEnd),
      effects: EditorView.scrollIntoView(selectionStart, { y: 'center' })
    });

    this.editorView.focus();
  }

  private createEditor(host: HTMLDivElement): void {
    this.zone.runOutsideAngular(() => {
      const language = this.language();

      this.editorView = new EditorView({
        state: EditorState.create({
          doc: this.value(),
          extensions: [
            basicSetup,
            keymap.of([indentWithTab]),
            indentUnit.of('  '),
            ...getLanguageExtensions(language),
            lintGutter(),
            oneDark,
            THEME_JSON_EDITOR_THEME,
            EditorState.tabSize.of(2),
            EditorView.contentAttributes.of({
              spellcheck: 'false',
              autocapitalize: 'off',
              autocorrect: 'off',
              'aria-label': language === 'css'
                ? this.appI18n.instant('theme.editor.cssAria')
                : this.appI18n.instant('theme.editor.jsonAria')
            }),
            EditorView.domEventHandlers({
              paste: (event, view) => {
                if (language !== 'json') {
                  return false;
                }

                const pastedText = event.clipboardData?.getData('application/json') || event.clipboardData?.getData('text/plain') || '';
                const formattedText = formatPastedJsonText(pastedText);

                if (!formattedText) {
                  return false;
                }

                event.preventDefault();
                view.dispatch(view.state.replaceSelection(formattedText));
                return true;
              }
            }),
            EditorView.updateListener.of((update) => {
              if (!update.docChanged || this.isApplyingExternalValue) {
                return;
              }

              const nextValue = update.state.doc.toString();

              this.zone.run(() => {
                this.valueChange.emit(nextValue);
              });
            })
          ]
        }),
        parent: host
      });
    });
  }
}
