import { removeItems } from "./utils";
import { DisplayObject } from "./displayObject";
import { Transform } from "./transform";

function sortChildren(a: any, b: any) {
  if (a.zIndex === b.zIndex) {
    return a._lastSortedIndex - b._lastSortedIndex;
  }

  return a.zIndex - b.zIndex;
}

const IDENTITY = Transform.IDENTITY;
export class Container extends DisplayObject {
  children: DisplayObject[] = [];
  sortableChildren = true;
  sortDirty = false;

  constructor() {
    super();

    this.sortableChildren = true;

    this.sortDirty = false;
  }

  onChildrenChange() {
    /* empty */
  }

  //保持child的坐标位置不变
  addChildWorldNoChange(child: DisplayObject) {
    const m = this.worldTransform.clone();
    m.invert();
    const wm = child.worldTransform.clone();
    wm.prepend(m);
    // m.append( child.worldTransform );

    child.transform.setFromMatrix(wm);

    this.addChild(child);

    child.updateTransform();
  }

  removeChildWorldNoChange(child: DisplayObject) {
    const m = child.worldTransform.clone();
    child.transform.setFromMatrix(m);

    this.removeChild(child);

    child.updateTransform();
  }

  addChild(child: DisplayObject) {
    // if the child has a parent then lets remove it as PixiJS objects can only exist in one place
    if (child.parent) {
      child.parent.removeChild(child);
    }

    child.parent = this;
    this.sortDirty = true;

    // ensure child transform will be recalculated
    child.transform._parentID = -1;

    this.children.push(child);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange();
    //   this.emit("childAdded", child, this, this.children.length - 1);
    //   child.emit("added", this);

    return child;
  }

  addChildAt(child: DisplayObject, index: number) {
    if (index < 0 || index > this.children.length) {
      throw new Error(
        `${child}addChildAt: The index ${index} supplied is out of bounds ${this.children.length}`
      );
    }

    if (child.parent) {
      child.parent.removeChild(child);
    }

    child.parent = this;
    this.sortDirty = true;

    // ensure child transform will be recalculated
    child.transform._parentID = -1;

    this.children.splice(index, 0, child);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange();
    // child.emit("added", this);
    // this.emit("childAdded", child, this, index);

    return child;
  }

  /**
   * Swaps the position of 2 Display Objects within this container.
   *
   */
  swapChildren(child: any, child2: any) {
    if (child === child2) {
      return;
    }

    const index1 = this.getChildIndex(child);
    const index2 = this.getChildIndex(child2);

    this.children[index1] = child2;
    this.children[index2] = child;
    this.onChildrenChange();
  }

  /**
   * Returns the index position of a child DisplayObject instance
   */
  getChildIndex(child: DisplayObject) {
    const index = this.children.indexOf(child);

    if (index === -1) {
      throw new Error(
        "The supplied DisplayObject must be a child of the caller"
      );
    }

    return index;
  }

  /**
   * Changes the position of an existing child in the display object container
   */
  setChildIndex(child: DisplayObject, index: number) {
    if (index < 0 || index >= this.children.length) {
      throw new Error(
        `The index ${index} supplied is out of bounds ${this.children.length}`
      );
    }

    const currentIndex = this.getChildIndex(child);

    removeItems(this.children, currentIndex, 1); // remove from old position
    this.children.splice(index, 0, child); // add at new position

    this.onChildrenChange();
  }

  /**
   * Returns the child at the specified index
   */
  getChildAt(index: number) {
    if (index < 0 || index >= this.children.length) {
      throw new Error(`getChildAt: Index (${index}) does not exist.`);
    }

    return this.children[index];
  }

  /**
   * Removes one or more children from the container.
   *
   */
  removeChild(child: DisplayObject) {
    const index = this.children.indexOf(child);

    if (index === -1) return null;

    child.parent = null;
    // ensure child transform will be recalculated
    child.transform._parentID = -1;
    removeItems(this.children, index, 1);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange();
    //   child.emit("removed", this);
    //   this.emit("childRemoved", child, this, index);

    return child;
  }

