/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
@license
Copyright 2018 The Advanced REST client authors <arc@mulesoft.com>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
*/
import { html, nothing, TemplateResult } from 'lit'
import { property, query } from 'lit/decorators.js'
// import * as monaco from 'monaco-editor'; // /esm/vs/editor/editor.main.js
import { editor, Uri } from 'monaco-editor'
import { assignTheme } from '../../monaco/theme.js'
import { detectLanguage } from '../../monaco/helper.js'
import { MonacoSchema } from '../../monaco/types.js'
import * as ScrollHelper from '../../lib/ScrollHelper.js'
import { Events } from '../../events/Events.js'
import ApiElement from '../ApiElement.js'

/* global monaco */

let modelId = 0

export default class BodyTextEditor extends ApiElement {
  @query('#container') accessor container!: HTMLDivElement

  protected modelUri?: Uri

  protected valueInternal = ''

  /**
   * A HTTP body.
   * @attribute
   */
  get value(): string | undefined {
    return this.valueInternal
  }

  @property({ type: String })
  set value(value) {
    const old = this.valueInternal
    if (old === value) {
      return
    }
    this.valueInternal = value || ''
    const { editor: e } = this
    if (e) {
      e.setValue(value || '')
    }
  }

  protected contentTypeInternal?: string

  protected languageInternal?: string

  /**
   * The current language of the editor. It is set via the `contentType`.
   */
  get language(): string | undefined {
    return this.languageInternal
  }

  protected readOnlyValue = false

  /**
   * Uses the current content type to detect language support.
   * @attribute
   */
  get contentType(): string | undefined {
    return this.contentTypeInternal
  }

  @property({ type: String, reflect: true })
  set contentType(value: string | undefined) {
    const old = this.contentTypeInternal
    if (old === value) {
      return
    }
    this.contentTypeInternal = value
    const oldLang = this.languageInternal
    const lang = detectLanguage(value)
    if (oldLang === lang) {
      return
    }
    this.languageInternal = lang
    this.setLanguage(lang)
  }

  /**
   * When set the editor is in read only mode.
   * @attribute
   */
  get readOnly(): boolean {
    return this.readOnlyValue
  }

  @property({ type: Boolean, reflect: true })
  set readOnly(value: boolean) {
    const old = this.readOnlyValue
    if (old === value) {
      return
    }
    this.readOnlyValue = value
    this.setEditorConfigProperty('readOnly', value)
  }

  protected monacoInstance?: editor.IStandaloneCodeEditor

  get editor(): editor.IStandaloneCodeEditor | undefined {
    return this.monacoInstance
  }

  protected validationSchemas?: MonacoSchema[]

  get schemas(): MonacoSchema[] | undefined {
    return this.validationSchemas
  }

  set schemas(value: MonacoSchema[] | undefined) {
    const old = this.validationSchemas
    if (old === value) {
      return
    }
    this.validationSchemas = value
    this.updateEditorSchemas(value)
  }

  protected observer: ResizeObserver

  /**
   * @attribute
   */
  @property({ type: Boolean }) accessor allowFormInfo: boolean | undefined

  constructor() {
    super()
    this.valueChanged = this.valueChanged.bind(this)
    this.observer = new ResizeObserver(this.handleObservedResize.bind(this))
  }

  override connectedCallback(): void {
    super.connectedCallback()
    ScrollHelper.addListeners(this, this.resizeHandler.bind(this))
    const { container } = this
    if (container) {
      this.observer.observe(container)
    }
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    ScrollHelper.removeListeners(this)
    this.editor?.dispose()
    const { container } = this
    if (container) {
      this.observer.unobserve(container)
    }
  }

  override firstUpdated(): void {
    const config = this.generateEditorConfig()
    const node = this.container
    // @ts-ignore
    const instance = monaco.editor.create(node, config) as editor.IStandaloneCodeEditor
    // instance.onDidChangeModelContent(this.valueChanged);
    // We use `onDidBlurEditorText` which is the closest we can get to an input's
    // `change` event. This will reduce the number of updates on the parent
    // editor.
    instance.onDidBlurEditorText(this.valueChanged)
    this.monacoInstance = instance
    this.setupActions(instance)
    this.observer.observe(node)
  }

  protected refreshDebouncer?: number

  /**
   * Handler for the `resize` event provided by the resizable mixin.
   */
  protected resizeHandler(): void {
    if (this.refreshDebouncer) {
      clearTimeout(this.refreshDebouncer)
    }
    this.refreshDebouncer = setTimeout(this.refreshEditor.bind(this)) as unknown as number
  }

