import { globalThis } from './utils/server-safe-globals.js';
import {
  MediaUIAttributes,
  MediaStateReceiverAttributes,
} from './constants.js';
import {
  getOrInsertCSSRule,
  getStringAttr,
  namedNodeMapToObject,
  setStringAttr,
} from './utils/element-utils.js';
import MediaController from './media-controller.js';

function getTemplateHTML(_attrs: Record<string, string>) {
  return /*html*/ `
    <style>
      :host {
        box-sizing: border-box;
        display: var(--media-control-display, var(--media-preview-thumbnail-display, inline-block));
        overflow: hidden;
      }

      img {
        display: none;
        position: relative;
      }
    </style>
    <img crossorigin loading="eager" decoding="async">
  `;
}

/**
 *
 * @attr {string} mediacontroller - The element `id` of the media controller to connect to (if not nested within).
 * @attr {string} mediapreviewimage - (read-only) Set to the timeline preview image URL.
 * @attr {string} mediapreviewcoords - (read-only) Set to the active preview image coordinates.
 *
 * @cssproperty [--media-preview-thumbnail-display = inline-block] - `display` property of display.
 * @cssproperty [--media-control-display = inline-block] - `display` property of control.
 * @cssproperty [--media-preview-thumbnail-object-fit = contain] - Controls how the thumbnail scales within its container. `contain` (default) maintains aspect ratio, `fill` allows independent width/height scaling.
 */
class MediaPreviewThumbnail extends globalThis.HTMLElement {
  static shadowRootOptions = { mode: 'open' as ShadowRootMode };
  static getTemplateHTML = getTemplateHTML;

  #mediaController: MediaController;

  static get observedAttributes() {
    return [
      MediaStateReceiverAttributes.MEDIA_CONTROLLER,
      MediaUIAttributes.MEDIA_PREVIEW_IMAGE,
      MediaUIAttributes.MEDIA_PREVIEW_COORDS,
    ];
  }

  imgWidth: number;
  imgHeight: number;

  constructor() {
    super();

    if (!this.shadowRoot) {
      // Set up the Shadow DOM if not using Declarative Shadow DOM.
      this.attachShadow((this.constructor as typeof MediaPreviewThumbnail).shadowRootOptions);

      const attrs = namedNodeMapToObject(this.attributes);
      this.shadowRoot.innerHTML = (this.constructor as typeof MediaPreviewThumbnail).getTemplateHTML(attrs);
    }
  }

  connectedCallback(): void {
    const mediaControllerId = this.getAttribute(
      MediaStateReceiverAttributes.MEDIA_CONTROLLER
    );
    if (mediaControllerId) {
      this.#mediaController =
        // @ts-ignore
        this.getRootNode()?.getElementById(mediaControllerId);
      this.#mediaController?.associateElement?.(this);
    }
  }

  disconnectedCallback(): void {
    // Use cached mediaController, getRootNode() doesn't work if disconnected.
    this.#mediaController?.unassociateElement?.(this);
    this.#mediaController = null;
  }

  attributeChangedCallback(
    attrName: string,
    oldValue: string | null,
    newValue: string | null
  ): void {
    if (
      [
        MediaUIAttributes.MEDIA_PREVIEW_IMAGE,
        MediaUIAttributes.MEDIA_PREVIEW_COORDS,
      ].includes(attrName as any)
    ) {
      this.update();
    }
    if (attrName === MediaStateReceiverAttributes.MEDIA_CONTROLLER) {
      if (oldValue) {
        this.#mediaController?.unassociateElement?.(this);
        this.#mediaController = null;
      }
      if (newValue && this.isConnected) {
        // @ts-ignore
        this.#mediaController = this.getRootNode()?.getElementById(newValue);
        this.#mediaController?.associateElement?.(this);
      }
    }
  }

  /**
   * @type {string | undefined} The url of the preview image
   */
  get mediaPreviewImage() {
    return getStringAttr(this, MediaUIAttributes.MEDIA_PREVIEW_IMAGE);
  }

