/**
 * @license
 * Copyright (c) 2026 - 2026 Vaadin Ltd.
 * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
 */
import './vaadin-slider-bubble.js';
import { css, html, LitElement, render } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { FocusMixin } from '@vaadin/a11y-base/src/focus-mixin.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js';
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
import { field } from '@vaadin/field-base/src/styles/field-base-styles.js';
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { sliderStyles } from './styles/vaadin-slider-base-styles.js';
import { SliderMixin } from './vaadin-slider-mixin.js';

/**
 * `<vaadin-slider>` is a web component that represents a range slider
 * for selecting numerical values within a defined range.
 *
 * ```html
 * <vaadin-slider min="0" max="100" step="1"></vaadin-slider>
 * ```
 *
 * ### Styling
 *
 * The following shadow DOM parts are available for styling:
 *
 * Part name            | Description
 * ---------------------|-----------------
 * `label`              | The label element
 * `required-indicator` | The required indicator element
 * `helper-text`        | The helper text element
 * `error-message`      | The error message element
 * `track`              | The slider track
 * `track-fill`         | The filled portion of the track
 * `thumb`              | The slider thumb
 * `marks`              | Container for min/max labels
 * `min`                | Minimum value label
 * `max`                | Maximum value label
 *
 * The following state attributes are available for styling:
 *
 * Attribute          | Description
 * -------------------|-------------
 * `active`           | Set when the slider is activated with mouse or touch
 * `disabled`         | Set when the slider is disabled
 * `readonly`         | Set when the slider is read-only
 * `focused`          | Set when the slider has focus
 * `focus-ring`       | Set when the slider is focused using the keyboard
 * `min-max-visible`  | Set when the min/max labels are displayed
 *
 * The following custom CSS properties are available for styling:
 *
 * Custom CSS property                          |
 * :--------------------------------------------|
 * `--vaadin-field-default-width`               |
 * `--vaadin-input-field-error-color`           |
 * `--vaadin-input-field-error-font-size`       |
 * `--vaadin-input-field-error-font-weight`     |
 * `--vaadin-input-field-helper-color`          |
 * `--vaadin-input-field-helper-font-size`      |
 * `--vaadin-input-field-helper-font-weight`    |
 * `--vaadin-input-field-label-color`           |
 * `--vaadin-input-field-label-font-size`       |
 * `--vaadin-input-field-label-font-weight`     |
 * `--vaadin-input-field-required-indicator`    |
 * `--vaadin-slider-bubble-arrow-size`          |
 * `--vaadin-slider-bubble-background`          |
 * `--vaadin-slider-bubble-border-color`        |
 * `--vaadin-slider-bubble-border-radius`       |
 * `--vaadin-slider-bubble-border-width`        |
 * `--vaadin-slider-bubble-offset`              |
 * `--vaadin-slider-bubble-padding`             |
 * `--vaadin-slider-bubble-shadow`              |
 * `--vaadin-slider-bubble-text-color`          |
 * `--vaadin-slider-bubble-font-size`           |
 * `--vaadin-slider-bubble-font-weight`         |
 * `--vaadin-slider-bubble-line-height`         |
 * `--vaadin-slider-fill-background`            |
 * `--vaadin-slider-fill-border-color`          |
 * `--vaadin-slider-fill-border-width`          |
 * `--vaadin-slider-marks-color`                |
 * `--vaadin-slider-marks-font-size`            |
 * `--vaadin-slider-marks-font-weight`          |
 * `--vaadin-slider-thumb-border-color`         |
 * `--vaadin-slider-thumb-border-radius`        |
 * `--vaadin-slider-thumb-border-width`         |
 * `--vaadin-slider-thumb-cursor`               |
 * `--vaadin-slider-thumb-cursor-active`        |
 * `--vaadin-slider-thumb-height`               |
 * `--vaadin-slider-thumb-width`                |
 * `--vaadin-slider-track-background`           |
 * `--vaadin-slider-track-border-color`         |
 * `--vaadin-slider-track-border-radius`        |
 * `--vaadin-slider-track-border-width`         |
 * `--vaadin-slider-track-height`               |
 *
 * In order to style the slider bubble, use `<vaadin-slider-bubble>` shadow DOM parts:
 *
 * Part name        | Description
 * -----------------|----------------------
 * `overlay`        | The overlay container
 * `content`        | The overlay content
 * `arrow`          | Arrow pointing to the thumb
 *
 * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
 *
 * @fires {Event} change - Fired when the user commits a value change.
 * @fires {Event} input - Fired when the slider value changes during user interaction.
 * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
 * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
 * @fires {CustomEvent} validated - Fired whenever the field is validated.
 *
 * @customElement vaadin-slider
 * @extends HTMLElement
 * @mixes ElementMixin
 * @mixes FieldMixin
 * @mixes FocusMixin
 * @mixes SliderMixin
 * @mixes ThemableMixin
 */
