import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit, forwardRef, ChangeDetectorRef } from '@angular/core';
import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NG_VALUE_ACCESSOR } from '@angular/forms';
import { EditorComponent, NodeSpec, schema, Schema, FontSizeItem } from '@progress/kendo-angular-editor';

import { MsgBoxService } from '../../../services/msg-box.service';
import { DropDownOption } from '../../../entity/dropDownOption';
import { MD2Icon } from '../massive-darkness2.model';
import { first } from 'rxjs/operators';
import { MD2IconPickerDlgComponent } from './md2-icon-picker-dlg.component';
import { NbDialogService } from '@nebular/theme';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import { DialogService } from '@progress/kendo-angular-dialog';
@Component({
  selector: 'md2-html-editor',
  templateUrl: './md2-html-editor.component.html',
  styleUrl: './md2-html-editor.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MD2HtmlEditorComponent),
      multi: true
    }
  ]
})
export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewInit {
  @ViewChild(EditorComponent) editor: EditorComponent;
  @ViewChild('fontSizeDropdown') fontSizeDropdown: any;

  value: string = '';
  disabled: boolean = false;

  messageTemplateSchema = this.createCustomSchema();
  customCssClass = '.MD2Icon{font-family: "Massive Darkness 2", sans-serif !important; font-size: 40px; margin-left:5px}  body{font-size: 30px;  }';

  // Default font size: 30px
  fontSizeData: FontSizeItem[] = [
    { size: 30, text: '30px' },
    { size: 8, text: '8px' },
    { size: 10, text: '10px' },
    { size: 12, text: '12px' },
    { size: 14, text: '14px' },
    { size: 16, text: '16px' },
    { size: 18, text: '18px' },
    { size: 20, text: '20px' },
    { size: 24, text: '24px' },
    { size: 30, text: '30px' },
    { size: 36, text: '36px' },
    { size: 48, text: '48px' },
    { size: 60, text: '60px' },
    { size: 72, text: '72px' }
  ];

  defaultFontSize: FontSizeItem = { size: 30, text: '30px' };

  // ControlValueAccessor interface
  private onChangeFn = (value: string) => { };
  private onTouchedFn = () => { };

  constructor(
    private msgBoxService: MsgBoxService,
    private dialogService: DialogService,
    private cdr: ChangeDetectorRef,
    elementRef: ElementRef, ngZone: NgZone, @Inject(PLATFORM_ID) platformId: Object) {

  }

  ngAfterViewInit(): void {
    // Set default font size after view initialization
    // The fontSizeDropdown is the EditorFontSizeComponent instance
    setTimeout(() => {
      if (this.fontSizeDropdown) {
        // Access the fontSizeDropDownList component which has the defaultItem property
        if (this.fontSizeDropdown.fontSizeDropDownList) {
          this.fontSizeDropdown.fontSizeDropDownList.defaultItem = this.defaultFontSize;
        }
        // Also try setting it directly on the component if it has the property
        if (this.fontSizeDropdown.defaultItem !== undefined) {
          this.fontSizeDropdown.defaultItem = this.defaultFontSize;
        }
      }

      // Ensure the editor value is set if writeValue was called before view init
      if (this.editor && this.value && this.editor.value !== this.value) {
        this.editor.value = this.value;
      }
    }, 0);
  }

  // ControlValueAccessor implementation
  writeValue(value: string | null | undefined): void {
    const newValue = value || '';

    // Only update if the value actually changed to avoid unnecessary updates
    if (this.value !== newValue) {
      this.value = newValue;

      // Angular's [value] binding will handle updating the editor
      // But if editor is already initialized and value is out of sync, ensure sync
      if (this.editor && this.editor.value !== this.value) {
        // Use setTimeout to avoid ExpressionChangedAfterItHasBeenCheckedError
        setTimeout(() => {
          if (this.editor && this.editor.value !== this.value) {
            this.editor.value = this.value;
          }
        }, 0);
      }

      // Trigger change detection to ensure the binding updates
      if (!this.cdr['destroyed']) {
        this.cdr.markForCheck();
      }
    }
  }

  registerOnChange(fn: (value: string) => void): void {
    this.onChangeFn = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouchedFn = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onChange(value: string): void {
    // Only update if value actually changed to prevent infinite loops
    if (this.value !== value) {
      this.value = value || '';
      this.onChangeFn(this.value);
    }
  }

  onTouched(): void {
    this.onTouchedFn();
  }

  showInsertMD2Icon() {
    this.dialogService.open({
      title: 'Insert MD2 Icon',
      content: MD2IconPickerDlgComponent,
      width: '800px',
      height: 600
    }).result.subscribe((html: string) => {
      if (html && this.editor) {
        this.insertAfterSelection(html, true);
        return;
        // Insert the HTML content at the current cursor position
        // Use ProseMirror's dispatch method to insert content at cursor
        const view = this.editor.view;
        if (view && view.state) {
          try {
            // Parse the HTML content
            const dom = document.createElement('div');
            dom.innerHTML = html;

            // Use ProseMirror's DOMParser to parse HTML
            // Access it from the view's state
            const pmState = view.state;

            // Use the editor's exec method or manual dispatch
            if ((this.editor as any).exec) {
              // Try using exec with 'insertHTML' command if available
              try {
                (this.editor as any).exec('insertHTML', html);
              } catch (e) {
                throw new Error('insertHTML not supported');
              }
            } else {
              throw new Error('exec method not available');
            }
          } catch (e) {
            console.error('Error inserting HTML:', e);
            // Fallback: append to the end
            const currentValue = this.editor.value || '';
            const newValue = currentValue + ' ' + html;
            this.value = newValue;
            this.onChange(newValue);
          }
        } else {
          // Fallback: append to the end
          const currentValue = this.editor.value || '';
          const newValue = currentValue + ' ' + html;
          this.value = newValue;
          this.onChange(newValue);
        }
      }
    });
  }

  // Text manipulation methods for Kendo Editor
  /**
   * Parses HTML string to ProseMirror nodes
   * @param htmlContent - HTML string to parse
   * @returns ProseMirror Fragment
   */
  private parseHtmlContent(htmlContent: string): any {
    if (!this.editor || !this.editor.view) return null;

    const view = this.editor.view;
    const element = document.createElement('div');
    element.innerHTML = htmlContent;

    // Use ProseMirror's DOMParser to parse HTML into nodes
    const parser = ProseMirrorDOMParser.fromSchema(view.state.schema);
    const slice = parser.parseSlice(element);
    return slice.content;
  }

  /**
   * Inserts HTML content at the beginning of the editor
   * @param content - HTML string to insert
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public insertAtBeginning(content: string, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.insert(0, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }

  /**
   * Inserts HTML content at the end of the editor
   * @param content - HTML string to insert
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public insertAtEnd(content: string, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const endPos = view.state.doc.content.size;
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.insert(endPos, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }

  /**
   * Inserts HTML content before the current selection
   * @param content - HTML string to insert
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public insertBeforeSelection(content: string, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const { from } = view.state.selection;
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.insert(from, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }

  /**
   * Inserts HTML content after the current selection
   * @param content - HTML string to insert
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public insertAfterSelection(content: string, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const { to } = view.state.selection;
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.insert(to, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }

  /**
   * Replaces the currently selected text with new content
   * @param content - HTML string to replace selection with
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public replaceSelectedText(content: string, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const { from, to } = view.state.selection;
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (!contentNode) return;

    // If there's no selection, just insert at cursor position
    if (from === to) {
      const transaction = view.state.tr.insert(from, contentNode);
      view.dispatch(transaction);
    } else {
      // Replace the selected content
      const transaction = view.state.tr.replaceWith(from, to, contentNode);
      view.dispatch(transaction);
    }
    this.onChange(this.editor.value);
  }

  /**
   * Gets the currently selected text in the editor
   * @returns The selected text as a string
   */
  public getSelectedText(): string {
    if (!this.editor) return '';
    return this.editor.selectionText || '';
  }
  public getSelectionTextOrWholeText(): string {
    if (!this.editor) return '';

    // If there's selected text, return it
    if (this.editor.selectionText && this.editor.selectionText.trim().length > 0) {
      return this.editor.selectionText;
    }

    // Otherwise, get the whole text content from the editor's document
    if (this.editor.view && this.editor.view.state) {
      return this.editor.view.state.doc.textContent;
    }

    // Fallback: strip HTML tags and return plain text
    const div = document.createElement('div');
    div.innerHTML = this.editor.value;
    return div.textContent || div.innerText || '';
  }

  /**
   * Gets the current cursor position or selection range
   * @returns Object with 'from' and 'to' positions
   */
  public getSelectionRange(): { from: number; to: number } | null {
    if (!this.editor || !this.editor.view) return null;

    const { from, to } = this.editor.view.state.selection;
    return { from, to };
  }

  /**
   * Inserts HTML content at a specific position
   * @param content - HTML string to insert
   * @param position - Position to insert at (0 = beginning)
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public insertAtPosition(content: string, position: number, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const maxPos = view.state.doc.content.size;
    const safePos = Math.min(Math.max(0, position), maxPos);
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.insert(safePos, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }

  /**
   * Replaces text in a specific range
   * @param content - HTML string to insert
   * @param from - Start position
   * @param to - End position
   * @param asHtml - If true, parses as HTML; if false, inserts as plain text
   */
  public replaceRange(content: string, from: number, to: number, asHtml: boolean = false): void {
    if (!this.editor || !this.editor.view) return;

    const view = this.editor.view;
    const maxPos = view.state.doc.content.size;
    const safeFrom = Math.min(Math.max(0, from), maxPos);
    const safeTo = Math.min(Math.max(safeFrom, to), maxPos);
    const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);

    if (contentNode) {
      const transaction = view.state.tr.replaceWith(safeFrom, safeTo, contentNode);
      view.dispatch(transaction);
      this.onChange(this.editor.value);
    }
  }



  // showVariablePicker() {
  //   this.easyEditorService.openTableMultiPicker(this.variables, this.variableTableSettings, "Please select a variable").pipe(first()).subscribe(result => {
  //     if (result) {
  //       result.forEach(c => {
  //         this.insertAfterSelection(`<${RbjTagNode} class="rbj-tag" ${RbjTagIdAttribute}="${c.name}" ${RbjTagValueAttribute}="${c.name}">${c.name}</${RbjTagNode}>`, true);
  //       });
  //     }
  //   });
  // }


  // Private methods
  private createCustomSchema(): Schema {
    let nodes = schema.spec.nodes.addBefore("div", "rbjTag", rbjTagNodeSpec);
    let marks = schema.spec.marks;
    //marks = marks.addToStart("rbjSpanTag", rbjTagMarkSpec);
    return new Schema({
      nodes: nodes,
      marks: marks
    });
  }
}