  /**
   * Removes a child from the specified index position.
   */
  removeChildAt(index: number) {
    const child = this.getChildAt(index);

    // ensure child transform will be recalculated..
    child.parent = null;
    child.transform._parentID = -1;
    removeItems(this.children, index, 1);

    // ensure bounds will be recalculated
    this._boundsID++;

    // TODO - lets either do all callbacks or all events.. not both!
    this.onChildrenChange();
    // child.emit("removed", this);
    // this.emit("childRemoved", child, this, index);

    return child;
  }

  /**
   * Removes all children from this container that are within the begin and end indexes.
   *
   */
  removeChildren(beginIndex = 0, endIndex: number) {
    const begin = beginIndex;
    const end = typeof endIndex === "number" ? endIndex : this.children.length;
    const range = end - begin;
    let removed;

    if (range > 0 && range <= end) {
      removed = this.children.splice(begin, range);

      for (let i = 0; i < removed.length; ++i) {
        removed[i].parent = null;
        if (removed[i].transform) {
          removed[i].transform._parentID = -1;
        }
      }

      this._boundsID++;

      this.onChildrenChange();

      for (let i = 0; i < removed.length; ++i) {
        // removed[i].emit("removed", this);
        // this.emit("childRemoved", removed[i], this, i);
      }

      return removed;
    } else if (range === 0 && this.children.length === 0) {
      return [];
    }

    throw new RangeError(
      "removeChildren: numeric values are outside the acceptable range."
    );
  }

  /**
   * Sorts children by zIndex. Previous order is mantained for 2 children with the same zIndex.
   */
  sortChildren() {
    let sortRequired = false;

    for (let i = 0, j = this.children.length; i < j; ++i) {
      const child = this.children[i];

      child._lastSortedIndex = i;

      if (!sortRequired && child.zIndex !== 0) {
        sortRequired = true;
      }
    }

    if (sortRequired && this.children.length > 1) {
      this.children.sort(sortChildren);
    }

    this.sortDirty = false;
  }

  /**
   * Updates the transform on all children of this container for rendering
   */
  updateTransform() {
    if (this.sortableChildren && this.sortDirty) {
      this.sortChildren();
    }

    this._boundsID++;

    this.transform.updateTransform(
      this.parent ? this.parent.transform : IDENTITY
    );

    // TODO: check render flags, how to process stuff here
    this.worldAlpha = this.alpha * (this.parent ? this.parent.worldAlpha : 1);

    for (let i = 0, j = this.children.length; i < j; ++i) {
      const child = this.children[i];

      if (child.visible) {
        child.updateTransform();
      }
    }
  }

  /**
   * Recalculates the bounds of the container.
   *
   */
  calculateBounds() {
    this._bounds.clear();

    this._calculateBounds();

    for (let i = 0; i < this.children.length; i++) {
      const child = this.children[i];

      if (!child.visible) {
        continue;
      }

      //child.calculateBounds();

      // TODO: filter+mask, need to mask both somehow
      //   if (child._mask) {
      //     child._mask.calculateBounds();
      //     this._bounds.addBoundsMask(child._bounds, child._mask._bounds);
      //   } else if (child.filterArea) {
      //     this._bounds.addBoundsArea(child._bounds, child.filterArea);
      //   } else {
      this._bounds.addBounds(child._bounds);
      //   }
    }

    this._lastBoundsID = this._boundsID;
  }

  /**
   * Recalculates the bounds of the object. Override this to
   * calculate the bounds of the specific object (not including children).
   */
  _calculateBounds() {
    // FILL IN//
  }

  /**
   * The width of the Container, setting this will actually modify the scale to achieve the value set
   *
   * @member {number}
   */
  get width() {
    return this.scale.x * this.getLocalBounds().width;
  }
  _width = 0;

  set width(value) {
    const width = this.getLocalBounds().width;

    if (width !== 0) {
      this.scale.x = value / width;
    } else {
      this.scale.x = 1;
    }
    this._width = value;
  }

  /**
   * The height of the Container, setting this will actually modify the scale to achieve the value set
   * @member {number}
   */
  get height() {
    return this.scale.y * this.getLocalBounds().height;
  }
  _height = 0;

  
  set height(value) {
    const height = this.getLocalBounds().height;

    if (height !== 0) {
      this.scale.y = value / height;
    } else {
      this.scale.y = 1;
    }
    this._height = value;
  }
  containerUpdateTransform = this.updateTransform;
}
