/**
 * Copyright Aquera Inc 2023
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {
  LitElement,
  html,
  CSSResultArray,
  TemplateResult,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styles } from './nile-input.css';
import '../nile-icon';
import { classMap } from 'lit/directives/class-map.js';
import { query, state } from 'lit/decorators.js';
import { defaultValue } from '../internal/default-value';
import { FormControlController } from '../internal/form';
import { HasSlotController } from '../internal/slot';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { watch } from '../internal/watch';
import type { CSSResultGroup } from 'lit';
import NileElement, { NileFormControl } from '../internal/nile-element';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

/**
 * Nile icon component.
 *
 * @tag nile-input
 *
 */
@customElement('nile-input')
export class NileInput extends NileElement implements NileFormControl {
  static styles: CSSResultGroup = styles;

  private readonly formControlController = new FormControlController(this, {
    assumeInteractionOn: ['nile-blur', 'nile-input'],
  });
  private readonly hasSlotController = new HasSlotController(
    this,
    'help-text',
    'label'
  );

  @query('.input__control') input: HTMLInputElement;

  @state() private hasFocus = false;
  @property() title = ''; // make reactive to pass through

  /**
   * The type of input. Works the same as a native `<input>` element, but only a subset of types are supported. Defaults
   * to `text`.
   */
  @property({ reflect: true }) type:
    | 'date'
    | 'datetime-local'
    | 'email'
    | 'number'
    | 'password'
    | 'search'
    | 'tel'
    | 'text'
    | 'time'
    | 'url' = 'text';

  /** The name of the input, submitted as a name/value pair with form data. */
  @property() name = '';

  /** The current value of the input, submitted as a name/value pair with form data. */
  @property() value = '';

  @property({ type: Boolean }) checkNonPrintableChar: boolean = false;

  /** The default value of the form control. Primarily used for resetting the form control. */
  @defaultValue() defaultValue = '';

  /** The input's size. */
  @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';

  /** Draws a filled input. */
  @property({ type: Boolean, reflect: true }) filled = false;

  /** Sets the input to a warning state, changing its visual appearance. */
  @property({ type: Boolean }) warning = false;

  /** Sets the input to an error state, changing its visual appearance. */
  @property({ type: Boolean }) error = false;

  /** Sets the input to a success state, changing its visual appearance. */
  @property({ type: Boolean }) success = false;

  /** Sets the input to a Destructive state, changing its visual appearance. */
  @property({ type: Boolean }) destructive = false;

  /** Draws a pill-style input with rounded edges. */
  @property({ type: Boolean, reflect: true }) pill = false;

  /** The input's label. If you need to display HTML, use the `label` slot instead. */
  @property() label = '';

  @property({ attribute: 'help-text', reflect: true }) helpText = '';

  @property({ attribute: 'error-message', reflect: true }) errorMessage = '';

  /** Adds a clear button when the input is not empty. */
  @property({ type: Boolean }) clearable = false;

  /** Disables the input. */
  @property({ type: Boolean, reflect: true }) disabled = false;

  /** Placeholder text to show as a hint when the input is empty. */
  @property() placeholder = '';

  /** Makes the input readonly. */
  @property({ type: Boolean, reflect: true }) readonly = false;

  /** Adds a button to toggle the password's visibility. Only applies to password types. */
  @property({ attribute: 'password-toggle', type: Boolean }) passwordToggle =
    false;

  /** Determines whether or not the password is currently visible. Only applies to password input types. */
  @property({ attribute: 'password-visible', type: Boolean }) passwordVisible =
    false;

  /** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
  @property({ attribute: 'no-spin-buttons', type: Boolean }) noSpinButtons =
    false;

  /**
   * By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
   * to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
   * the same document or shadow root for this to work.
   */
  @property({ reflect: true }) form = '';

  /** Makes the input a required field. */
  @property({ type: Boolean, reflect: true }) required = false;

  /** A regular expression pattern to validate input against. */
  @property() pattern: string;

