/*
 * Copyright (c) 2010, 2025 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {aria, Device, events, graphics, HtmlComponent, InitModelOf, KeyStrokeContext, SliderEventMap, SliderLayout, SliderModel, SliderNavigationKeyStroke, SliderShiftNavigationKeyStroke, Widget} from '../index';
import $ from 'jquery';

export class Slider extends Widget implements SliderModel {
  declare model: SliderModel;
  declare eventMap: SliderEventMap;
  declare self: Slider;

  protected static FLOATING_POINT_ERROR_CORRECTION = 100000000;

  value: number;
  minValue: number;
  maxValue: number;
  step: number;

  protected _mouseMoveHandler = this._onMouseMove.bind(this);
  protected _mouseUpHandler = this._onMouseUp.bind(this);

  $window: JQuery<Window>;
  $track: JQuery<HTMLDivElement>;
  $thumb: JQuery<HTMLDivElement>;

  constructor() {
    super();

    this.value = null;
    this.minValue = null;
    this.maxValue = null;
    this.step = null;
    this.tabbable = true;

    this.$track = null;
    this.$thumb = null;
  }

  protected override _init(options: InitModelOf<this>) {
    super._init(options);
    this.value = options.value;
    this.minValue = options.minValue;
    this.maxValue = options.maxValue;
    this.step = options.step;
  }

  protected override _createKeyStrokeContext(): KeyStrokeContext {
    return new KeyStrokeContext();
  }

  protected override _initKeyStrokeContext() {
    super._initKeyStrokeContext();
    this.registerKeyStrokes([
      new SliderNavigationKeyStroke(this),
      new SliderShiftNavigationKeyStroke(this)
    ]);
  }

  protected override _render() {
    this.$container = this.$parent.appendDiv('slider')
      .on('blur', this._onBlur.bind(this))
      .on('focus', this._onFocus.bind(this));
    this.htmlComp = HtmlComponent.install(this.$container, this.session);
    this.htmlComp.setLayout(new SliderLayout(this));

    this.$track = this.$container.appendDiv('slider-track');
    this.$thumb = this.$container.appendDiv('slider-thumb');

    this.$window = this.$container.window();
    this.$container.on('mousedown touchstart', this._onMouseDown.bind(this));

    aria.role(this.$container, 'slider');
  }

  protected override _renderProperties() {
    super._renderProperties();
    this._renderValue();
    this._renderMinValue();
    this._renderMaxValue();
    this._renderStep();
  }

  protected override _remove() {
    this.$track = null;
    this.$thumb = null;
    this.$window
      .off('mousemove touchmove', this._mouseMoveHandler)
      .off('mouseup touchend touchcancel', this._mouseUpHandler);
    super._remove();
  }

  protected _onFocus(event: JQuery.FocusEvent) {
    this.setFocused(true);
  }

  protected _onBlur(event: JQuery.BlurEvent) {
    this.setFocused(false);
  }

  setValue(value: number) {
    this.setProperty('value', value);
  }

  protected _renderValue() {
    this._setThumbPosition(this._valueToLocalPosition(this._limitValue(this.value)));
    this.$container.attr('aria-valuenow', this.value);
  }

  setMinValue(minValue: number) {
    this.setProperty('minValue', minValue);
  }

  protected _renderMinValue() {
    this._update();
    this.$container.attr('aria-valuemin', this.minValue);
  }

  setMaxValue(maxValue: number) {
    this.setProperty('maxValue', maxValue);
  }

  protected _renderMaxValue() {
    this._update();
    this.$container.attr('aria-valuemax', this.maxValue);
  }

  setStep(step: number) {
    this.setProperty('step', step);
  }

  protected _renderStep() {
    this._update();
  }

  protected _onMouseDown(event: JQuery.MouseDownEvent | JQuery.TouchStartEvent) {
    if (!this.enabledComputed) {
      return null;
    }

    events.fixTouchEvent(event);

    this.$window
      .off('mousemove touchmove', this._mouseMoveHandler)
      .off('mouseup touchend touchcancel', this._mouseUpHandler)
      .on('mousemove touchmove', this._mouseMoveHandler)
      .on('mouseup touchend touchcancel', this._mouseUpHandler);
    $('iframe').addClass('dragging-in-progress');

    this._moveThumbTo(event.pageX);
  }

  protected _onMouseMove(event: JQuery.MouseMoveEvent | JQuery.TouchMoveEvent) {
    events.fixTouchEvent(event);
    this._moveThumbTo(event.pageX);
  }

  protected _onMouseUp(event: JQuery.MouseUpEvent | JQuery.TouchEndEvent | JQuery.TouchCancelEvent) {
    this.$window
      .off('mousemove touchmove', this._mouseMoveHandler)
      .off('mouseup touchend touchcancel', this._mouseUpHandler);
    $('iframe').removeClass('dragging-in-progress');
  }

  protected _moveThumbTo(pageX: number) {
    const value = this._normalizeValue(this._localPositionToValue(pageX - this.$container.offset().left));
    this._setThumbPosition(this._valueToLocalPosition(value));
    this.setValue(value);
  }

  protected _setThumbPosition(position: number) {
    const borderLeft = graphics.borders(this.$container).left;
    this.$thumb.cssLeft(position - borderLeft);
    this.$track.cssWidth(position - borderLeft);
  }

  move(moveBy: number) {
    this.setValue(this._normalizeValue(this.value + moveBy));
  }

  /** @internal called by SliderLayout */
  _update() {
    if (this.rendered) {
      this._setThumbPosition(this._valueToLocalPosition(this.value));
    }
  }

  protected _valueToLocalPosition(value: number) {
    if (this.maxValue === this.minValue) {
      return this.$container.cssWidth() / 2;
    }

    // Offset thumb on either side by half its width, so it does not stick out of the slider area
    const offset = this.$thumb.cssWidth() / 2;
    const minPosition = offset;
    const maxPosition = this.$container.cssWidth() - offset;

    let position = (value - this.minValue) / (this.maxValue - this.minValue) * (this.$container.cssWidth() - this.$thumb.cssWidth()) + offset;
    return Math.round(Math.max(minPosition, Math.min(maxPosition, position)));
  }

  protected _localPositionToValue(position: number) {
    const offset = this.$thumb.cssWidth() / 2;
    const minPosition = offset;
    const maxPosition = this.$container.cssWidth() - offset;

    if (position <= minPosition) {
      return this.minValue;
    }
    if (position >= maxPosition) {
      return this.maxValue;
    }
    return (position - offset) / (this.$container.cssWidth() - this.$thumb.cssWidth()) * (this.maxValue - this.minValue) + this.minValue;
  }

  protected _normalizeValue(value: number) {
    return this._limitValue(this._calculateSteppedValue(value));
  }

  protected _limitValue(value: number) {
    return Math.min(Math.max(value, this.minValue), this.maxValue);
  }

  protected _calculateSteppedValue(value: number) {
    if (!this.step) { // 0 or not set
      return value;
    }

    const range = this.maxValue - this.minValue;
    const fullStepCount = Math.floor(range / this.step);
    const lastStepStart = this.minValue + fullStepCount * this.step;
    const lastStepSize = this.maxValue - lastStepStart;

    let steppedValue: number;

    if (lastStepSize > 0 && value > lastStepStart) {
      // Value is in the range of the last (incomplete) step
      steppedValue = (value - lastStepStart >= lastStepSize / 2) ? this.maxValue : lastStepStart;
    } else {
      // Value is within the full step range
      const stepsFromMin = Math.round((value - this.minValue) / this.step);
      steppedValue = this.minValue + stepsFromMin * this.step;
    }

    return Math.round(steppedValue * Slider.FLOATING_POINT_ERROR_CORRECTION) / Slider.FLOATING_POINT_ERROR_CORRECTION;
  }
}

