packages/react-moveable/src/MoveableGroup.tsx

import MoveableManager from "./MoveableManager";
import { GroupableProps, GroupRect, MoveableManagerInterface, MoveableTargetGroupsType, RectInfo } from "./types";
import ChildrenDiffer from "@egjs/children-differ";
import { getControlAbleGesto, getTargetAbleGesto } from "./gesto/getAbleGesto";
import Groupable from "./ables/Groupable";
import { MIN_NUM, MAX_NUM, TINY_NUM } from "./consts";
import {
    getAbsolutePosesByState, equals, unsetGesto, rotatePosesInfo,
    convertTransformOriginArray,
    isDeepArrayEquals,
    sign,
    getRefTarget,
} from "./utils";
import { minus, plus } from "@scena/matrix";
import { getIntersectionPointsByConstants, getMinMaxs } from "overlap-area";
import { find, isArray, throttle } from "@daybrush/utils";
import { getMoveableTargetInfo } from "./utils/getMoveableTargetInfo";
import { solveC, solveConstantsDistance } from "./Snappable/utils";
import { setStoreCache } from "./store/Store";

function getMaxPos(poses: number[][][], index: number) {
    return Math.max(...poses.map(([pos1, pos2, pos3, pos4]) => {
        return Math.max(pos1[index], pos2[index], pos3[index], pos4[index]);
    }));
}
function getMinPos(poses: number[][][], index: number) {
    return Math.min(...poses.map(([pos1, pos2, pos3, pos4]) => {
        return Math.min(pos1[index], pos2[index], pos3[index], pos4[index]);
    }));
}


function getGroupRect(parentPoses: number[][][], rotation: number): GroupRect {
    let pos1 = [0, 0];
    let pos2 = [0, 0];
    let pos3 = [0, 0];
    let pos4 = [0, 0];
    let width = 0;
    let height = 0;

    if (!parentPoses.length) {
        return {
            pos1,
            pos2,
            pos3,
            pos4,
            minX: 0,
            minY: 0,
            maxX: 0,
            maxY: 0,
            width,
            height,
            rotation,
        };
    }
    const fixedRotation = throttle(rotation, TINY_NUM);

    if (fixedRotation % 90) {
        const rad = fixedRotation / 180 * Math.PI;
        const a1 = Math.tan(rad);
        const a2 = -1 / a1;
        // ax = y  // -ax + y = 0 // 0 => 1
        // -ax = y // ax + y = 0  // 0 => 3
        const a1MinMax = [MAX_NUM, MIN_NUM];
        const a1MinMaxPos = [[0, 0], [0, 0]];
        const a2MinMax = [MAX_NUM, MIN_NUM];
        const a2MinMaxPos = [[0, 0], [0, 0]];

        parentPoses.forEach(poses => {
            poses.forEach(pos => {

                // const b1 = pos[1] - a1 * pos[0];
                // const b2 = pos[1] - a2 * pos[0];

                const a1Dist = solveConstantsDistance([-a1, 1, 0], pos);
                const a2Dist = solveConstantsDistance([-a2, 1, 0], pos);

                if (a1MinMax[0] > a1Dist) {
                    a1MinMaxPos[0] = pos;
                    a1MinMax[0] = a1Dist;
                }
                if (a1MinMax[1] < a1Dist) {
                    a1MinMaxPos[1] = pos;
                    a1MinMax[1] = a1Dist;
                }
                if (a2MinMax[0] > a2Dist) {
                    a2MinMaxPos[0] = pos;
                    a2MinMax[0] = a2Dist;
                }
                if (a2MinMax[1] < a2Dist) {
                    a2MinMaxPos[1] = pos;
                    a2MinMax[1] = a2Dist;
                }
            });
        });

        const [a1MinPos, a1MaxPos] = a1MinMaxPos;
        const [a2MinPos, a2MaxPos] = a2MinMaxPos;

        const minHorizontalLine = [-a1, 1, solveC([-a1, 1], a1MinPos)];
        const maxHorizontalLine = [-a1, 1, solveC([-a1, 1], a1MaxPos)];

        const minVerticalLine = [-a2, 1, solveC([-a2, 1], a2MinPos)];
        const maxVerticalLine = [-a2, 1, solveC([-a2, 1], a2MaxPos)];

        [pos1, pos2, pos3, pos4] = [
            [minHorizontalLine, minVerticalLine],
            [minHorizontalLine, maxVerticalLine],
            [maxHorizontalLine, minVerticalLine],
            [maxHorizontalLine, maxVerticalLine],
        ].map(([line1, line2]) => getIntersectionPointsByConstants(line1, line2)[0]);

        width = a2MinMax[1] - a2MinMax[0];
        height = a1MinMax[1] - a1MinMax[0];
    } else {
        const minX = getMinPos(parentPoses, 0);
        const minY = getMinPos(parentPoses, 1);
        const maxX = getMaxPos(parentPoses, 0);
        const maxY = getMaxPos(parentPoses, 1);

        pos1 = [minX, minY];
        pos2 = [maxX, minY];
        pos3 = [minX, maxY];
        pos4 = [maxX, maxY];
        width = maxX - minX;
        height = maxY - minY;
        if (fixedRotation % 180) {
            // 0
            // 1 2
            // 3 4
            // 90
            // 3 1
            // 4 2
            // 180
            // 4 3
            // 2 1
            // 270
            // 2 4
            // 1 3
            // 1, 2, 3,4 = 3 1 4 2
            const changedX = [pos3, pos1, pos4, pos2];

            [pos1, pos2, pos3, pos4] = changedX;
            width = maxY - minY;
            height = maxX - minX;
        }

    }
    if (fixedRotation % 360 > 180) {
        // 1 2   4 3
        // 3 4   2 1
        const changedX = [pos4, pos3, pos2, pos1];

        [pos1, pos2, pos3, pos4] = changedX;
    }
    const { minX, minY, maxX, maxY } = getMinMaxs([pos1, pos2, pos3, pos4]);

    return {
        pos1,
        pos2,
        pos3,
        pos4,
        width,
        height,
        minX,
        minY,
        maxX,
        maxY,
        rotation,
    };
}
type SelfGroup = Array<MoveableManager | null | SelfGroup>;
type CheckedMoveableManager = { finded: boolean; manager: MoveableManager };