// Define the custom node specification for the rbj-tag element.
export const rbjTagNodeSpec: NodeSpec = {
  // Define the node attributes for the tag
  attrs: {
    "md2-icon": { default: "" },
    "class": { default: "" },
    // "tag-value": { default: "" },
    // "tag-preview": { default: "" }
  },
  // Specify that this node should be treated as an inline element
  inline: true,
  // Allow the node to be part of inline content
  group: "inline",
  // Make it atomic (non-editable, treated as a single unit)
  atom: true,

  // Define how the node should be rendered in the DOM
  toDOM: (node) => {
    let md2IconText = node.attrs["md2-icon"] as string;
    let classValue = node.attrs["class"] as string;
    if (classValue.includes('dice')) {
      md2IconText = '';
    }
    // let displayValue = tagPreview == 'true' ? node.attrs["tag-value"] : node.attrs["rbj-tag-id"];

    return [
      "span",
      {
        class: classValue,
        // "rbj-tag-id": node.attrs["rbj-tag-id"],
        // "tag-marker": node.attrs["tag-marker"],
        // "tag-value": node.attrs["tag-value"],
        // "tag-preview": node.attrs["tag-preview"],
        contenteditable: "false",
        //spellcheck: "false",  style="font-size: 36px;"
        style: "display: inline;"
      },
      //node.attrs["tag-marker"] + node.attrs["tag-value"]  // Display the tag content directly
      md2IconText
    ];
  },

  // Define how to parse the node from existing DOM elements
  parseDOM: [
    {
      // Look for span elements with class rbj-tag (higher priority)
      tag: "span[md2-icon]",
      priority: 51,  // Higher priority to catch before other parsers
      // Extract attributes from the DOM element
      getAttrs: (dom) => {
        const element = dom as HTMLElement;
        // Must have rbj-tag-id attribute to be valid
        if (!element.hasAttribute("md2-icon")) {
          return false;
        }
        return {
          "md2-icon": element.getAttribute("md2-icon") || "",
          "class": element.className || "",
          // "tag-preview": element.getAttribute("tag-preview") || "false",
          // "tag-value": element.getAttribute("tag-value") || element.getAttribute("rbj-tag-id")
        };
      }
    }
  ]
};