// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-lit-render-outside-of-view */

import './CSSAngleEditor.js';
import './CSSAngleSwatch.js';

import * as Platform from '../../../../core/platform/platform.js';
import * as Lit from '../../../lit/lit.js';

import cssAngleStyles from './cssAngle.css.js';
import {
  type Angle,
  AngleUnit,
  convertAngleUnit,
  getNewAngleFromEvent,
  getNextUnit,
  parseText,
  roundAngleByUnit,
} from './CSSAngleUtils.js';
import {ValueChangedEvent} from './InlineEditorUtils.js';

const {render, html} = Lit;
const styleMap = Lit.Directives.styleMap;

export class PopoverToggledEvent extends Event {
  static readonly eventName = 'popovertoggled';
  data: {open: boolean};

  constructor(open: boolean) {
    super(PopoverToggledEvent.eventName, {});
    this.data = {open};
  }
}

export class UnitChangedEvent extends Event {
  static readonly eventName = 'unitchanged';
  data: {value: string};

  constructor(value: string) {
    super(UnitChangedEvent.eventName, {});
    this.data = {value};
  }
}

interface EventTypes {
  [PopoverToggledEvent.eventName]: PopoverToggledEvent;
  [UnitChangedEvent.eventName]: UnitChangedEvent;
  [ValueChangedEvent.eventName]: ValueChangedEvent;
}

export interface CSSAngleData {
  angleText: string;
  containingPane: HTMLElement;
}

const DefaultAngle = {
  value: 0,
  unit: AngleUnit.RAD,
};

export class CSSAngle extends HTMLElement {
  private angle: Angle = DefaultAngle;
  private displayedAngle: Angle = DefaultAngle;
  private propertyValue = '';
  private containingPane?: HTMLElement;
  private angleElement: HTMLElement|null = null;
  private swatchElement: HTMLElement|null = null;
  private popoverOpen = false;
  private popoverStyleTop = '';
  private popoverStyleLeft = '';
  private onMinifyingAction = this.minify.bind(this);

  set data(data: CSSAngleData) {
    const parsedResult = parseText(data.angleText);
    if (!parsedResult) {
      return;
    }
    this.angle = parsedResult;
    this.displayedAngle = {...parsedResult};
    this.containingPane = data.containingPane;
    this.render();
  }

  disconnectedCallback(): void {
    this.unbindMinifyingAction();
  }

  // We bind and unbind mouse event listeners upon popping over and minifying,
  // because we anticipate most of the time this widget is minified even when
  // it's attached to the DOM tree.
  popOver(): void {
    if (!this.containingPane) {
      return;
    }

    if (!this.angleElement) {
      this.angleElement = this.querySelector<HTMLElement>('.css-angle');
    }
    if (!this.swatchElement) {
      this.swatchElement = this.querySelector('devtools-css-angle-swatch');
    }
    if (!this.angleElement || !this.swatchElement) {
      return;
    }

    this.dispatchEvent(new PopoverToggledEvent(true));
    this.bindMinifyingAction();

    const miniIconBottom = this.swatchElement.getBoundingClientRect().bottom;
    const miniIconLeft = this.swatchElement.getBoundingClientRect().left;
    if (miniIconBottom && miniIconLeft) {
      // We offset mini icon's X and Y positions with the containing styles
      // pane's positions because DevTools' root SplitWidget's
      // sidebar slot, where most of the DevTools content lives,
      // has an offset of positions, which makes all of its children's DOMRect
      // positions to have this offset.
      const offsetTop = this.containingPane.getBoundingClientRect().top;
      const offsetLeft = this.containingPane.getBoundingClientRect().left;
      this.popoverStyleTop = `${miniIconBottom - offsetTop}px`;
      this.popoverStyleLeft = `${miniIconLeft - offsetLeft}px`;
    }

    this.popoverOpen = true;
    this.render();
    this.angleElement.focus();
  }

  override addEventListener<K extends keyof EventTypes>(
      type: K, listener: (this: CSSAngle, ev: EventTypes[K]) => void,
      options?: boolean|AddEventListenerOptions|undefined): void;
  override addEventListener<K extends keyof HTMLElementEventMap>(
      type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => void,
      options?: boolean|AddEventListenerOptions|undefined): void;
  override addEventListener(
      type: string, listener: EventListenerOrEventListenerObject,
      options?: boolean|AddEventListenerOptions|undefined): void {
    super.addEventListener(type, listener, options);
  }