function findMoveableGroups(
    moveables: CheckedMoveableManager[],
    childTargetGroups: MoveableTargetGroupsType,
): SelfGroup {
    const groups = childTargetGroups.map(targetGroup => {
        if (isArray(targetGroup)) {
            const childMoveableGroups = findMoveableGroups(moveables, targetGroup);
            const length = childMoveableGroups.length;

            if (length > 1) {
                return childMoveableGroups;
            } else if (length === 1) {
                return childMoveableGroups[0];
            } else {
                return null;
            }
        } else {
            const checked = find(moveables, ({ manager }) => manager.props.target === targetGroup)!;

            if (checked) {
                checked.finded = true;
                return checked.manager;
            }
            return null;
        }
    }).filter(Boolean);

    if (groups.length === 1 && isArray(groups[0])) {
        return groups[0];
    }
    return groups;
}

/**
 * @namespace Moveable.Group
 * @description You can make targets moveable.
 */
class MoveableGroup extends MoveableManager<GroupableProps> {
    public static defaultProps = {
        ...MoveableManager.defaultProps,
        transformOrigin: ["50%", "50%"],
        groupable: true,
        dragArea: true,
        keepRatio: true,
        targets: [],
        defaultGroupRotate: 0,
        defaultGroupOrigin: "50% 50%",
    };
    public differ: ChildrenDiffer<HTMLElement | SVGElement> = new ChildrenDiffer();
    public moveables: MoveableManager[] = [];
    public transformOrigin = "50% 50%";
    public renderGroupRects: GroupRect[] = [];
    private _targetGroups: MoveableTargetGroupsType = [];
    private _hasFirstTargets = false;