  /** The minimum length of input that will be considered valid. */
  @property({ type: Number }) minlength: number;

  /** The maximum length of input that will be considered valid. */
  @property({ type: Number }) maxlength: number;

  /** The input's minimum value. Only applies to date and number input types. */
  @property() min: number | string;

  /** The input's maximum value. Only applies to date and number input types. */
  @property() max: number | string;

  /**
   * Specifies the granularity that the value must adhere to, or the special value `any` which means no stepping is
   * implied, allowing any numeric value. Only applies to date and number input types.
   */
  @property() step: number | 'any';

  /** Controls whether and how text input is automatically capitalized as it is entered by the user. */
  @property() autocapitalize:
    | 'off'
    | 'none'
    | 'on'
    | 'sentences'
    | 'words'
    | 'characters';

  /** Indicates whether the browser's autocorrect feature is on or off. */
  @property() autocorrect: 'off' | 'on';

  /**
   * Specifies what permission the browser has to provide assistance in filling out form field values. Refer to
   * [this page on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for available values.
   */
  @property() autocomplete: string = 'off';

  /** Indicates that the input should receive focus on page load. */
  @property({ type: Boolean, attribute: true, reflect: true }) autofocus: boolean;

  /** Used to customize the label or icon of the Enter key on virtual keyboards. */
  @property() enterkeyhint:
    | 'enter'
    | 'done'
    | 'go'
    | 'next'
    | 'previous'
    | 'search'
    | 'send';

  /** Enables spell checking on the input. */
  @property({
    type: Boolean,
    converter: {
      // Allow "true|false" attribute values but keep the property boolean
      fromAttribute: value => (!value || value === 'false' ? false : true),
      toAttribute: value => (value ? 'true' : 'false'),
    },
  })
  spellcheck = true;

  @property({ type: Boolean, reflect: true }) canSavePassword = false;

  /**
   * Tells the browser what type of data will be entered by the user, allowing it to display the appropriate virtual
   * keyboard on supportive devices.
   */
  @property() inputmode:
    | 'none'
    | 'text'
    | 'decimal'
    | 'numeric'
    | 'tel'
    | 'search'
    | 'email'
    | 'url';

  //
  // NOTE: We use an in-memory input for these getters/setters instead of the one in the template because the properties
  // can be set before the component is rendered.
  //

  @state() hasPrintableCharacters: boolean = false;

  @state() markedValue: string;

  @property() noOutline: any = false;

  @property() noPadding: any = false;

  connectedCallback() {
    super.connectedCallback();
    this.emit('nile-init');
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.emit('nile-destroy');
  }

  /** Gets or sets the current value as a `Date` object. Returns `null` if the value can't be converted. */
  get valueAsDate() {
    const input = document.createElement('input');
    input.type = 'date';
    input.value = this.value;
    return input.valueAsDate;
  }

  set valueAsDate(newValue: Date | null) {
    const input = document.createElement('input');
    input.type = 'date';
    input.valueAsDate = newValue;
    this.value = input.value;
  }

  /** Gets or sets the current value as a number. Returns `NaN` if the value can't be converted. */
  get valueAsNumber() {
    const input = document.createElement('input');
    input.type = 'number';
    input.value = this.value;
    return input.valueAsNumber;
  }

  set valueAsNumber(newValue: number) {
    const input = document.createElement('input');
    input.type = 'number';
    input.valueAsNumber = newValue;
    this.value = input.value;
  }

  /** Gets the validity state object */
  get validity() {
    return this.input.validity;
  }

  /** Gets the validation message */
  get validationMessage() {
    return this.input.validationMessage;
  }

  firstUpdated() {
    this.emit('nile-after-init');
    this.formControlController.updateValidity();
  }

  private handleBlur() {
    this.hasFocus = false;
    this.emit('nile-blur', { value: this.value });
  }

  private handleChange() {
    this.value = this.input.value;
    this.emit('nile-change', { value: this.value });
  }

