import { DataNamespace, DataProperty, DataPropertyType, DataPropertyTypes, EventUtils } from "@api-client/core/build/browser.js";
import { css, CSSResult, html, TemplateResult, nothing, PropertyValues } from "lit";
import { property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { live } from "lit/directives/live.js";
import { IPropertySchema } from "@api-client/core/build/src/models/data/Bindings.js";
import ApiElement from "../ApiElement.js";
import EditorCommon from './CommonStyles.js';
import theme from '../theme.js';
import typography from "../../styles/m3/typography.module.js";
import type CheckboxElement from "../../ui/input/CheckboxElement.js";
import type { SupportedInputTypes } from "../../types/input.js";
import type Input from "../../ui/input/Input.js";
import '../../define/ui/ui-text-field.js';
import '../../define/ui/ui-checkbox.js';
import '../../define/ui/ui-divider.js';
import '../../define/ui/ui-icon-button.js';
import '../../define/ui/ui-list.js';
import '../../define/ui/ui-list-item.js';
import '../../define/ui/ui-dropdown-list.js';
import '../../define/ui/ui-icon.js';

interface IInputTemplateOptions {
  type: DataPropertyType;
  name: string;
  label: string;
  handler: (e: Event) => void;
  value?: unknown;
  addButton?: boolean;
}

export default class PropertyFormElement extends ApiElement {
  static override get styles(): CSSResult[] {
    return [
      theme,
      EditorCommon,
      typography,
      css`
      :host {
        display: block;
      }

      .no-description {
        margin: 1.2rem 20px;
      }

      .enum-values {
        margin: 0px 0px 20px;
        padding: 0px 0px 20px;
      }

      .input-item {
        display: flex;
        list-style: none;
        margin: 4px 20px;
      }

      .input.list-input {
        margin: 0;
        flex: 1;
      }
      `
    ]
  }
  
  /**
   * The key of the property to edit.
   */
  @property({ type: String, reflect: true }) key?: string;

  /**
   * The read data namespace.
   */
  @property({ type: Object }) root?: DataNamespace;

  /**
   * The computed entity when the key or root change.
   */
  @state() protected _property?: DataProperty;

  override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('key')) {
      this._computeProperty();
    }
  }

  /**
   * Computes the view value for the property.
   * This should be called before the update is complete so this won't trigger another update.
   */
  protected _computeProperty(): void {
    const { root, key } = this;
    if (!root || !key) {
      this._property = undefined;
      return;
    }
    this._property = root.definitions.properties.find(i => i.key === key);
  }

  protected _infoChangeHandler(e: Event): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const node = e.target as Input;
    const { name, value } = node;
    if (!['name', 'description', 'displayName'].includes(name as string)) {
      return;
    }
    if (name === 'name' && !value) {
      return;
    }
    _property.info[name as 'name' | 'description' | 'displayName'] = value;
    this.notifyChange();
    this.requestUpdate();
  }

  setType(value: DataPropertyType): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    if (_property.type === value) {
      return;
    }
    _property.type = value;
    this.notifyChange();
    this.requestUpdate();
  }

  protected typeSelectHandler(e: CustomEvent): void {
    const target = e.detail.item as HTMLElement | undefined;
    if (!target) {
      return;
    }
    const value = target.dataset.value as DataPropertyType | undefined;
    if (value) {
      this.setType(value);
    }
  }

  protected _checkedHandler(e: Event): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const input = e.target as CheckboxElement;
    const name = input.name as 'multiple' | 'required' | 'primary' | 'index';
    const { checked } = input;
    if (_property[name] === checked) {
      return;
    }
    _property[name] = checked;
    this.notifyChange();
    this.requestUpdate();
  }

  protected defaultValueHandler(e: CustomEvent): void {
    const target = e.target as HTMLElement;
    let value: string | undefined;
    if (target.localName === 'ui-dropdown-list') {
      const item = e.detail.item as HTMLElement;
      value = item.dataset.value;
      this.requestUpdate();
    } else {
      const input = e.target as Input;
      value = input.value;
    }
    this.setDefaultValue(value);
  }

  /**
   * Sets a new defaultValue on the property schema
   * @param value The default value to set. Empty to remove.
   */
  setDefaultValue(value?: string): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const adapted = this._getAdapted(_property);
    const schema = adapted.schema as IPropertySchema;
    if (value === schema.defaultValue) {
      return;
    }
    if (value) {
      schema.defaultValue = value;
    } else {
      delete schema.defaultValue;
    }
    this.notifyChange();
  }

  /**
   * Adds an example value to the current property's schema.
   * @param value The example value to add
   */
  addExample(value: string): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const adapted = this._getAdapted(_property);
    const schema = adapted.schema as IPropertySchema;
    if (!schema.examples) {
      schema.examples = [];
    }
    const list = schema.examples as string[];
    list.push(value as string);
    this.notifyChange();
    this.requestUpdate();
  }

  /**
   * Removes an example value at position.
   * @param index The position from which to remove the example value.
   */
  removeExample(index: number): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const adapted = this._getAdapted(_property);
    const schema = adapted.schema as IPropertySchema;
    if (!schema.examples) {
      return;
    }
    schema.examples.splice(index, 1);
    this.notifyChange();
    this.requestUpdate();
  }

  protected exampleAddHandler(e: Event): void {
    const input = (e.currentTarget as HTMLElement).parentElement as Input;
    const { value } = input;
    if (!value) {
      return;
    }
    this.addExample(value);
    input.value = '';
  }

  protected exampleRemoveHandler(e: Event): void {
    const node = e.currentTarget as HTMLElement;
    const { dataset } = node;
    if (!dataset.index) {
      return;
    }
    const index = Number(dataset.index);
    this.removeExample(index);
  }

  /**
   * Adds an enum value to the current property's schema.
   * @param value The enum value to add
   */
  addEnum(value: string): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const adapted = this._getAdapted(_property);
    const schema = adapted.schema as IPropertySchema;
    if (!schema.enum) {
      schema.enum = [];
    }
    const list = schema.enum as string[];
    list.push(value as string);
    this.notifyChange();
    this.requestUpdate();
  }

  /**
   * Removes an enum value at position.
   * @param index The position from which to remove the enum value.
   */
  removeEnum(index: number): void {
    const { _property } = this;
    if (!_property) {
      return;
    }
    const adapted = this._getAdapted(_property);
    const schema = adapted.schema as IPropertySchema;
    if (!schema.enum) {
      return;
    }
    schema.enum.splice(index, 1);
    this.notifyChange();
    this.requestUpdate();
  }

  protected enumAddHandler(e: Event): void {
    const input = (e.currentTarget as HTMLElement).parentElement as Input;
    const { value } = input;
    if (!value) {
      return;
    }
    this.addEnum(value);
    input.value = '';
  }

  protected enumRemoveHandler(e: Event): void {
    const node = e.currentTarget as HTMLElement;
    const { dataset } = node;
    if (!dataset.index) {
      return;
    }
    const index = Number(dataset.index);
    this.removeEnum(index);
  }

  protected _getAdapted(dataProperty: DataProperty): DataProperty {
    let adapted: DataProperty;
    if (!dataProperty.adapts) {
      adapted = dataProperty.createAdapted();
    } else {
      adapted = dataProperty.readAdapted() as DataProperty;
    }
    if (!adapted.schema) {
      adapted.schema = {};
    }
    return adapted;
  }

  protected override render(): TemplateResult {
    const { _property: item } = this;
    if (!item) {
      return html``;
    }
    const { info, multiple = false, required = false, primary = false, index = false, readOnly = false, writeOnly = false, deprecated = false } = item;
    // const allTags = this.root?.definitions.tags;
    return html`
    <form name="data-property" @submit="${EventUtils.cancelEvent}" data-key="${item.key}">
      <ui-text-field class="input" name="name" .value="${live(info.name || '')}" label="Property name" required @change="${this._infoChangeHandler}"></ui-text-field>
      <ui-text-field class="input" name="displayName" .value="${live(info.displayName || '')}" label="Display name (optional)" @change="${this._infoChangeHandler}"></ui-text-field>
      <ui-text-field class="input" name="description" .value="${live(info.description || '')}" label="Property description (optional)" @change="${this._infoChangeHandler}"></ui-text-field>
      ${this.renderTypeSelector(item)}

      <div class="checkbox-group">
        <label class="label-large">
          <ui-checkbox name="required" .checked="${required}" title="Whether the property is required in the schema" @change="${this._checkedHandler}"></ui-checkbox>
          Required
        </label>
        <label class="label-large">
          <ui-checkbox name="multiple" .checked="${multiple}" title="When set it declares this property as an array. Multiple instances of the value are permitted." @change="${this._checkedHandler}"></ui-checkbox>
          Multiple
        </label>
        <label class="label-large">
          <ui-checkbox name="primary" .checked="${primary}" title="Makes this property a primary key for the schema. This is optional for a schema." @change="${this._checkedHandler}"></ui-checkbox>
          Primary key
        </label>
        <label class="label-large">
          <ui-checkbox name="index" .checked="${index}" title="Indicates the property is a key and should be indexed." @change="${this._checkedHandler}"></ui-checkbox>
          Indexed key
        </label>
        <label class="label-large">
          <ui-checkbox name="readOnly" .checked="${readOnly}" title="The value cannot be changed." @change="${this._checkedHandler}"></ui-checkbox>
          Read only
        </label>
        <label class="label-large">
          <ui-checkbox name="writeOnly" .checked="${writeOnly}" title="The value cannot be read." @change="${this._checkedHandler}"></ui-checkbox>
          Write only
        </label>
        <label class="label-large">
          <ui-checkbox name="deprecated" .checked="${deprecated}" title="Makes this property deprecated." @change="${this._checkedHandler}"></ui-checkbox>
          Deprecated
        </label>
      </div>
      
      ${this.renderSchemaEditor(item)}
    </form>
    `;
  }

  protected renderSchemaEditor(item: DataProperty): TemplateResult | typeof nothing {
    if (['binary'].includes(item.type)) {
      return nothing;
    }
    const adapted = item.readAdapted();
    const schema = adapted && adapted.schema;
    let examples: string[] | undefined;
    let values: string[] | undefined;
    if (schema) {
      examples = schema.examples;
      values = schema.enum;
    }
    return html`
    <ui-divider type="full"></ui-divider>
    <div class="editor-title title-medium">Schema</div>
    ${this.renderDefaultValueInput(item)}
    ${this.renderExamplesEditor(item, examples)}
    ${this.renderEnumEditor(item, values)}
    `;
  }

  protected renderDefaultValueInput(item: DataProperty): TemplateResult | typeof nothing {
    const adapted = item.readAdapted();
    const schema = adapted && adapted.schema;
    return this.renderTypedInput({
      name: 'defaultValue',
      label: 'Enter default value (optional)',
      type: item.type,
      handler: this.defaultValueHandler as (e: Event) => void,
      value: schema?.defaultValue,
    });
  }

  protected renderExamplesEditor(item: DataProperty, examples?: string[]): TemplateResult | typeof nothing {
    if (['nil', 'boolean', 'binary'].includes(item.type)) {
      return nothing;
    }
    return html`
    <ui-divider type="middle"></ui-divider>
    <div class="editor-title title-small">Examples</div>
    ${this.renderExampleInput(item)}
    ${this.renderExampleList(examples)}
    `;
  }

  protected renderExampleInput(item: DataProperty): TemplateResult {
    return this.renderTypedInput({
      name: 'example',
      label: 'Enter example value',
      type: item.type,
      handler: this.exampleAddHandler,
      addButton: true,
    });
  }

  protected renderExampleList(examples?: string[]): TemplateResult {
    if (!examples || !examples.length) {
      return html`<div class="no-description">No examples.</div>`;
    }
    return html`
    <ul class="value-list">
      ${examples.map((label, index) => html`
        <li>
          <span class="label-medium value" title="${label}">${label}</span>
          <ui-icon-button aria-label="Remove example" title="Removes the example" data-index="${index}" @click="${this.exampleRemoveHandler}">
            <ui-icon icon="deleteOutline" role="presentation"></ui-icon>
          </ui-icon-button>
        </li>
      `)}
    </ul>
    `;
  }

  protected renderEnumEditor(item: DataProperty, values?: string[]): TemplateResult | typeof nothing {
    if (['nil', 'boolean', 'binary'].includes(item.type)) {
      return nothing;
    }
    return html`
    <ui-divider type="middle"></ui-divider>
    <div class="editor-title title-small">Enum values</div>
    ${this.renderEnumInput(item)}
    ${this.renderEnumList(values)}
    `;
  }

  protected renderEnumInput(item: DataProperty): TemplateResult {
    return this.renderTypedInput({
      name: 'enum',
      label: 'Enter enum value',
      type: item.type,
      handler: this.enumAddHandler,
      addButton: true,
    });
  }

  protected renderEnumList(values?: string[]): TemplateResult {
    if (!values || !values.length) {
      return html`
      <div class="no-description">No enum values.</div>
      `;
    }
    return html`
    <ul class="value-list">
      ${values.map((label, index) => html`
        <li>
          <span class="label-medium value" title="${label}">${label}</span>
          <ui-icon-button aria-label="Remove enum value" title="Removes the enum value" data-index="${index}" @click="${this.enumRemoveHandler}">
            <ui-icon icon="deleteOutline" role="presentation"></ui-icon>
          </ui-icon-button>
        </li>
      `)}
    </ul>
    `;
  }

  protected propertyToInputType(type: DataPropertyType): SupportedInputTypes {
    switch (type) {
      // for `number` type we use text with `inputmode=numeric` attribute to mitigate the use of `step`.
      case 'integer': return 'number'; 
      case 'date': return 'date';
      case 'time': return 'time';
      case 'datetime': return 'datetime-local';
      default: return 'text';
    }
  }

  /**
   * Renders an input that depends on the property type.
   */
  protected renderTypedInput(opts: IInputTemplateOptions): TemplateResult {
    if (opts.type === 'boolean') {
      return this.renderBooleanInput(opts);
    }
    if (opts.type === 'nil') {
      return this.renderNilInput(opts);
    }
    return this.renderTextField(opts);
  }

  protected renderTextField(opts: IInputTemplateOptions): TemplateResult {
    const inputType = this.propertyToInputType(opts.type);
    let step: string | undefined;
    let inputMode: string | undefined;
    let pattern: string | undefined;
    if (['time', 'datetime-local'].includes(opts.type) || opts.type === 'integer') {
      step = '1';
    } else if (opts.type === 'number') {
      inputMode = 'numeric';
      pattern = '^\\d*\\.?\\d*$';
    }
    const changeListener = opts.addButton ? undefined : opts.handler;
    return html`
    <ui-text-field 
      class="input" 
      name="${opts.name}" 
      .value="${live(opts.value as string || '')}" 
      label="${opts.label}" 
      type="${inputType}" 
      step="${ifDefined(step)}"
      inputMode="${ifDefined(inputMode)}"
      pattern="${ifDefined(pattern)}"
      @change="${changeListener}"
    >
      ${opts.addButton ? html`
      <ui-icon-button type="outlined" slot="suffix" aria-label="Add value" title="Click this button to accept the value" @click="${opts.handler}">
        <ui-icon icon="add" role="presentation"></ui-icon>
      </ui-icon-button>
      ` : ''}
    </ui-text-field>
    `;
  }

  protected renderNilInput(opts: IInputTemplateOptions): TemplateResult {
    return html`
    <ui-dropdown-list class="input dropdown" @select="${opts.handler}" closeOnOutsideClick>
      <ui-text-field readOnly label="${opts.label}" name="${opts.name}" .value="${opts.value as string || ''}" role="button">
        <ui-icon slot="suffix" icon="arrowDropDown"></ui-icon>
      </ui-text-field>
      <ui-list slot="dropdown" role="menu">
        <ui-list-item data-value="" role="menuitem">None</ui-list-item>
        <ui-list-item data-value="null" role="menuitem">Nil</ui-list-item>
      </ui-list>
    </ui-dropdown-list>
    `;
  }

  protected renderBooleanInput(opts: IInputTemplateOptions): TemplateResult {
    return html`
    <ui-dropdown-list class="input dropdown" @select="${opts.handler}" closeOnOutsideClick>
      <ui-text-field readOnly label="${opts.label}" name="${opts.name}" .value="${opts.value as string || ''}" role="button">
        <ui-icon slot="suffix" icon="arrowDropDown"></ui-icon>
      </ui-text-field>
      <ui-list slot="dropdown" role="menu">
        <ui-list-item data-value="" role="menuitem">None</ui-list-item>
        <ui-list-item data-value="true" role="menuitem">True</ui-list-item>
        <ui-list-item data-value="false" role="menuitem">False</ui-list-item>
      </ui-list>
    </ui-dropdown-list>
    `;
  }

  protected renderTypeSelector(item: DataProperty): TemplateResult {
    const { type = 'any' } = item;
    return html`
    <ui-dropdown-list class="input dropdown" @select="${this.typeSelectHandler}" closeOnOutsideClick>
      <ui-text-field readOnly title="Property data type" label="Data type" .value="${type}" role="button">
        <ui-icon slot="suffix" icon="arrowDropDown"></ui-icon>
      </ui-text-field>
      <ui-list slot="dropdown" role="menu">
        ${DataPropertyTypes.map(value => html`<ui-list-item data-value="${value}" role="menuitem">${value}</ui-list-item>`)}
      </ui-list>
    </ui-dropdown-list>
    `;
  }
}