    public componentDidMount() {
        super.componentDidMount();
    }
    public checkUpdate() {
        this._isPropTargetChanged = false;
        this.updateAbles();
    }
    public getTargets() {
        return this.props.targets!;
    }
    public updateRect(type?: "Start" | "" | "End", isTarget?: boolean, isSetState = true) {
        const state = this.state;

        if (!this.controlBox || state.isPersisted) {
            return;
        }
        setStoreCache(true);
        this.moveables.forEach(moveable => {
            moveable.updateRect(type, false, false);
        });

        const props = this.props;
        const moveables = this.moveables;
        const target = state.target! || props.target!;
        const checkeds = moveables.map(moveable => ({ finded: false, manager: moveable }));
        const targetGroups = this.props.targetGroups || [];
        const moveableGroups = findMoveableGroups(
            checkeds,
            targetGroups,
        );
        const useDefaultGroupRotate = props.useDefaultGroupRotate;

        moveableGroups.push(...checkeds.filter(({ finded }) => !finded).map(({ manager }) => manager));

        const renderGroupRects: GroupRect[] = [];
        const isReset = !isTarget || (type !== "" && props.updateGroup);
        let defaultGroupRotate = props.defaultGroupRotate || 0;

        if (!this._hasFirstTargets) {
            const persistedRoatation = props.persistData?.rotation;

            if (persistedRoatation != null) {
                defaultGroupRotate = persistedRoatation;
            }
        }

        function getMoveableGroupRect(group: SelfGroup, parentRotation: number, isRoot?: boolean): GroupRect {
            const posesRotations = group.map(moveable => {
                if (isArray(moveable)) {
                    const rect = getMoveableGroupRect(moveable, parentRotation);
                    const poses = [rect.pos1, rect.pos2, rect.pos3, rect.pos4];

                    renderGroupRects.push(rect);
                    return { poses, rotation: rect.rotation };
                } else {
                    return {
                        poses: getAbsolutePosesByState(moveable!.state),
                        rotation: moveable!.getRotation(),
                    };
                }
            });
            const rotations = posesRotations.map(({ rotation }) => rotation);

            let groupRotation = 0;
            const firstRotation = rotations[0];
            const isSameRotation = rotations.every(nextRotation => {
                return Math.abs(firstRotation - nextRotation) < 0.1;
            });

            if (isReset) {
                groupRotation = !useDefaultGroupRotate && isSameRotation ? firstRotation : defaultGroupRotate;
            } else {
                groupRotation = !useDefaultGroupRotate && !isRoot && isSameRotation ? firstRotation : parentRotation;
            }
            const groupPoses = posesRotations.map(({ poses }) => poses);
            const groupRect = getGroupRect(
                groupPoses,
                groupRotation,
            );

            return groupRect;
        }
        const rootGroupRect = getMoveableGroupRect(moveableGroups, this.rotation, true);

        if (isReset) {
            // reset rotataion
            this.rotation = rootGroupRect.rotation;
            this.transformOrigin = props.defaultGroupOrigin || "50% 50%";
            this.scale = [1, 1];
        }


        this._targetGroups = targetGroups;
        this.renderGroupRects = renderGroupRects;
        const transformOrigin = this.transformOrigin;
        const rotation = this.rotation;
        const scale = this.scale;
        const { width, height, minX, minY } = rootGroupRect;
        const posesInfo = rotatePosesInfo(
            [
                [0, 0],
                [width, 0],
                [0, height],
                [width, height],
            ],
            convertTransformOriginArray(transformOrigin, width, height),
            this.rotation / 180 * Math.PI,
        );

        const { minX: deltaX, minY: deltaY } = getMinMaxs(posesInfo.result);
        const rotateScale = ` rotate(${rotation}deg)`
            + ` scale(${sign(scale[0])}, ${sign(scale[1])})`;
        const transform = `translate(${-deltaX}px, ${-deltaY}px)${rotateScale}`;

        this.controlBox.style.transform
            = `translate3d(${minX}px, ${minY}px, ${this.props.translateZ || 0})`;

        target.style.cssText += `left:0px;top:0px;`
            + `transform-origin:${transformOrigin};`
            + `width:${width}px;height:${height}px;`
            + `transform: ${transform}`;
        state.width = width;
        state.height = height;

        const container = this.getContainer();
        const info = getMoveableTargetInfo(
            this.controlBox,
            target,
            this.controlBox,
            this.getContainer(),
            this._rootContainer || container,
            [],
        );
        const pos = [info.left!, info.top!];
        const [
            pos1,
            pos2,
            pos3,
            pos4,
        ] = getAbsolutePosesByState(info); // info.left + info.pos(1 ~ 4)

        const minPos = getMinMaxs([pos1, pos2, pos3, pos4]);
        const delta = [minPos.minX, minPos.minY];
        const direction = sign(scale[0] * scale[1]);

        info.pos1 = minus(pos1, delta);
        info.pos2 = minus(pos2, delta);
        info.pos3 = minus(pos3, delta);
        info.pos4 = minus(pos4, delta);
        // info.left = info.left + delta[0];
        // info.top = info.top + delta[1];
        info.left = minX - info.left! + delta[0];
        info.top = minY - info.top! + delta[1];
        info.origin = minus(plus(pos, info.origin!), delta);
        info.beforeOrigin = minus(plus(pos, info.beforeOrigin!), delta);
        info.originalBeforeOrigin = plus(pos, info.originalBeforeOrigin!);
        info.transformOrigin = minus(plus(pos, info.transformOrigin!), delta);
        target.style.transform
            = `translate(${-deltaX - delta[0]}px, ${-deltaY - delta[1]}px)`
            + rotateScale;

        setStoreCache();
        this.updateState(
            {
                ...info,
                posDelta: delta,
                direction,
                beforeDirection: direction,
            },
            isSetState,
        );
    }
    public getRect(): RectInfo {
        return {
            ...super.getRect(),
            children: this.moveables.map(child => child.getRect()),
        };
    }
    public triggerEvent(name: string, e: any, isManager?: boolean): any {
        if (isManager || name.indexOf("Group") > -1) {
            return super.triggerEvent(name as any, e);
        } else {
            this._emitter.trigger(name, e);
        }
    }
    public getRequestChildStyles() {
        const styleNames = this.getEnabledAbles().reduce((names, able) => {
            const ableStyleNames = (able.requestChildStyle?.() ?? []) as Array<keyof CSSStyleDeclaration>;

            return [...names, ...ableStyleNames];
        }, [] as Array<keyof CSSStyleDeclaration>);


        return styleNames;
    }