class Slider extends FieldMixin(
  SliderMixin(FocusMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement)))))),
) {
  static get is() {
    return 'vaadin-slider';
  }

  static get styles() {
    return [
      field,
      sliderStyles,
      css`
        :host([focus-ring]) [part='thumb'] {
          outline: var(--vaadin-focus-ring-width) var(--_outline-style, solid) var(--vaadin-focus-ring-color);
          outline-offset: 1px;
        }

        #controls {
          grid-template-columns:
            [track-start fill-start]
            calc(var(--value) * var(--_track-width))
            [fill-end thumb1]
            var(--_thumb-width)
            calc((1 - var(--value)) * var(--_track-width))
            [track-end];
        }

        [part='track-fill'] {
          border-start-start-radius: inherit;
          border-end-start-radius: inherit;
        }
      `,
    ];
  }

  static get experimental() {
    return 'sliderComponent';
  }

  static get lumoInjector() {
    return { ...super.lumoInjector, includeBaseStyles: true };
  }

  static get properties() {
    return {
      /**
       * The value of the slider.
       */
      value: {
        type: Number,
        value: 0,
        notify: true,
        sync: true,
      },

      /** @private */
      __active: {
        type: Boolean,
        value: false,
        reflectToAttribute: true,
        attribute: 'active',
        sync: true,
      },

      /** @private */
      __focusInside: {
        type: Boolean,
        value: false,
        sync: true,
      },

      /** @private */
      __hoverInside: {
        type: Boolean,
        value: false,
        sync: true,
      },

      /** @private */
      __bubbleOpened: {
        type: Boolean,
        value: false,
        sync: true,
      },
    };
  }

  /** @protected */
  render() {
    const [value] = this.__value;
    const percent = this.__getPercentFromValue(value);
    const { min, max } = this.__getConstraints();

    return html`
      <div class="vaadin-slider-container">
        <div part="label">
          <slot name="label"></slot>
          <span part="required-indicator" aria-hidden="true" @click="${this.focus}"></span>
        </div>

        <div id="controls" style="${styleMap({ '--value': percent })}">
          <div part="track">
            <div part="track-fill"></div>
          </div>
          <div part="thumb"></div>
          <slot name="input"></slot>
          <slot name="bubble"></slot>
        </div>

        <div part="marks" aria-hidden="true">
          <span part="min">${min}</span>
          <span part="max">${max}</span>
        </div>

        <div part="helper-text">
          <slot name="helper"></slot>
        </div>

        <div part="error-message">
          <slot name="error-message"></slot>
        </div>
      </div>
    `;
  }

  constructor() {
    super();

    this.__value = [this.value];
    this.__inputId = `slider-${generateUniqueId()}`;

    this.__onPointerUp = this.__onPointerUp.bind(this);
  }

  /** @protected */
  ready() {
    super.ready();

    const input = this.querySelector('[slot="input"]');
    this._inputElement = input;
    this.ariaTarget = input;

    this.addController(new LabelledInputController(input, this._labelController));

    this.__thumbElement = this.shadowRoot.querySelector('[part="thumb"]');
    this.__bubbleElement = this.querySelector('vaadin-slider-bubble');
  }

  /** @private */
  __onPointerDown(event) {
    super.__onPointerDown(event);

    if (!this.readonly && event.composedPath()[0] === this._inputElement) {
      this.setAttribute('active', '');
      window.addEventListener('pointerup', this.__onPointerUp);
      window.addEventListener('pointercancel', this.__onPointerUp);
    }
  }

  /** @private */
  __onPointerUp() {
    window.removeEventListener('pointerup', this.__onPointerUp);
    window.removeEventListener('pointercancel', this.__onPointerUp);
    this.removeAttribute('active');
  }

  /**
   * Override update to render slotted `<input type="range" />`
   * into light DOM after rendering shadow DOM.
   * @protected
   */
  update(props) {
    super.update(props);

    const [value] = this.__value;
    const { min, max, step } = this.__getConstraints();

    render(
      html`
        <input
          type="range"
          id="${this.__inputId}"
          slot="input"
          .min="${min}"
          .max="${max}"
          .value="${value}"
          .step="${step}"
          .disabled="${this.disabled}"
          tabindex="${this.disabled ? -1 : 0}"
          @pointerenter="${this.__onPointerEnter}"
          @pointermove="${this.__onPointerMove}"
          @pointerleave="${this.__onPointerLeave}"
          @keydown="${this.__onKeyDown}"
          @input="${this.__onInput}"
          @change="${this.__onChange}"
        />
        <vaadin-slider-bubble
          slot="bubble"
          .positionTarget="${this.__thumbElement}"
          .opened="${this.valueAlwaysVisible || this.__bubbleOpened}"
          theme="${ifDefined(this._theme)}"
        >
          ${value}
        </vaadin-slider-bubble>
      `,
      this,
      { host: this },
    );
  }

  /** @protected */
  willUpdate(props) {
    super.willUpdate(props);

    this.__updateBubbleState(props, {
      active: '__active',
      focused: '__focusInside',
      hover: '__hoverInside',
      opened: '__bubbleOpened',
    });
  }

  /** @protected */
  updated(props) {
    super.updated(props);

    if (props.has('value') || props.has('min') || props.has('max') || props.has('step')) {
      this.__updateValue(this.value);
    }
  }

  /**
   * @param {FocusOptions=} options
   * @protected
   * @override
   */
  focus(options) {
    if (this.disabled) {
      return;
    }

    if (this._inputElement) {
      this._inputElement.focus();
    }

    super.focus(options);
  }

  /**
   * @protected
   * @override
   */
  blur() {
    if (this._inputElement) {
      this._inputElement.blur();
    }
  }

  /**
   * @private
   * @override
   */
  __commitValue() {
    this.value = this.__value[0];
  }

  /** @private */
  __onInput(event) {
    event.stopPropagation();
    this.__updateValue(event.target.value, 0);
    this.__updateBubble();
    this.__dispatchInputEvent();
    this.__commitValue();
  }

  /** @private */
  __onKeyDown(event) {
    const inputKeys = ['ArrowLeft', 'ArrowDown', 'ArrowRight', 'ArrowUp', 'PageUp', 'PageDown', 'Home', 'End'];
    if (this.readonly && inputKeys.includes(event.key)) {
      event.preventDefault();
    }
  }

  /**
   * Override method inherited from `FocusMixin` to update bubble state.
   *
   * @param {boolean} focused
   * @protected
   * @override
   */
  _setFocused(focused) {
    super._setFocused(focused);

    this.__focusInside = focused;
  }

  /** @private */
  __isThumbEvent(event) {
    const rect = this.__thumbElement.getBoundingClientRect();
    return (
      event.clientX >= rect.left &&
      event.clientX <= rect.right &&
      event.clientY >= rect.top &&
      event.clientY <= rect.bottom
    );
  }

  /** @private */
  __onPointerEnter(event) {
    if (this.__isThumbEvent(event)) {
      this.__hoverInside = true;
    }
  }

  /** @private */
  __onPointerMove(event) {
    this.__hoverInside = this.__isThumbEvent(event);
  }

  /** @private */
  __onPointerLeave() {
    this.__hoverInside = false;
  }

  /** @private */
  __updateBubble() {
    this.__bubbleElement.$.overlay._updatePosition();
  }
}

defineCustomElement(Slider);

export { Slider };