  private handleClearClick(event: MouseEvent) {
    this.value = '';
    this.emit('nile-clear', { value: this.value });
    this.emit('nile-input', { value: this.value });
    this.emit('nile-change', { value: this.value });
    this.input.focus();

    event.stopPropagation();
  }

  private handleFocus() {
    this.hasFocus = true;
    this.emit('nile-focus', { value: this.value });

    if (this.checkNonPrintableChar) {
      this.markNonPrintableCharacters();
    }
  }

  private handleInput() {
    this.value = this.input.value;
    this.formControlController.updateValidity();
    if(this.value && this.type === 'number') {
      this.emit('nile-input', { value: Number(this.value) });
    } else {
      this.emit('nile-input', { value: this.value });
    }
  }

  private handleInvalid(event: Event) {
    this.formControlController.setValidity(false);
    this.formControlController.emitInvalidEvent(event);
  }

  private handleKeyDown(event: KeyboardEvent) {
    const hasModifier =
      event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;

    // Pressing enter when focused on an input should submit the form like a native input, but we wait a tick before
    // submitting to allow users to cancel the keydown event if they need to
    if (event.key === 'Enter' && !hasModifier) {
      setTimeout(() => {
        //
        // When using an Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
        // to check for this is to look at event.isComposing, which will be true when the IME is open.
        //

        //
        if (!event.defaultPrevented && !event.isComposing) {
          this.formControlController.submit();
        }
      });
    }
  }

  private handlePasswordToggle() {
    this.passwordVisible = !this.passwordVisible;
  }

  @watch('disabled', { waitUntilFirstUpdate: true })
  handleDisabledChange() {
    // Disabled form controls are always valid
    this.formControlController.setValidity(this.disabled);
  }

  @watch('step', { waitUntilFirstUpdate: true })
  handleStepChange() {
    // If step changes, the value may become invalid so we need to recheck after the update. We set the new step
    // imperatively so we don't have to wait for the next render to report the updated validity.
    this.input.step = String(this.step);
  }

  @watch('value', { waitUntilFirstUpdate: true })
  async handleValueChange() {
    await this.updateComplete;

    if (this.checkNonPrintableChar) {
      this.markNonPrintableCharacters();
    }
  }

  /** checks non printable characters in the value. */
  markNonPrintableCharacters() {
    let markedValue = '';
    this.hasPrintableCharacters = false;
    if (this.value) {
      for (let i = 0, n = this.value.length; i < n; i++) {
        const charCode = this.value.charCodeAt(i);

        if (charCode > 127) {
          markedValue += `<span class="input__srtiked-text">${this.value.charAt(
            i
          )}</span>`;
          this.hasPrintableCharacters = true;
        } else {
          markedValue += this.value.charAt(i);
        }
      }
    }

    this.markedValue = markedValue;
    this.emit('nile-value-marked', {
      value: this.markedValue,
      hasNonPrintableCharacters: this.hasPrintableCharacters,
    });
  }

  /** Removes all non printable characters from the value. */
  removeAllNonPrintableCharacters() {
    let cleanedValue = '';

    if (this.value) {
      for (let i = 0, n = this.value.length; i < n; i++) {
        const charCode = this.value.charCodeAt(i);

        // Consider a character printable if it's not in the specified non-printable ranges
        if (charCode < 128) {
          cleanedValue += this.value.charAt(i);
        }
      }
    }

    this.value = cleanedValue;
    this.emit('nile-npchar-removed', { value: this.value });
  }

  /** Sets focus on the input. */
  focus(options?: FocusOptions) {
    this.input.focus(options);
  }

  /** Removes focus from the input. */
  blur() {
    this.input.blur();
  }

  /** Selects all the text in the input. */
  select() {
    this.input.select();
  }