  /**
   * A function that is called from the `[resizeHandler]` in a timeout to reduce the number
   * of computations during the application initialization, where it can receive hundreds of
   * the `resize` events.
   */
  protected refreshEditor(): void {
    this.refreshDebouncer = undefined
    if (!this.monacoInstance) {
      return
    }
    this.monacoInstance.layout()
  }

  /**
   * @param lang New language to set
   */
  protected setLanguage(lang?: string): void {
    const { editor: e } = this
    if (!e || !lang) {
      return
    }
    const model = e.getModel()
    if (model) {
      // @ts-ignore
      monaco.editor.setModelLanguage(model, lang)
    }
  }

  /**
   * Sets up editor actions
   */
  protected setupActions(inst: editor.IStandaloneCodeEditor): void {
    inst.addAction({
      id: 'send-http-request',
      label: 'Send request',
      keybindings: [
        // @ts-ignore
        monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
      ],
      precondition: undefined,
      keybindingContext: undefined,
      contextMenuGroupId: 'navigation',
      contextMenuOrder: 1.5,
      run: (): void => {
        Events.Http.Request.send(this)
      },
    })
  }

  protected changeTimeout?: number

  protected valueChanged(): void {
    const { editor: ed } = this
    if (!ed) {
      return
    }
    const editorValue = ed.getValue()
    if (editorValue === this.valueInternal) {
      return
    }
    this.valueInternal = editorValue
    if (this.changeTimeout) {
      // window.clearTimeout(this[changeTimeout]);
      window.cancelAnimationFrame(this.changeTimeout)
    }
    this.changeTimeout = window.requestAnimationFrame(() => {
      this.notifyChange()
    })
  }

  /**
   * Generates Monaco configuration
   */
  protected generateEditorConfig(): editor.IStandaloneEditorConstructionOptions {
    const { value = '', readOnly, languageInternal: language } = this
    // @ts-ignore
    this.modelUri = monaco.Uri.parse(`http://raw-editor/model${++modelId}.json`)
    // @ts-ignore
    const model = monaco.editor.createModel(value, language || 'json', this.modelUri)

    const schemas = this.getCurrentSchemas()
    this.updateEditorSchemas(schemas)

    let config: editor.IStandaloneEditorConstructionOptions = {
      minimap: {
        enabled: false,
      },
      readOnly,
      formatOnType: true,
      folding: true,
      tabSize: 2,
      detectIndentation: true,
      // value,
      // automaticLayout: true,
      model,
      padding: { top: 0, bottom: 0 },
      scrollBeyondLastLine: false,
      foldingHighlight: true,
      selectionHighlight: true,
    }
    // @ts-ignore
    config = assignTheme(monaco, config)
    if (language) {
      config.language = language
    }
    return config
  }

  protected getCurrentSchemas(): MonacoSchema[] {
    const { schemas } = this
    if (Array.isArray(schemas) && schemas.length) {
      schemas[0].fileMatch = [this.modelUri?.toString() || '']
      return schemas
    }
    return [this.createDefaultSchema()]
  }

  protected createDefaultSchema(): MonacoSchema {
    const schema: MonacoSchema = {
      uri: 'http://raw-editor/default-schema.json',
      fileMatch: [this.modelUri?.toString() || ''],
      schema: {},
    }
    return schema
  }

  protected updateEditorSchemas(schemas?: MonacoSchema[]): void {
    if (!this.modelUri || !schemas) {
      // the editor is not yet ready. This will be called again when it is ready.
      return
    }
    let value = schemas
    if (!Array.isArray(value) || !value.length) {
      value = [this.createDefaultSchema()]
    } else {
      value[0] = { ...value[0], fileMatch: [this.modelUri.toString()] }
    }
    // @ts-ignore
    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
      validate: true,
      schemas: value,
    })
  }

  /**
   * @param prop The property to set
   */
  protected setEditorConfigProperty(prop: keyof editor.IEditorOptions, value: unknown): void {
    const { editor: e } = this
    if (!e) {
      return
    }
    const opts = {
      [prop]: value,
    }
    e.updateOptions(opts)
  }

  protected handleObservedResize(entries: ResizeObserverEntry[]): void {
    const { editor: e } = this
    if (!e) {
      return
    }
    const [entry] = entries
    const rect = entry.contentRect
    e.layout({ height: rect.height, width: rect.width })
  }

  override render(): TemplateResult {
    return html`
      <div id="container"></div>
      ${this.renderFormInfo()}
    `
  }

  protected renderFormInfo(): TemplateResult | typeof nothing {
    if (!this.allowFormInfo) {
      return nothing
    }
    return html`
      <p class="form-info">
        Note that the content-type header will be added when the request is sent. We encourage you to use variables to
        insert sensitive data.
      </p>
    `
  }
}
