/*
 * If not stated otherwise in this file or this component's LICENSE file the
 * following copyright and licenses apply:
 *
 * Copyright 2023 Comcast Cable Communications Management, LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { UpdateType, type CoreNode } from './CoreNode.js';
import type { Coord } from './lib/utils.js';

export enum AutosizeMode {
  Children = 0,
  Texture = 1,
}

export enum AutosizeUpdateType {
  None = 0,
  Filtered = 1,
  All = 2,
}

const applyDimensions = (node: CoreNode, w: number, h: number) => {
  node.props.w = w;
  node.props.h = h;
  node.setUpdateType(UpdateType.Local);
};

const getFilteredChildren = (
  children: number[],
  childMap: Map<number, CoreNode>,
) => {
  const filtered: CoreNode[] = [];
  while (children.length > 0) {
    const id = children.pop()!;
    const child = childMap.get(id)!;
    filtered.push(child);
  }
  return filtered;
};

let autosizerId = 0;

export class Autosizer {
  public id = autosizerId++;
  mode: AutosizeMode = AutosizeMode.Children;
  updateType: AutosizeUpdateType = AutosizeUpdateType.All;
  lastWidth: number = 0;
  lastHeight: number = 0;
  lastHasChanged: boolean = false;

  flaggedChildren: number[] = [];
  childMap: Map<number, CoreNode> = new Map();

  minX = Infinity;
  minY = Infinity;
  maxX = -Infinity;
  maxY = -Infinity;

  corners: [Coord, Coord, Coord, Coord] = [
    { x: 0, y: 0 },
    { x: 0, y: 0 },
    { x: 0, y: 0 },
    { x: 0, y: 0 },
  ];

  constructor(public node: CoreNode) {
    if (node.texture !== null) {
      this.mode = AutosizeMode.Texture;
    }
  }

  attach(node: CoreNode) {
    this.childMap.set(node.id, node);
    node.parentAutosizer = this;

    //bubble down to attach to grandchildren
    if (node.children.length > 0 && node.autosizer === null) {
      const children = node.children;
      for (let i = 0; i < children.length; i++) {
        this.attach(children[i]!);
      }
    }
  }

  detach(node: CoreNode) {
    if (this.childMap.delete(node.id) === true) {
      node.parentAutosizer = null;
      if (node.children.length > 0 && node.autosizer === null) {
        const children = node.children;
        for (let i = 0; i < children.length; i++) {
          this.detach(children[i]!);
        }
      }
      //detached a child, need full update
      this.setUpdateType(AutosizeUpdateType.All);
    }
  }

  patch(id: number) {
    const entry = this.childMap.get(id);
    if (entry === undefined) {
      return;
    }
    this.flaggedChildren.push(id);
    this.setUpdateType(AutosizeUpdateType.Filtered);
  }

  setUpdateType(updateType: AutosizeUpdateType) {
    this.updateType |= updateType;
    this.node.setUpdateType(UpdateType.Autosize);
  }

  setMode(mode: AutosizeMode) {
    this.mode = mode;
    this.setUpdateType(AutosizeUpdateType.All);
  }

  update() {
    const node = this.node;

    if (
      this.mode === AutosizeMode.Texture &&
      node.texture !== null &&
      node.texture.dimensions !== null
    ) {
      const { w, h } = node.texture.dimensions;
      if (w !== node.w || h !== node.h) {
        applyDimensions(node, w, h);
      }
      this.lastWidth = w;
      this.lastHeight = h;
      this.updateType = AutosizeUpdateType.None;
      return;
    }

    let filtered: CoreNode[] =
      this.updateType === AutosizeUpdateType.Filtered
        ? getFilteredChildren(this.flaggedChildren, this.childMap)
        : Array.from(this.childMap.values());

    if (filtered.length === 0) {
      return;
    }

    const corners = this.corners;
    let minX = this.minX;
    let minY = this.minY;
    let maxX = this.maxX;
    let maxY = this.maxY;

    for (let i = 0; i < filtered.length; i++) {
      const child = filtered[i]!;
      if (child.isRenderable === false || child.localTransform === undefined) {
        continue;
      }

      const { tx, ty, ta, tb, tc, td } = child.localTransform;
      const w = child.props.w;
      const h = child.props.h;

      const childMinX = tx;
      const childMaxX = tx + w * ta;
      const childMinY = ty;
      const childMaxY = ty + h * td;

      corners[0].x = childMinX;
      corners[0].y = childMinY;
      corners[1].x = childMaxX;

      //no rotation/scale
      if (tb === 0 && tc === 0) {
        corners[1].y = childMinY;
        corners[2].x = childMaxX;
        corners[2].y = childMaxY;
        corners[3].x = childMinX;
        corners[3].y = childMaxY;
      } else {
        corners[1].y = tx + w * tc;
        corners[2].x = tx + w * ta + h * tb;
        corners[2].y = ty + w * tc + h * td;
        corners[3].x = tx + h * tb;
        corners[3].y = ty + h * td;
      }

      for (let j = 0; j < 4; j++) {
        const corner = corners[j]!;
        if (corner.x < minX) minX = corner.x;
        if (corner.y < minY) minY = corner.y;
        if (corner.x > maxX) maxX = corner.x;
        if (corner.y > maxY) maxY = corner.y;
      }
    }
    this.updateType = AutosizeUpdateType.None;

    const newWidth = maxX > 0 ? maxX : 0;
    const newHeight = maxY > 0 ? maxY : 0;

    applyDimensions(node, newWidth, newHeight);
    this.lastWidth = newWidth;
    this.lastHeight = newHeight;
  }

  destroy() {
    if (this.childMap.size > 0) {
      for (const child of this.childMap.values()) {
        child.parentAutosizer = null;
      }
    }
    this.childMap.clear();
    this.flaggedChildren.length = 0;
  }
}