  /** Sets the start and end positions of the text selection (0-based). */
  setSelectionRange(
    selectionStart: number,
    selectionEnd: number,
    selectionDirection: 'forward' | 'backward' | 'none' = 'none'
  ) {
    this.input.setSelectionRange(
      selectionStart,
      selectionEnd,
      selectionDirection
    );
  }

  /** Replaces a range of text with a new string. */
  setRangeText(
    replacement: string,
    start?: number,
    end?: number,
    selectMode?: 'select' | 'start' | 'end' | 'preserve'
  ) {
    // @ts-expect-error - start, end, and selectMode are optional
    this.input.setRangeText(replacement, start, end, selectMode);

    if (this.value !== this.input.value) {
      this.value = this.input.value;
    }
  }

  // /** Displays the browser picker for an input element (only works if the browser supports it for the input type). */
  // showPicker() {
  //   if ('showPicker' in HTMLInputElement.prototype) {
  //     this.input.showPicker();
  //   }
  // }

  /** Increments the value of a numeric input type by the value of the step attribute. */
  stepUp() {
    this.input.stepUp();
    if (this.value !== this.input.value) {
      this.value = this.input.value;
    }
  }

  /** Decrements the value of a numeric input type by the value of the step attribute. */
  stepDown() {
    this.input.stepDown();
    if (this.value !== this.input.value) {
      this.value = this.input.value;
    }
  }

  /** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
  checkValidity() {
    return this.input.checkValidity();
  }

  /** Gets the associated form, if one exists. */
  getForm(): HTMLFormElement | null {
    return this.formControlController.getForm();
  }

  /** Checks for validity and shows the browser's validation message if the control is invalid. */
  reportValidity() {
    return this.input.reportValidity();
  }

  /** Sets a custom validation message. Pass an empty string to restore validity. */
  setCustomValidity(message: string) {
    this.input.setCustomValidity(message);
    this.formControlController.updateValidity();
  }

  public inputFocus() {
    this.input.focus();
  }

  render() {
    const hasLabelSlot = this.hasSlotController.test('label');
    const hasHelpTextSlot = this.hasSlotController.test('help-text');
    const hasLabel = this.label ? true : !!hasLabelSlot;
    const hasLabelSuffixSlot = this.hasSlotController.test('label-suffix');
    const hasHelpText = this.helpText ? true : false;
    const hasErrorMessage = this.errorMessage ? true : false;
    const hasClearIcon =
      this.clearable &&
      !this.disabled &&
      !this.readonly &&
      (typeof this.value === 'number' || this.value.length > 0);

    return html`
      <div
        part="form-control"
        class=${classMap({
          'form-control': true,
          'form-control--small': this.size === 'small',
          'form-control--medium': this.size === 'medium',
          'form-control--large': this.size === 'large',
          'form-control--has-label': hasLabel,
          'form-control--has-help-text': hasHelpText,
        })}
      >
        <label
          part="form-control-label"
          class="form-control__label"
          for="input"
          aria-hidden=${hasLabel ? 'false' : 'true'}
        >
          <slot name="label">${this.label}</slot>
        </label>

        ${hasLabelSuffixSlot ? html` <slot name="label-suffix"></slot> ` : ``}

        <div part="form-control-input" class="form-control-input">
        ${this.checkNonPrintableChar
  ? html`
      <nile-popup ?active=${this.hasPrintableCharacters}
        placement="bottom-start"
        distance="5"
        exportparts="input"
        strategy="fixed"
      >
        <div
          slot="anchor"
          part="base"
          class=${classMap({
            input: true,

            // Sizes
            'input--small': this.size === 'small',
            'input--medium': this.size === 'medium',
            'input--large': this.size === 'large',

            // States
            'input--pill': this.pill,
            'input--standard': !this.filled,
            'input--warning': this.warning,
            'input--error': this.error,
            'input--success': this.success,
            'input--destructive': this.destructive,
            'input--filled': this.filled,
            'input--disabled': this.disabled,
            'input--focused': this.hasFocus,
            'input--empty': !this.value,
            'input--no-spin-buttons': this.noSpinButtons,
          })}
        >
          <slot name="prefix" part="prefix" class="input__prefix"></slot>
          <input
            part="input"
            id="input"
            class="input__control ${
              this.type === 'password' && !this.passwordVisible
                ? 'input__password'
                : ''
            }"
            type=${this.type === 'password'
              ? this.passwordVisible
                ? 'text'
                : 'password'
              : this.type}
            title=${
              this.title /* An empty title prevents browser validation tooltips from appearing on hover */
            }
            name=${ifDefined(this.name)}
            ?disabled=${this.disabled}
            tabindex=${this.disabled ? '-1' : '0'}
            ?readonly=${this.readonly}
            ?required=${this.required}
            placeholder=${ifDefined(this.placeholder)}
            minlength=${ifDefined(this.minlength)}
            maxlength=${ifDefined(this.maxlength)}
            min=${ifDefined(this.min)}
            max=${ifDefined(this.max)}
            step=${ifDefined(this.step as number)}
            .value=${live(this.value)}
            autocapitalize=${ifDefined(this.autocapitalize)}
            autocomplete=${ifDefined(this.autocomplete as any)}
            autocorrect=${ifDefined(this.autocorrect)}
            ?autofocus=${this.autofocus}
            spellcheck=${this.spellcheck}
            pattern=${ifDefined(this.pattern)}
            enterkeyhint=${ifDefined(this.enterkeyhint)}
            inputmode=${ifDefined(this.inputmode)}
            aria-describedby="help-text"
            @change=${this.handleChange}
            @input=${this.handleInput}
            @invalid=${this.handleInvalid}
            @keydown=${this.handleKeyDown}
            @focus=${this.handleFocus}
            @blur=${this.handleBlur}
          />