  minify(): void {
    if (this.popoverOpen === false) {
      return;
    }

    this.popoverOpen = false;
    this.dispatchEvent(new PopoverToggledEvent(false));
    this.unbindMinifyingAction();
    this.render();
  }

  updateProperty(value: string): void {
    this.propertyValue = value;
    this.render();
  }

  updateAngle(angle: Angle): void {
    this.displayedAngle = roundAngleByUnit(convertAngleUnit(angle, this.displayedAngle.unit));
    this.angle = this.displayedAngle;
    this.dispatchEvent(new ValueChangedEvent(`${this.angle.value}${this.angle.unit}`));
    this.render();
  }

  private displayNextUnit(): void {
    const nextUnit = getNextUnit(this.displayedAngle.unit);
    this.displayedAngle = roundAngleByUnit(convertAngleUnit(this.angle, nextUnit));
    this.dispatchEvent(new UnitChangedEvent(`${this.displayedAngle.value}${this.displayedAngle.unit}`));
  }

  private bindMinifyingAction(): void {
    document.addEventListener('mousedown', this.onMinifyingAction);
    if (this.containingPane) {
      this.containingPane.addEventListener('scroll', this.onMinifyingAction);
    }
  }

  private unbindMinifyingAction(): void {
    document.removeEventListener('mousedown', this.onMinifyingAction);
    if (this.containingPane) {
      this.containingPane.removeEventListener('scroll', this.onMinifyingAction);
    }
  }

  private onMiniIconClick(event: MouseEvent|KeyboardEvent): void {
    event.stopPropagation();
    if (event.shiftKey && !this.popoverOpen) {
      this.displayNextUnit();
      return;
    }
    this.popoverOpen ? this.minify() : this.popOver();
  }

  // Fix that the previous text will be selected when double-clicking the angle icon
  private consume(event: MouseEvent): void {
    event.stopPropagation();
  }

  private onKeydown(event: KeyboardEvent): void {
    if (!this.popoverOpen) {
      if (Platform.KeyboardUtilities.isEnterOrSpaceKey(event)) {
        this.onMiniIconClick(event);
        event.preventDefault();
      }
      return;
    }

    switch (event.key) {
      case 'Escape':
        event.stopPropagation();
        this.minify();
        this.blur();
        break;
      case 'ArrowUp':
      case 'ArrowDown': {
        const newAngle = getNewAngleFromEvent(this.angle, event);
        if (newAngle) {
          this.updateAngle(newAngle);
        }
        event.preventDefault();
        break;
      }
    }
  }

  private render(): void {
    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    render(html`
      <style>${cssAngleStyles}</style>
      <div class="css-angle" @focusout=${this.minify} @keydown=${this.onKeydown} tabindex="-1">
        <div class="preview">
          <devtools-css-angle-swatch
            @click=${this.onMiniIconClick}
            @mousedown=${this.consume}
            @dblclick=${this.consume}
            .data=${{
              angle: this.angle,
            }}>
          </devtools-css-angle-swatch>
          <slot></slot>
        </div>
        ${this.popoverOpen ? this.renderPopover() : null}
      </div>
    `, this, {
      host: this,
    });
    // clang-format on
  }

  private renderPopover(): Lit.TemplateResult {
    let contextualBackground = '';
    if (this.propertyValue && !this.propertyValue.match(/url\(.*\)/i)) {
      contextualBackground = this.propertyValue;
    }

    // Disabled until https://crbug.com/1079231 is fixed.
    // clang-format off
    return html`
      <devtools-css-angle-editor
        class="popover popover-css-angle"
        style=${styleMap({top: this.popoverStyleTop, left: this.popoverStyleLeft})}
        .data=${{
          angle: this.angle,
          onAngleUpdate: (angle: Angle):void => {
            this.updateAngle(angle);
          },
          background: contextualBackground,
        }}>
      </devtools-css-angle-editor>`;
    // clang-format on
  }
}

customElements.define('devtools-css-angle', CSSAngle);

declare global {
  interface HTMLElementTagNameMap {
    'devtools-css-angle': CSSAngle;
  }
}
