namespace Nori {

    class Panel : Element {

        field _children = [];
        field _childrenIds = [];

        field _canFlushIncrementally = true;
        field _childrenIdsAdded = [];
        field _childRemovalCount = 0;
        field _singleChild;

        constructor(type, singleChildPanel) : base(type) {
            this._isPanel = true;
            this._singleChild = singleChildPanel;
        }

        function setChild(e) {
            if (!this._singleChild) throw new Exception("setChild is not valid for this type of panel. Use addChild");
            if (this._children.length == 1) {
                this.removeChild(this._children[0]);
            }
            return this.addChild(e);
        }

        function addChild(e) {
            if (e._parent != null) {
                throw new Exception("Element already added to a parent.");
            }

            if (this._singleChild && this._children.length == 1) {
                throw new Exception("This panel can only support one child element.");
            }

            e._parent = this;
            this._children.add(e);
            this._childrenIds.add(e._id);

            if (this._canFlushIncrementally) {
                this._childrenIdsAdded.add(e._id);
            }

            if (this._frame != null) {
                Panel._applyFrameToElementAndChildren(e, this._frame);
            }

            return this;
        }

        function getChildren() { return this._children.clone(); }
        function firstChild() { return this._children.length == 0 ? null : this._children[0]; }
        function lastChild() { return this._children.length == 0 ? null : this._children[-1]; }

        function findChildIndex(e) {
            len = this._children.length;
            if (len > 0) {
                if (this._children[-1] == e) return len - 1;
                for (i = 0; i < len; ++i) {
                    if (this._children[i] == e) return i;
                }
            }
            return -1;
        }

        function clearChildren() {
            while (this._children.length > 0) {
                this.removeChild(this._children[-1]);
            }
            return this;
        }

        function removeChild(child) {
            index = this.findChildIndex(child);
            if (index == -1) throw new Exception("Attempted to remove a non-existent child element.");

            isLast = index == this._children.length - 1;
            this._children.remove(index);
            this._childrenIds.remove(index);

            if (this._canFlushIncrementally) {
                if (isLast) {
                    if (this._childrenIdsAdded.length > 0) {
                        this._childrenIdsAdded.pop();
                    } else {
                        this._childRemovalCount++;
                    }
                } else {
                    this._canFlushIncrementally = false;
                    this._childrenIdsAdded.clear();
                    this._childRemovalCount = 0;
                }
            }

            if (this._frame != null) {
                this._frame._dirtyPanels[this._id] = this;
            }

            return this;
        }

        function findElement(id) {
            if (this._userDefinedId == id) return this;
            for (child : this._children) {
                if (child._userDefinedId == id) return child;
                if (child._isPanel) {
                    descendant = child.findElement(id);
                    if (descendant != null) {
                        return descendant;
                    }
                }
            }
            return null;
        }

        static function _applyFrameToElementAndChildren(e, frame) {
            queue = [e];
            parent = e._parent;
            if (parent != frame) {
                frame._dirtyPanels[parent._id] = parent;
            }
            i = 0;
            while (i < queue.length) {
                current = queue[i++];
                current._frame = frame;
                id = current._id;
                frame._fullFlushElements[id] = current;
                frame._elementById[id] = current;
                frame._loadQueue.add(current);
                current._isClean = false;
                if (current._isPanel) {
                    frame._dirtyPanels[id] = current;
                    queue.concat(current._children);
                }
            }
        }

        static function _applyFrameRemovalToElementAndChildren(e, frame) {
            queue = [e];
            i = 0;
            elementLookup = frame._elementById;
            while (i < queue.length) {
                current = queue[i++];
                current._frame = null;
                id = current._id;
                elementLookup.remove(id);
            }
        }
    }
}
