import * as p from "@bokehjs/core/properties"
import { div } from "@bokehjs/core/dom"

import { HTMLBox, HTMLBoxView } from "./layout"

export class QuillInputView extends HTMLBoxView {
  override model: QuillInput
  protected container: HTMLDivElement
  protected _editor: HTMLDivElement
  protected _editing: boolean
  protected _toolbar: HTMLDivElement | null

  quill: any

  connect_signals(): void {
    super.connect_signals()
    this.connect(this.model.properties.disabled.change, () => this.quill.enable(!this.model.disabled))
    this.connect(this.model.properties.visible.change, () => {
      if (this.model.visible)
        this.container.style.visibility = 'visible';
    })
    this.connect(this.model.properties.text.change, () => {
      if (this._editing)
        return
      this._editing = true
      this.quill.enable(false)
      this.quill.setContents([])
      this.quill.clipboard.dangerouslyPasteHTML(this.model.text)
      this.quill.enable(!this.model.disabled)
      this._editing = false
    })
    const { mode, toolbar, placeholder } = this.model.properties
    this.on_change([placeholder], () => {
      this.quill.root.setAttribute('data-placeholder', this.model.placeholder)
    })
    this.on_change([mode, toolbar], () => {
      this.render()
      this._layout_toolbar()
    })
  }

  _layout_toolbar(): void {
    if (this._toolbar == null) {
      this.el.style.removeProperty('padding-top')
    } else {
      const height = this._toolbar.getBoundingClientRect().height + 1
      this.el.style.paddingTop = height + "px"
      this._toolbar.style.marginTop = -height + "px"
    }
  }

  render(): void {
    super.render()
    this.container = div({ style: "visibility: hidden;" })
    this.shadow_el.appendChild(this.container)
    const theme = (this.model.mode === 'bubble') ? 'bubble' : 'snow'
    this.watch_stylesheets()
    this.quill = new (window as any).Quill(this.container, {
      modules: {
        toolbar: this.model.toolbar
      },
      readOnly: true,
      placeholder: this.model.placeholder,
      theme: theme
    });

    // Apply ShadowDOM patch found at:
    // https://github.com/quilljs/quill/issues/2961#issuecomment-1775999845

    const hasShadowRootSelection = !!((document.createElement('div').attachShadow({ mode: 'open' }) as any).getSelection);
    // Each browser engine has a different implementation for retrieving the Range
    const getNativeRange = (rootNode: any) => {
      try {
        if (hasShadowRootSelection) {
          // In Chromium, the shadow root has a getSelection function which returns the range
          return rootNode.getSelection().getRangeAt(0);
        } else {
          const selection = window.getSelection();
          if ((selection as any).getComposedRanges) {
            // Webkit range retrieval is done with getComposedRanges (see: https://bugs.webkit.org/show_bug.cgi?id=163921)
            return (selection as any).getComposedRanges(rootNode)[0];
          } else {
            // Gecko implements the range API properly in Native Shadow: https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt
            return (selection as any).getRangeAt(0);
          }
        }
      } catch {
        return null;
      }
    }

    /**
     * Original implementation uses document.active element which does not work in Native Shadow.
     * Replace document.activeElement with shadowRoot.activeElement
     **/
    this.quill.selection.hasFocus = () => {
      const rootNode = (this.quill.root.getRootNode() as ShadowRoot);
      return rootNode.activeElement === this.quill.root;
    }

    /**
     * Original implementation uses document.getSelection which does not work in Native Shadow.
     * Replace document.getSelection with shadow dom equivalent (different for each browser)
     **/
    this.quill.selection.getNativeRange = () => {
      const rootNode = (this.quill.root.getRootNode() as ShadowRoot);
      const nativeRange = getNativeRange(rootNode);
      return !!nativeRange ? this.quill.selection.normalizeNative(nativeRange) : null;
    };

    /**
     * Original implementation relies on Selection.addRange to programmatically set the range, which does not work
     * in Webkit with Native Shadow. Selection.addRange works fine in Chromium and Gecko.
     **/
    this.quill.selection.setNativeRange = (startNode: any, startOffset: any) => {
      var endNode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : startNode;
      var endOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : startOffset;
      var force = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
      if (startNode != null && (this.quill.selection.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) {
        return;
      }
      var selection = document.getSelection();
      if (selection == null) return;
      if (startNode != null) {
        if (!this.quill.selection.hasFocus()) this.quill.selection.root.focus();
        var native = (this.quill.selection.getNativeRange() || {}).native;
        if (native == null || force || startNode !== native.startContainer || startOffset !== native.startOffset || endNode !== native.endContainer || endOffset !== native.endOffset) {
          if (startNode.tagName == "BR") {
            startOffset = [].indexOf.call(startNode.parentNode.childNodes, startNode);
            startNode = startNode.parentNode;
          }
          if (endNode.tagName == "BR") {
            endOffset = [].indexOf.call(endNode.parentNode.childNodes, endNode);
            endNode = endNode.parentNode;
          }
          selection.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
        }
      } else {
        selection.removeAllRanges();
        this.quill.selection.root.blur();
        document.body.focus();
      }
    }

    this._editor = (this.shadow_el.querySelector('.ql-editor') as HTMLDivElement)
    this._toolbar = (this.shadow_el.querySelector('.ql-toolbar') as HTMLDivElement)

    const delta = this.quill.clipboard.convert(this.model.text);
    this.quill.setContents(delta);

    this.quill.on('text-change', () => {
      if (this._editing)
        return
      this._editing = true
      this.model.text = this._editor.innerHTML
      this._editing = false
    });
    if (!this.model.disabled)
      this.quill.enable(!this.model.disabled)

    document.addEventListener("selectionchange", (..._args: any[]) => {
      // Update selection and some other properties
      this.quill.selection.update()
    });
  }

  style_redraw(): void {
    if (this.model.visible)
      this.container.style.visibility = 'visible';

    const delta = this.quill.clipboard.convert(this.model.text);
    this.quill.setContents(delta);

    this.invalidate_layout()
  }

  after_layout(): void {
    super.after_layout()
    this._layout_toolbar()
  }
}

export namespace QuillInput {
  export type Attrs = p.AttrsOf<Props>

  export type Props = HTMLBox.Props & {
    mode:        p.Property<string>
    placeholder: p.Property<string>
    text:        p.Property<string>
    toolbar:     p.Property<any>
  }
}

export interface QuillInput extends QuillInput.Attrs { }

export class QuillInput extends HTMLBox {
  properties: QuillInput.Props

  constructor(attrs?: Partial<QuillInput.Attrs>) {
    super(attrs)
  }

  static __module__ = "panel.models.quill"

  static {
    this.prototype.default_view = QuillInputView

    this.define<QuillInput.Props>(({Any, String}) => ({
      mode:         [ String, 'toolbar' ],
      placeholder:  [ String,        '' ],
      text:         [ String,        '' ],
      toolbar:      [ Any,         null ],
    }))

    this.override<QuillInput.Props>({
      height: 300
    })
  }
}
