/**
 * Group是一个容器，可以插入子节点，Group的变换也会被应用到子节点上
 * @module zrender/graphic/Group
 * @example
 *     const Group = require('zrender/graphic/Group');
 *     const Circle = require('zrender/graphic/shape/Circle');
 *     const g = new Group();
 *     g.position[0] = 100;
 *     g.position[1] = 100;
 *     g.add(new Circle({
 *         style: {
 *             x: 100,
 *             y: 100,
 *             r: 20,
 *         }
 *     }));
 *     zr.add(g);
 */

import * as zrUtil from '../core/util';
import Element, { ElementProps } from '../Element';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Displayable from './Displayable';
import { ZRenderType } from '../zrender';

export interface GroupProps extends ElementProps {
}

class Group extends Element<GroupProps> {

    readonly isGroup = true

    private _children: Element[] = []


    constructor(opts?: GroupProps) {
        super();

        this.attr(opts);
    }

    /**
     * Get children reference.
     */
    childrenRef() {
        return this._children;
    }

    /**
     * Get children copy.
     */
    children() {
        return this._children.slice();
    }

    /**
     * 获取指定 index 的儿子节点
     */
    childAt(idx: number): Element {
        return this._children[idx];
    }

    /**
     * 获取指定名字的儿子节点
     */
    childOfName(name: string): Element {
        const children = this._children;
        for (let i = 0; i < children.length; i++) {
            if (children[i].name === name) {
                return children[i];
            }
        }
    }

    childCount(): number {
        return this._children.length;
    }

    /**
     * 添加子节点到最后
     */
    add(child: Element): Group {
        if (child) {
            if (child !== this && child.parent !== this) {
                this._children.push(child);
                this._doAdd(child);
            }
            if (process.env.NODE_ENV !== 'production') {
                if (child.__hostTarget) {
                    throw 'This elemenet has been used as an attachment';
                }
            }
        }

        return this;
    }

    /**
     * 添加子节点在 nextSibling 之前
     */
    addBefore(child: Element, nextSibling: Element) {
        if (child && child !== this && child.parent !== this
            && nextSibling && nextSibling.parent === this) {

            const children = this._children;
            const idx = children.indexOf(nextSibling);

            if (idx >= 0) {
                children.splice(idx, 0, child);
                this._doAdd(child);
            }
        }

        return this;
    }

    replace(oldChild: Element, newChild: Element) {
        const idx = zrUtil.indexOf(this._children, oldChild);
        if (idx >= 0) {
            this.replaceAt(newChild, idx);
        }
        return this;
    }

    replaceAt(child: Element, index: number) {
        const children = this._children;
        const old = children[index];

        if (child && child !== this && child.parent !== this && child !== old) {
            children[index] = child;

            old.parent = null;
            const zr = this.__zr;
            if (zr) {
                old.removeSelfFromZr(zr);
            }

            this._doAdd(child);
        }

        return this;
    }

    _doAdd(child: Element) {
        if (child.parent) {
            // Parent must be a group
            (child.parent as Group).remove(child);
        }

        child.parent = this;

        const zr = this.__zr;
        if (zr && zr !== (child as Group).__zr) {    // Only group has __storage

            child.addSelfToZr(zr);
        }

        zr && zr.refresh();
    }

    /**
     * Remove child
     * @param child
     */
    remove(child: Element) {
        const zr = this.__zr;
        const children = this._children;

        const idx = zrUtil.indexOf(children, child);
        if (idx < 0) {
            return this;
        }
        children.splice(idx, 1);

        child.parent = null;

        if (zr) {

            child.removeSelfFromZr(zr);
        }

        zr && zr.refresh();

        return this;
    }

    /**
     * Remove all children
     */
    removeAll() {
        const children = this._children;
        const zr = this.__zr;
        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            if (zr) {
                child.removeSelfFromZr(zr);
            }
            child.parent = null;
        }
        children.length = 0;

        return this;
    }

    /**
     * 遍历所有子节点
     */
    eachChild<Context>(
        cb: (this: Context, el: Element, index?: number) => void,
        context?: Context
    ) {
        const children = this._children;
        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            cb.call(context, child, i);
        }
        return this;
    }

    /**
     * Visit all descendants.
     * Return false in callback to stop visit descendants of current node
     */
    // TODO Group itself should also invoke the callback.
    traverse<T>(
        cb: (this: T, el: Element) => boolean | void,
        context?: T
    ) {
        for (let i = 0; i < this._children.length; i++) {
            const child = this._children[i];
            const stopped = cb.call(context, child);

            if (child.isGroup && !stopped) {
                child.traverse(cb, context);
            }
        }
        return this;
    }

    addSelfToZr(zr: ZRenderType) {
        super.addSelfToZr(zr);
        for (let i = 0; i < this._children.length; i++) {
            const child = this._children[i];
            child.addSelfToZr(zr);
        }
    }

    removeSelfFromZr(zr: ZRenderType) {
        super.removeSelfFromZr(zr);
        for (let i = 0; i < this._children.length; i++) {
            const child = this._children[i];
            child.removeSelfFromZr(zr);
        }
    }

    getBoundingRect(includeChildren?: Element[]): BoundingRect {
        // TODO Caching
        // FIXME: if no child or all ignored, the returned boundingRect (0, 0, 0, 0)
        //  is not a correct bounding rect in some scenarios, such as rect union and intersection detection.
        const tmpRect = new BoundingRect(0, 0, 0, 0);
        const children = includeChildren || this._children;
        const tmpMat: MatrixArray = [];
        let rect = null;

        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            // TODO invisible?
            if (child.ignore || (child as Displayable).invisible) {
                continue;
            }

            const childRect = child.getBoundingRect();
            const transform = child.getLocalTransform(tmpMat);
            // TODO
            // The boundingRect cacluated by transforming original
            // rect may be bigger than the actual bundingRect when rotation
            // is used. (Consider a circle rotated aginst its center, where
            // the actual boundingRect should be the same as that not be
            // rotated.) But we can not find better approach to calculate
            // actual boundingRect yet, considering performance.
            if (transform) {
                BoundingRect.applyTransform(tmpRect, childRect, transform);
                rect = rect || tmpRect.clone();
                rect.union(tmpRect);
            }
            else {
                rect = rect || childRect.clone();
                rect.union(childRect);
            }
        }
        return rect || tmpRect;
    }
}

Group.prototype.type = 'group';
// Storage will use childrenRef to get children to render.
export interface GroupLike extends Element {
    childrenRef(): Element[]
}

export default Group;