  set mediaPreviewImage(value) {
    setStringAttr(this, MediaUIAttributes.MEDIA_PREVIEW_IMAGE, value);
  }

  /**
   * @type {Array<number> | undefined} Fixed length array [x, y, width, height] or undefined
   */
  get mediaPreviewCoords() {
    const attrVal = this.getAttribute(MediaUIAttributes.MEDIA_PREVIEW_COORDS);

    if (!attrVal) return undefined;

    return attrVal.split(/\s+/).map((coord) => +coord);
  }

  set mediaPreviewCoords(value) {
    if (!value) {
      this.removeAttribute(MediaUIAttributes.MEDIA_PREVIEW_COORDS);
      return;
    }

    this.setAttribute(MediaUIAttributes.MEDIA_PREVIEW_COORDS, value.join(' '));
  }

  update(): void {
    const coords = this.mediaPreviewCoords;
    const previewImage = this.mediaPreviewImage;

    if (!(coords && previewImage)) return;

    const [x, y, w, h] = coords;
    const src = previewImage.split('#')[0];

    const computedStyle = getComputedStyle(this);
    const { maxWidth, maxHeight, minWidth, minHeight } = computedStyle;

    // Check if user wants independent width/height scaling (fill mode)
    // Default is 'contain' which preserves aspect ratio
    const objectFit = computedStyle.getPropertyValue('--media-preview-thumbnail-object-fit').trim() || 'contain';

    let scaleX: number;
    let scaleY: number;

    if (objectFit === 'fill') {
      const maxRatioX = parseInt(maxWidth) / w;
      const maxRatioY = parseInt(maxHeight) / h;
      const minRatioX = parseInt(minWidth) / w;
      const minRatioY = parseInt(minHeight) / h;

      scaleX = maxRatioX < 1 ? maxRatioX : Math.max(maxRatioX, minRatioX);
      scaleY = maxRatioY < 1 ? maxRatioY : Math.max(maxRatioY, minRatioY);
    } else {
      const maxRatio = Math.min(parseInt(maxWidth) / w, parseInt(maxHeight) / h);
      const minRatio = Math.max(parseInt(minWidth) / w, parseInt(minHeight) / h);

      const isScalingDown = maxRatio < 1;
      const scale = isScalingDown ? maxRatio : minRatio > 1 ? minRatio : 1;

      scaleX = scale;
      scaleY = scale;
    }

    const { style } = getOrInsertCSSRule(this.shadowRoot, ':host');
    const imgStyle = getOrInsertCSSRule(this.shadowRoot, 'img').style;
    const img = this.shadowRoot.querySelector('img');

    // Revert one set of extremum to its initial value on a known scale direction.
    const isScalingDown = Math.min(scaleX, scaleY) < 1;
    const extremum = isScalingDown ? 'min' : 'max';
    style.setProperty(`${extremum}-width`, 'initial', 'important');
    style.setProperty(`${extremum}-height`, 'initial', 'important');
    style.width = `${w * scaleX}px`;
    style.height = `${h * scaleY}px`;

    const resize = () => {
      imgStyle.width = `${this.imgWidth * scaleX}px`;
      imgStyle.height = `${this.imgHeight * scaleY}px`;
      imgStyle.display = 'block';
    };

    if (img.src !== src) {
      img.onload = () => {
        this.imgWidth = img.naturalWidth;
        this.imgHeight = img.naturalHeight;
        resize();

        img.onload = null;
      };
      img.src = src;
      resize();
    }

    resize();
    imgStyle.transform = `translate(-${x * scaleX}px, -${y * scaleY}px)`;
  }
}

if (!globalThis.customElements.get('media-preview-thumbnail')) {
  globalThis.customElements.define(
    'media-preview-thumbnail',
    MediaPreviewThumbnail
  );
}

export default MediaPreviewThumbnail;