          ${
            hasClearIcon
              ? html`
                  <button
                    part="clear-button"
                    class="input__clear"
                    type="button"
                    aria-label="clearEntry"
                    @click=${this.handleClearClick}
                    tabindex="-1"
                  >
                    <slot name="clear-icon">
                      <nile-icon name="var(--nile-icon-error, var(--ng-icon-x-circle))" method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))" library="system"></nile-icon>
                    </slot>
                  </button>
                `
              : ''
          }
          ${
            this.passwordToggle && !this.disabled
              ? html`
                  <button
                    part="password-toggle-button"
                    class="input__password-toggle"
                    type="button"
                    aria-label=${this.passwordVisible
                      ? 'hidePassword'
                      : 'showPassword'}
                    @click=${this.handlePasswordToggle}
                    tabindex="-1"
                  >
                    ${this.passwordVisible
                      ? html`
                          <slot name="show-password-icon">
                            <nile-icon
                              name="var(--nile-icon-eye, var(--ng-icon-eye))"
                              method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                              library="system"
                            ></nile-icon>
                          </slot>
                        `
                      : html`
                          <slot name="hide-password-icon">
                            <nile-icon
                              name="var(--nile-icon-eye, var(--ng-icon-eye))"
                              method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                              library="system"
                            ></nile-icon>
                          </slot>
                        `}
                  </button>
                `
              : ''
          }

          <slot name="suffix" part="suffix" class="input__suffix"></slot>
        </div>

        <div class="input__non-printable">
          Non-printable character detected.
          <nile-badge variant="error" @click=${
          this.removeAllNonPrintableCharacters
          }class="input__remove-non-printable">
            Remove All
          </nile-badge>

