/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { Range1d } from "../geometry3d/Range";
import { Range1dArray } from "../numerics/Range1dArray";
import { ClipStepAction, ClipUtilities } from "./ClipUtils";
/** BooleanClipNode is an abstract base class for boolean actions by an array of clippers.
 * * Derived class must implement
 *   * The single point test `isPointOnOrInsideChildren`
 *   * Boolean operation on 1d intervals `combineIntervals`
 * * The `keepInside` flag controls an additional optional flip of the boolean result.
 *   * if `keepInside === true`, accept the "inside" of the clip clippers
 *   * if `keepInside === false`, accept the "outside" of the child clippers.
 * * Hence the combinations of derived classes for (OR, AND, XOR) and keepInside are
 *   * (OR, true) = simple union (OR), i.e. "in" one or more clips
 *   * (OR, false) = complement of union (NOR), i.e. "outside" all clips
 *   * (AND, true) = simple intersection (AND), i.e. "in" all clips
 *   * (AND, false) = complement of intersection (NAND), i.e. "outside" one or more clips
 *   * (XOR,true) = simple parity, i.e. "in" an odd number of clips
 *   * (XOR,false) = complement of parity ), i.e. "in" an even number of clips
 * @internal
 */
export class BooleanClipNode {
    _clippers;
    _intervalsA;
    _intervalsB;
    _keepInside;
    constructor(keepInside) {
        this._keepInside = keepInside;
        this._clippers = [];
        this._intervalsA = [];
        this._intervalsB = [];
    }
    toJSON() {
        const data = [];
        for (const c of this._clippers) {
            const c1 = c;
            if (c1.toJSON)
                data.push(c1.toJSON());
        }
        // return this.formatJSON(data);
        const s = this.operationName;
        const json = {};
        json[s] = data;
        return json;
    }
    /** Capture a (reference to a) child node or nodes */
    captureChild(child) {
        if (Array.isArray(child)) {
            for (const c of child)
                this.captureChild(c);
        }
        else {
            this._clippers.push(child);
        }
    }
    /** Toggle the "keepInside" behavior.  Return the prior value.  */
    toggleResult() {
        return this.selectResult(!this._keepInside);
    }
    /** Set the "keepInside" behavior  */
    selectResult(keepInside) {
        const s = this._keepInside;
        this._keepInside = keepInside;
        return s;
    }
    /**
     * Conditionally (if a1 > a0 strictly) call announce (a0, a1).
     * * Return 0 if not called, 1 if called.
     */
    testedAnnounceNN(a0, a1, announce) {
        if (a0 < a1) {
            if (announce)
                announce(a0, a1);
            return 1;
        }
        return 0;
    }
    /**
     * Conditionally (if a1 > a0 strictly) call announce (a0, a1, cp).
     * * Return 0 if not called, 1 if called.
     */
    testedAnnounceNNC(a0, a1, cp, announce) {
        if (a0 < a1) {
            if (announce)
                announce(a0, a1, cp);
            return 1;
        }
        return 0;
    }
    /** Swap the _intervalsA and _intervalsB */
    swapAB() {
        const q = this._intervalsA;
        this._intervalsA = this._intervalsB;
        this._intervalsB = q;
    }
    /**
     * Announce all "outside intervals" --not masked by intervals
     * * Return true if any intervals announced.
     */
    announcePartsNN(keepInside, intervals, f0, f1, announce) {
        let numAnnounce = 0;
        if (!keepInside) {
            let lowFraction = f0;
            for (const interval of intervals) {
                numAnnounce += this.testedAnnounceNN(lowFraction, interval.low, announce);
                lowFraction = interval.high;
            }
            numAnnounce += this.testedAnnounceNN(lowFraction, f1, announce);
        }
        else {
            for (const interval of intervals) {
                // use f0..f1 ?
                numAnnounce += this.testedAnnounceNN(interval.low, interval.high, announce);
            }
        }
        return numAnnounce > 0;
    }
    /**
     * Announce all "outside intervals" --not masked by intervals
     * * Return true if any intervals announced.
     */
    announcePartsNNC(keepInside, intervals, f0, f1, cp, announce) {
        let numAnnounce = 0;
        if (!keepInside) {
            let lowFraction = f0;
            for (const interval of intervals) {
                numAnnounce += this.testedAnnounceNNC(lowFraction, interval.low, cp, announce);
                lowFraction = interval.high;
            }
            numAnnounce += this.testedAnnounceNNC(lowFraction, f1, cp, announce);
        }
        else {
            for (const interval of intervals) {
                // use f0..f1 ?
                numAnnounce += this.testedAnnounceNNC(interval.low, interval.high, cp, announce);
            }
        }
        return numAnnounce > 0;
    }
    /** Invoke callback to test if a point is "in" this clipper */
    isPointOnOrInside(point) {
        const q = this.isPointOnOrInsideChildren(point);
        return this._keepInside ? q : !q;
    }
    /** Announce "in" portions of a line segment. See `Clipper.announceClippedSegmentIntervals` */
    announceClippedSegmentIntervals(f0, f1, pointA, pointB, announce) {
        this._intervalsA.length = 0;
        const announceIntervalB = (a0, a1) => {
            this._intervalsB.push(Range1d.createXX(a0, a1));
        };
        // Strategy:
        //  _intervalsA is the accumulated UNION of from clippers
        // _intervalsB is the current clipper.
        // announceIntervalB appends single new interval to _intervalB
        // at end, output gaps in _intervalsA
        //
        let i = 0;
        for (const c of this._clippers) {
            this._intervalsB.length = 0;
            c.announceClippedSegmentIntervals(f0, f1, pointA, pointB, announceIntervalB);
            Range1dArray.simplifySortUnion(this._intervalsB);
            if (i === 0) {
                this.swapAB();
            }
            else {
                this._intervalsA = this.combineIntervals(this._intervalsA, this._intervalsB);
            }
            i++;
        }
        return this.announcePartsNN(this._keepInside, this._intervalsA, f0, f1, announce);
    }
    /** Announce "in" portions of a line segment.  See `Clipper.announceClippedSegmentIntervals` */
    announceClippedArcIntervals(arc, announce) {
        this._intervalsA.length = 0;
        const announceIntervalB = (a0, a1) => {
            this._intervalsB.push(Range1d.createXX(a0, a1));
        };
        let i = 0;
        for (const c of this._clippers) {
            this._intervalsB.length = 0;
            c.announceClippedArcIntervals(arc, announceIntervalB);
            Range1dArray.simplifySortUnion(this._intervalsB);
            if (i === 0) {
                this.swapAB();
            }
            else {
                this._intervalsA = this.combineIntervals(this._intervalsA, this._intervalsB);
            }
            i++;
        }
        return this.announcePartsNNC(this._keepInside, this._intervalsA, 0, 1, arc, announce);
    }
}
/**
 * Implement [BooleanClipNode] virtual methods for intersection (boolean OR) among children
 * @internal
 */