    public getMoveables(): MoveableManagerInterface[] {
        return [...this.moveables];
    }
    protected updateAbles() {
        super.updateAbles([...this.props.ables!, Groupable], "Group");
    }
    protected _updateTargets() {
        super._updateTargets();
        this._originalDragTarget = this.props.dragTarget || this.areaElement;
        this._dragTarget = getRefTarget(this._originalDragTarget, true);
    }
    protected _updateEvents() {
        const state = this.state;
        const props = this.props;


        const prevTarget = this._prevDragTarget;
        const nextTarget = props.dragTarget || this.areaElement;
        const targets = props.targets!;
        const { added, changed, removed } = this.differ.update(targets);
        const isTargetChanged = added.length || removed.length;

        if (isTargetChanged || this._prevOriginalDragTarget !== this._originalDragTarget) {
            unsetGesto(this, false);
            unsetGesto(this, true);
            this.updateState({ gestos: {} });
        }
        if (prevTarget !== nextTarget) {
            state.target = null;
        }
        if (!state.target) {
            state.target = this.areaElement;
            this.controlBox.style.display = "block";
        }
        if (state.target) {
            if (!this.targetGesto) {
                this.targetGesto = getTargetAbleGesto(this, this._dragTarget!, "Group");
            }
            if (!this.controlGesto) {
                this.controlGesto = getControlAbleGesto(this, "GroupControl");
            }
        }
        const isContainerChanged = !equals(state.container, props.container);

        if (isContainerChanged) {
            state.container = props.container;
        }


        if (
            isContainerChanged
            || isTargetChanged
            || this.transformOrigin !== (props.defaultGroupOrigin || "50% 50%")
            || changed.length
            || targets.length && !isDeepArrayEquals(this._targetGroups, props.targetGroups || [])
        ) {
            this.updateRect();
            this._hasFirstTargets = true;
        }
        this._isPropTargetChanged = !!isTargetChanged;
    }
    protected _updateObserver() { }
}

/**
 * Sets the initial rotation of the group.
 * @name Moveable.Group#defaultGroupRotate
 * @default 0
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, {
 *   target: [].slice.call(document.querySelectorAll(".target")),
 *   defaultGroupRotate: 0,
 * });
 *
 * moveable.defaultGroupRotate = 40;
 */

/**
 * Sets the initial origin of the group.
 * @name Moveable.Group#defaultGroupOrigin
 * @default 0
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, {
 *   target: [].slice.call(document.querySelectorAll(".target")),
 *   defaultGroupOrigin: "50% 50%",
 * });
 *
 * moveable.defaultGroupOrigin = "20% 40%";
 */


/**
 * Whether to hide the line in child moveable for group corresponding to the rect of the target.
 * @name Moveable.Group#hideChildMoveableDefaultLines
 * @default false
 * @example
 * import Moveable from "moveable";
 *
 * const moveable = new Moveable(document.body, {
 *   target: [].slice.call(document.querySelectorAll(".target")),
 *   hideChildMoveableDefaultLines: false,
 * });
 *
 * moveable.hideChildMoveableDefaultLines = true;
 */
export default MoveableGroup;