          <div class="input__srtiked-text-container">${unsafeHTML(
          this.markedValue
          )} </div>
        </div>
      </nile-popup>
    `
  : html`
      <div
        part="base"
        class=${classMap({
          input: true,

          // Sizes
          'input--small': this.size === 'small',
          'input--medium': this.size === 'medium',
          'input--large': this.size === 'large',

          // States
          'input--pill': this.pill,
          'input--standard': !this.filled,
          'input--warning': this.warning,
          'input--error': this.error,
          'input--success': this.success,
          'input--destructive': this.destructive,
          'input--filled': this.filled,
          'input--disabled': this.disabled,
          'input--focused': this.hasFocus,
          'input--empty': !this.value,
          'input--no-spin-buttons': this.noSpinButtons,
        })}
      >
        <slot name="prefix" part="prefix" class="input__prefix"></slot>
        <input
          part="input"
          id="input"
          class="input__control ${
            this.type === 'password' && !this.passwordVisible
              ? 'input__password'
              : ''
          }"
          type=${this.type === 'password'
            ? this.passwordVisible
              ? 'text'
              : 'password'
            : this.type}
          title=${this.title}
          name=${ifDefined(this.name)}
          ?disabled=${this.disabled}
          tabindex=${this.disabled ? '-1' : '0'}
          ?readonly=${this.readonly}
          ?required=${this.required}
          placeholder=${ifDefined(this.placeholder)}
          minlength=${ifDefined(this.minlength)}
          maxlength=${ifDefined(this.maxlength)}
          min=${ifDefined(this.min)}
          max=${ifDefined(this.max)}
          step=${ifDefined(this.step as number)}
          .value=${live(this.value)}
          autocapitalize=${ifDefined(this.autocapitalize)}
          autocomplete=${ifDefined(this.autocomplete as any)}
          autocorrect=${ifDefined(this.autocorrect)}
          ?autofocus=${this.autofocus}
          spellcheck=${this.spellcheck}
          pattern=${ifDefined(this.pattern)}
          enterkeyhint=${ifDefined(this.enterkeyhint)}
          inputmode=${ifDefined(this.inputmode)}
          aria-describedby="help-text"
          @change=${this.handleChange}
          @input=${this.handleInput}
          @invalid=${this.handleInvalid}
          @keydown=${this.handleKeyDown}
          @focus=${this.handleFocus}
          @blur=${this.handleBlur}
        />

        ${
          hasClearIcon
            ? html`
                <button
                  part="clear-button"
                  class="input__clear"
                  type="button"
                  aria-label="clearEntry"
                  @click=${this.handleClearClick}
                  tabindex="-1"
                >
                  <slot name="clear-icon">
                    <nile-icon
                      name="var(--nile-icon-error, var(--ng-icon-x-circle))"
                      method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                      library="system"
                    ></nile-icon>
                  </slot>
                </button>
              `
            : ''
        }

        ${
          this.passwordToggle && !this.disabled
            ? html`
                <button
                  part="password-toggle-button"
                  class="input__password-toggle"
                  type="button"
                  aria-label=${this.passwordVisible
                    ? 'hidePassword'
                    : 'showPassword'}
                  @click=${this.handlePasswordToggle}
                  tabindex="-1"
                >
                  ${this.passwordVisible
                    ? html`
                        <slot name="show-password-icon">
                          <nile-icon
                            name="var(--nile-icon-eye, var(--ng-icon-eye))"
                            method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                            library="system"
                          ></nile-icon>
                        </slot>
                      `
                    : html`
                        <slot name="hide-password-icon">
                          <nile-icon
                            name="var(--nile-icon-eye, var(--ng-icon-eye))"
                            method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                            library="system"
                          ></nile-icon>
                        </slot>
                      `}
                </button>
              `
            : ''
        }

        <slot name="suffix" part="suffix" class="input__suffix"></slot>
      </div>
    `}

        </div>

        ${
          hasHelpText
            ? html`
                <nile-form-help-text exportparts="container:help__text">${this.helpText}</nile-form-help-text>
              `
            : ``
        }

        ${
          hasErrorMessage
            ? html`
                <nile-form-error-message
                  >${this.errorMessage}</nile-form-error-message
                >
              `
            : ``
        }
        </div>
      </div>
    `;
  }
}

export default NileInput;

declare global {
  interface HTMLElementTagNameMap {
    'nile-input': NileInput;
  }
}