export class BooleanClipNodeUnion extends BooleanClipNode {
    get operationName() { return this._keepInside ? "OR" : "NOR"; }
    constructor(keepInside) {
        super(keepInside);
    }
    /** Return true if inside any child clipper */
    isPointOnOrInsideChildren(point) {
        for (const clipper of this._clippers) {
            if (clipper.isPointOnOrInside(point))
                return true;
        }
        return false;
    }
    combineIntervals(operandA, operandB) {
        return Range1dArray.unionSorted(operandA, operandB);
    }
    appendPolygonClip(xyz, insideFragments, outsideFragments, arrayCache) {
        ClipUtilities.doPolygonClipSequence(xyz, this._clippers, this._keepInside ? insideFragments : outsideFragments, this._keepInside ? outsideFragments : insideFragments, undefined, ClipStepAction.acceptIn, ClipStepAction.passToNextStep, ClipStepAction.acceptOut, arrayCache);
    }
}
/**
 * Implement [BooleanClipNode] virtual methods for intersection (boolean OR) among children
 * @internal
 */
export class BooleanClipNodeParity extends BooleanClipNode {
    get operationName() { return this._keepInside ? "XOR" : "NXOR"; }
    constructor(keepInside) {
        super(keepInside);
    }
    /** Return true if inside an odd number of clippers child clipper */
    isPointOnOrInsideChildren(point) {
        let q = false;
        for (const clipper of this._clippers) {
            if (clipper.isPointOnOrInside(point))
                q = !q;
        }
        return q;
    }
    combineIntervals(operandA, operandB) {
        return Range1dArray.paritySorted(operandA, operandB);
    }
    appendPolygonClip(xyz, insideFragments, outsideFragments, arrayCache) {
        ClipUtilities.doPolygonClipParitySequence(xyz, this._clippers, this._keepInside ? insideFragments : outsideFragments, this._keepInside ? outsideFragments : insideFragments, arrayCache);
    }
}
/**
 * Implement [BooleanClipNode] virtual methods for intersection (boolean OR) among children
 * @internal
 */
export class BooleanClipNodeIntersection extends BooleanClipNode {
    get operationName() { return this._keepInside ? "AND" : "NAND"; }
    constructor(keepInside) {
        super(keepInside);
    }
    /** Return false if outside of any child clipper */
    isPointOnOrInsideChildren(point) {
        for (const clipper of this._clippers) {
            if (!clipper.isPointOnOrInside(point))
                return false;
        }
        return true;
    }
    combineIntervals(operandA, operandB) {
        return Range1dArray.intersectSorted(operandA, operandB);
    }
    appendPolygonClip(xyz, insideFragments, outsideFragments, arrayCache) {
        ClipUtilities.doPolygonClipSequence(xyz, this._clippers, this._keepInside ? insideFragments : outsideFragments, this._keepInside ? outsideFragments : insideFragments, undefined, ClipStepAction.passToNextStep, ClipStepAction.acceptOut, ClipStepAction.acceptIn, arrayCache);
    }
}
//# sourceMappingURL=BooleanClipNode.js.map