/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Serialization
 */
import { BSplineWrapMode, KnotVector } from "../bspline/KnotVector";
import { NumberArray } from "../geometry3d/PointHelpers";
/**
 * The `SerializationHelpers` namespace has helper classes for serializing and deserializing geometry, such as B-spline curves and surfaces.
 * @public
 */
export var SerializationHelpers;
(function (SerializationHelpers) {
    /** Constructor for BSplineCurveData that populates the required data. Inputs are captured, not copied. */
    function createBSplineCurveData(poles, dim, knots, numPoles, order) {
        return { poles, dim, params: { numPoles, order, knots } };
    }
    SerializationHelpers.createBSplineCurveData = createBSplineCurveData;
    /** Constructor for BSplineSurfaceData that populates the required data. Inputs are captured, not copied. */
    function createBSplineSurfaceData(poles, dim, uKnots, uNumPoles, uOrder, vKnots, vNumPoles, vOrder) {
        return { poles, dim, uParams: { numPoles: uNumPoles, order: uOrder, knots: uKnots }, vParams: { numPoles: vNumPoles, order: vOrder, knots: vKnots } };
    }
    SerializationHelpers.createBSplineSurfaceData = createBSplineSurfaceData;
    /** Clone B-spline curve data */
    function cloneBSplineCurveData(source) {
        return {
            poles: (source.poles instanceof Float64Array) ? new Float64Array(source.poles) : NumberArray.copy2d(source.poles),
            dim: source.dim,
            weights: source.weights ? source.weights.slice() : undefined,
            params: {
                numPoles: source.params.numPoles,
                order: source.params.order,
                closed: source.params.closed,
                knots: source.params.knots.slice(),
                wrapMode: source.params.wrapMode,
            },
        };
    }
    SerializationHelpers.cloneBSplineCurveData = cloneBSplineCurveData;
    /** Clone B-spline surface data */
    function cloneBSplineSurfaceData(source) {
        return {
            poles: (source.poles instanceof Float64Array) ? new Float64Array(source.poles) : NumberArray.copy3d(source.poles),
            dim: source.dim,
            weights: source.weights ? ((source.weights instanceof Float64Array) ? source.weights.slice() : NumberArray.copy2d(source.weights)) : undefined,
            uParams: {
                numPoles: source.uParams.numPoles,
                order: source.uParams.order,
                closed: source.uParams.closed,
                knots: source.uParams.knots.slice(),
                wrapMode: source.uParams.wrapMode,
            },
            vParams: {
                numPoles: source.vParams.numPoles,
                order: source.vParams.order,
                closed: source.vParams.closed,
                knots: source.vParams.knots.slice(),
                wrapMode: source.vParams.wrapMode,
            },
        };
    }
    SerializationHelpers.cloneBSplineSurfaceData = cloneBSplineSurfaceData;
    /** Copy B-spline curve data from source to dest */
    function copyBSplineCurveDataPoles(source) {
        let nPole = 0;
        let nCoordPerPole = 0;
        let nPoleCoords = 0;
        let poleDimProduct = 0;
        if (source.poles instanceof Float64Array) {
            nPole = source.params.numPoles;
            nCoordPerPole = source.dim;
            nPoleCoords = source.poles.length;
            poleDimProduct = nPole * nCoordPerPole;
        }
        else {
            nPole = source.poles.length;
            if (nPole > 0)
                nCoordPerPole = source.poles[0].length;
            nPoleCoords = poleDimProduct = nPole * nCoordPerPole;
        }
        if (0 === poleDimProduct || poleDimProduct > nPoleCoords || nCoordPerPole !== source.dim)
            return {};
        let nWeight = 0;
        let nWeightCoords = 0;
        let weightDimProduct = 0;
        if (source.weights !== undefined) {
            if (source.weights instanceof Float64Array) {
                nWeight = source.params.numPoles;
                nWeightCoords = source.weights.length;
                weightDimProduct = nWeight;
            }
            else {
                nWeight = source.weights.length;
                nWeightCoords = weightDimProduct = nWeight;
            }
            if (0 === weightDimProduct || weightDimProduct > nWeightCoords || nWeight !== nPole)
                return {};
        }
        // convert variant source to structured number array
        let poles;
        let weights;
        if (source.poles instanceof Float64Array)
            poles = NumberArray.unpack2d(source.poles, nCoordPerPole);
        else
            poles = NumberArray.copy2d(source.poles);
        if (poles && source.weights)
            weights = NumberArray.create(source.weights);
        return { poles, weights };
    }
    /** Copy B-spline surface data from source to dest */
    function copyBSplineSurfaceDataPoles(source) {
        let nPoleRow = 0;
        let nPolePerRow = 0;
        let nCoordPerPole = 0;
        let nCoords = 0;
        let poleDimProduct = 0;
        if (source.poles instanceof Float64Array) {
            nPoleRow = source.vParams.numPoles;
            nPolePerRow = source.uParams.numPoles;
            nCoordPerPole = source.dim;
            nCoords = source.poles.length;
            poleDimProduct = nPoleRow * nPolePerRow * nCoordPerPole;
        }
        else {
            nPoleRow = source.poles.length;
            if (nPoleRow > 0)
                nPolePerRow = source.poles[0].length;
            if (nPolePerRow > 0)
                nCoordPerPole = source.poles[0][0].length;
            nCoords = poleDimProduct = nPoleRow * nPolePerRow * nCoordPerPole;
        }
        if (0 === poleDimProduct || poleDimProduct > nCoords || nCoordPerPole !== source.dim)
            return {};
        let nWeightRow = 0;
        let nWeightPerRow = 0;
        let nWeightCoords = 0;
        let weightDimProduct = 0;
        if (source.weights !== undefined) {
            if (source.weights instanceof Float64Array) {
                nWeightRow = source.vParams.numPoles;
                nWeightPerRow = source.uParams.numPoles;
                nWeightCoords = source.weights.length;
                weightDimProduct = nWeightRow * nWeightPerRow;
            }
            else {
                nWeightRow = source.weights.length;
                if (nWeightRow > 0)
                    nWeightPerRow = source.weights[0].length;
                nWeightCoords = weightDimProduct = nWeightRow * nWeightPerRow;
            }
            if (0 === weightDimProduct || weightDimProduct > nWeightCoords || nWeightRow !== nPoleRow || nWeightPerRow !== nPolePerRow)
                return {};
        }
        // convert variant source to structured number array
        let poles;
        let weights;
        if (source.poles instanceof Float64Array)
            poles = NumberArray.unpack3d(source.poles, nPolePerRow, nCoordPerPole);
        else
            poles = NumberArray.copy3d(source.poles);
        if (poles && source.weights) {
            if (source.weights instanceof Float64Array)
                weights = NumberArray.unpack2d(source.weights, nWeightPerRow);
            else
                weights = NumberArray.copy2d(source.weights);
        }
        return { poles, weights };
    }
    /** Convert B-spline curve data arrays to the types specified by options. */
    function convertBSplineCurveDataArrays(data, options) {
        if (undefined !== options?.jsonPoles) {
            const packedPoles = data.poles instanceof Float64Array;
            if (options.jsonPoles && packedPoles)
                data.poles = NumberArray.unpack2d(data.poles, data.dim);
            else if (!options.jsonPoles && !packedPoles)
                data.poles = NumberArray.pack(data.poles);
            if (data.weights) {
                const packedWeights = data.weights instanceof Float64Array;
                if (options.jsonPoles && packedWeights)
                    data.weights = NumberArray.create(data.weights);
                else if (!options.jsonPoles && !packedWeights)
                    data.weights = NumberArray.pack(data.weights);
            }
        }
        if (undefined !== options?.jsonKnots) {
            const packedKnots = data.params.knots instanceof Float64Array;
            if (options.jsonKnots && packedKnots)
                data.params.knots = NumberArray.create(data.params.knots);
            else if (!options.jsonKnots && !packedKnots)
                data.params.knots = NumberArray.pack(data.params.knots);
        }
    }
    /** Convert B-spline surface data arrays to the types specified by options. */
    function convertBSplineSurfaceDataArrays(data, options) {
        if (undefined !== options?.jsonPoles) {
            const packedPoles = data.poles instanceof Float64Array;
            if (options.jsonPoles && packedPoles)
                data.poles = NumberArray.unpack3d(data.poles, data.uParams.numPoles, data.dim);
            else if (!options.jsonPoles && !packedPoles)
                data.poles = NumberArray.pack(data.poles);
            if (data.weights) {
                const packedWeights = data.weights instanceof Float64Array;
                if (options.jsonPoles && packedWeights)
                    data.weights = NumberArray.unpack2d(data.weights, data.uParams.numPoles);
                else if (!options.jsonPoles && !packedWeights)
                    data.weights = NumberArray.pack(data.weights);
            }
        }
        if (undefined !== options?.jsonKnots) {
            const packedKnotsU = data.uParams.knots instanceof Float64Array;
            if (options.jsonKnots && packedKnotsU)
                data.uParams.knots = NumberArray.create(data.uParams.knots);
            else if (!options.jsonKnots && !packedKnotsU)
                data.uParams.knots = NumberArray.pack(data.uParams.knots);
            const packedKnotsV = data.vParams.knots instanceof Float64Array;
            if (options.jsonKnots && packedKnotsV)
                data.vParams.knots = NumberArray.create(data.vParams.knots);
            else if (!options.jsonKnots && !packedKnotsV)
                data.vParams.knots = NumberArray.pack(data.vParams.knots);
        }
    }
    /**
     * Process 1-based blocked indices into 0-based indices.
     * @param sourceIndices signed, 1-based, 0-terminated/padded source indices, blocking specified by `numPerBlock`
     * @param numPerBlock index blocking: fixed blocks of size numPerBlock > 1, possibly 0-padded; otherwise, variable-sized blocks terminated by 0
     * @param announceZeroBasedIndex callback to receive a 0-based index and optional flag indicating whether the sign of the source index is positive
     * @param terminateBlock optional callback called after each index block has been announced
    */
    function announceZeroBasedIndicesFromSignedOneBasedIndices(sourceIndices, numPerBlock, announceZeroBasedIndex, terminateBlock) {
        let numIndices = sourceIndices.length;
        if (!numIndices)
            return;
        if (numPerBlock > 1) {
            numIndices -= sourceIndices.length % numPerBlock;
            for (let i = 0; i < numIndices; i++) {
                const p = sourceIndices[i];
                if (p !== 0) // skip padding
                    announceZeroBasedIndex(Math.abs(p) - 1, p > 0);
                if (terminateBlock && ((i + 1) % numPerBlock) === 0)
                    terminateBlock();
            }
        }
        else {
            for (let i = 0; i < numIndices; i++) {
                const p = sourceIndices[i];
                if (p !== 0) // skip terminator
                    announceZeroBasedIndex(Math.abs(p) - 1, p > 0);
                if (terminateBlock) {
                    if (p === 0) {
                        if (i + 1 === numIndices || sourceIndices[i + 1] !== 0) // skip extra terminators
                            terminateBlock();
                    }
                    else {
                        if (i + 1 === numIndices) // missing last terminator
                            terminateBlock();
                    }
                }
            }
        }
    }
    SerializationHelpers.announceZeroBasedIndicesFromSignedOneBasedIndices = announceZeroBasedIndicesFromSignedOneBasedIndices;
    /**
     * Process 0-based indices with blocking specified by another index array.
     * @param sourceIndices 0-based source indices. This array is compressed (has no blocking).
     * @param blockingIndices 1-based source indices, blocking specified by `numPerBlock`. Assumed to have length equal to its zero count plus `sourceIndices.length`.
     * @param numPerBlock index blocking: fixed blocks of size numPerBlock > 1, possibly 0-padded; otherwise, variable-sized blocks terminated by 0
     * @param announceZeroBasedIndex callback to receive a 0-based index
     * @param terminateBlock optional callback called after each index block has been announced
    */
    function announceZeroBasedIndicesWithExternalBlocking(sourceIndices, blockingIndices, numPerBlock, announceZeroBasedIndex, terminateBlock) {
        if (!sourceIndices.length || !blockingIndices.length)
            return;
        const blockingZeroCount = blockingIndices.filter((i) => i === 0).length;
        if (sourceIndices.length + blockingZeroCount !== blockingIndices.length)
            return; // invalid input
        let iSource = 0;
        let numBlocking = blockingIndices.length;
        if (numPerBlock > 1) {
            numBlocking -= blockingIndices.length % numPerBlock;
            for (let iBlocking = 0; iBlocking < numBlocking && iSource < sourceIndices.length; iBlocking++) {
                const p = blockingIndices[iBlocking];
                if (p !== 0) // skip padding
                    announceZeroBasedIndex(sourceIndices[iSource++]);
                if (terminateBlock && ((iBlocking + 1) % numPerBlock) === 0)
                    terminateBlock();
            }
        }
        else {
            for (let iBlocking = 0; iBlocking < numBlocking && iSource < sourceIndices.length; iBlocking++) {
                const p = blockingIndices[iBlocking];
                if (p !== 0) // skip terminator
                    announceZeroBasedIndex(sourceIndices[iSource++]);
                if (terminateBlock) {
                    if (p === 0) {
                        if (iBlocking + 1 === numBlocking || blockingIndices[iBlocking + 1] !== 0) // skip extra terminators
                            terminateBlock();
                    }
                    else {
                        if (iBlocking + 1 === numBlocking) // missing last terminator
                            terminateBlock();
                    }
                }
            }
        }
    }
    SerializationHelpers.announceZeroBasedIndicesWithExternalBlocking = announceZeroBasedIndicesWithExternalBlocking;
    /** Special values for persistent zero-based edge mate indices. */
    let EdgeMateIndex;
    (function (EdgeMateIndex) {
        /** Separates face loops. */
        EdgeMateIndex[EdgeMateIndex["BlockSeparator"] = -1] = "BlockSeparator";
        /** Indicates no edge mate. */
        EdgeMateIndex[EdgeMateIndex["NoEdgeMate"] = -2] = "NoEdgeMate";
    })(EdgeMateIndex = SerializationHelpers.EdgeMateIndex || (SerializationHelpers.EdgeMateIndex = {}));
    ;
    /**
     * Build two index arrays into a source array:
     * * `sourceStarts[k]` is the first index of the k_th block in `sourceIndices`.
     * * `compressedStarts[k]` is the first index of the k_th block in a compressed clone `C` of `sourceIndices`
     * with all pads/terminators removed.
     * * The last entry of `sourceStarts`/`compressedStarts` is the length of `sourceIndices`/`C`.
     * @returns `undefined` if invalid inputs, or the two computed arrays of block start indices.
    */
    function buildBlockStartIndices(sourceIndices, numPerBlock, blockSeparator) {
        if (sourceIndices.length === 0 || sourceIndices[0] === blockSeparator)
            return undefined;
        const sourceStarts = [];
        const compressedStarts = [];
        sourceStarts.push(0);
        compressedStarts.push(0);
        const getBlockLength = (iBlockStart) => {
            if (sourceIndices[iBlockStart] === blockSeparator)
                return 0;
            for (let i = iBlockStart + 1; i < sourceIndices.length; i++) {
                if ((sourceIndices[i - 1] !== blockSeparator && sourceIndices[i] === blockSeparator) || (numPerBlock > 2 && (i % numPerBlock === 0)))
                    return i - iBlockStart;
                if (i === sourceIndices.length - 1) // last fixed block, or unterminated last variable block
                    return sourceIndices.length - iBlockStart;
            }
            return 0;
        };
        for (let i = 0; i < sourceIndices.length;) {
            const blockLength = getBlockLength(i);
            if (blockLength <= 0)
                return undefined;
            i += blockLength;
            for (; i < sourceIndices.length && sourceIndices[i] === blockSeparator; i++)
                ;
            sourceStarts.push(i);
            compressedStarts.push(compressedStarts[compressedStarts.length - 1] + blockLength);
        }
        return { sourceStarts, compressedStarts };
    }
    /**
     * Compress a 0-based blocked index array by removing block separators/pads and remapping each index.
     * * The entries of `sourceIndices` are reflexive indices, i.e., they index `sourceIndices`.
     * * The remapped index `j` must refer to the same block location in the compressed array to which the
     * original index `i >= 0` refers in `sourceIndices`; therefore `j` is obtained from `i` by subtracting the
     * number of block separators/pads preceding `sourceIndices[i]`.
     * @param sourceIndices array of blocked indices to process. Each entry is a 0-based reflexive index, `nullValue`, or `blockSeparator`.
     * @param numPerBlock index blocking for sourceIndices: padded block size > 2 or variable-sized terminated blocks.
     * @param blockSeparator negative value that terminates/pads blocks in sourceIndices, e.g. -1. This value is not announced.
     * @param nullValue negative value that represents "no index" in sourceIndices, e.g., -2. This value is announced as "undefined".
     * @param announceRemappedIndex callback to receive a remapped index.
     * @returns true if and only if the mapping was successful.
     */
    function announceCompressedZeroBasedReflexiveIndices(sourceIndices, numPerBlock, blockSeparator, nullValue, announceRemappedIndex) {
        if (!sourceIndices.length || blockSeparator >= 0 || nullValue >= 0 || (blockSeparator === nullValue))
            return false;
        // remapped index = source index - # preceding terminators/pads in sourceIndices
        // Instead of counting terminators/pads, we use a pair of block start index arrays
        const blocking = buildBlockStartIndices(sourceIndices, numPerBlock, blockSeparator);
        if (!blocking)
            return false;
        for (const index of sourceIndices) {
            if (index === nullValue)
                announceRemappedIndex(undefined);
            else if (index >= 0) {
                const iBlock = NumberArray.searchStrictlyIncreasingNumbers(blocking.sourceStarts, index);
                if (iBlock === undefined)
                    return false;
                const blockOffset = index - blocking.sourceStarts[iBlock];
                announceRemappedIndex(blocking.compressedStarts[iBlock] + blockOffset);
            }
        }
        return true;
    }
    SerializationHelpers.announceCompressedZeroBasedReflexiveIndices = announceCompressedZeroBasedReflexiveIndices;
    /**
     * Uncompress a 0-based index array by inserting block separators and remapping each index.
     * * Defined entries of `sourceIndices` are reflexive indices, i.e., they index `sourceIndices`.
     * * The remapped index `j` must refer to the same block location in the uncompressed array to which the
     * original defined index `i` refers in `sourceIndices`; therefore `j` is obtained from `i` by adding the
     * number of full blocks preceding `sourceIndices[i]`.
     * @param sourceIndices array of compressed indices to process. Each entry is a 0-based reflexive index, or `undefined`.
     * @param sourceStarts sourceStarts[k] is the first index of the k_th block in `sourceIndices`; its last entry is the
     * length of `sourceIndices`.
     * @param blockSeparator negative value that represents an announced block terminator, e.g. -1.
     * @param nullValue negative value to announce for an undefined source index, e.g., -2.
     * @param announceRemappedIndex callback to receive a remapped index.
     * @returns true if and only if the mapping was successful.
     */
    function announceUncompressedZeroBasedReflexiveIndices(sourceIndices, sourceStarts, blockSeparator, nullValue, announceRemappedIndex) {
        if (!sourceIndices.length || sourceStarts.length < 2 || blockSeparator >= 0 || nullValue >= 0 || (blockSeparator === nullValue))
            return false;
        // remapped index = source index + # preceding blocks in sourceIndices
        for (let i = 0; i < sourceStarts.length - 1; i++) {
            for (let j = sourceStarts[i]; j < sourceStarts[i + 1]; j++) {
                const index = sourceIndices[j];
                if (index === undefined)
                    announceRemappedIndex(nullValue);
                else if (index >= 0) {
                    const iBlock = NumberArray.searchStrictlyIncreasingNumbers(sourceStarts, index);
                    if (iBlock === undefined)
                        return false;
                    announceRemappedIndex(index + iBlock);
                }
            }
            announceRemappedIndex(blockSeparator);
        }
        return true;
    }
    SerializationHelpers.announceUncompressedZeroBasedReflexiveIndices = announceUncompressedZeroBasedReflexiveIndices;
    /** Helper class for preparing geometry data for import. */
    class Import {
        /** copy knots, with options to control destination type and extraneous knot removal */
        static copyKnots(knots, options, iStart, iEnd) {
            if (undefined === iStart)
                iStart = 0;
            if (undefined === iEnd)
                iEnd = knots.length;
            if (options?.removeExtraKnots) {
                ++iStart; // ignore start knot
                --iEnd; // ignore end knot
            }
            let newNumKnots = iEnd - iStart;
            if (newNumKnots < 0)
                newNumKnots = 0;
            const newKnots = options?.jsonKnots ? new Array(newNumKnots) : new Float64Array(newNumKnots);
            for (let i = iStart, k = 0; i < iEnd; i++, k++)
                newKnots[k] = knots[i];
            return newKnots;
        }
        /**
         * Recognize the special legacy periodic B-spline data of mode BSplineWrapMode.OpenByRemovingKnots, and return the corresponding modern open clamped knots.
         * * Note that the B-spline poles corresponding to the converted knots remain unchanged, but it is assumed that first and last poles are equal.
         * * Example: the legacy 7-point quadratic circle periodic knots {-1/3 0 0 0 1/3 1/3 2/3 2/3 1 1 1 4/3} are converted to open knots {0 0 1/3 1/3 2/3 2/3 1 1}.
         * * General form of knot vector (k = order, d = k-1 = degree, p = numPoles):
         * * * legacy input: {k/2 periodically extended knots} {start knot multiplicity k} {p-k interior knots} {end knot multiplicity k} {d/2 periodically extended knots}
         * * * converted output: {start knot multiplicity d} {p-k interior knots} {end knot multiplicity d}
         * @param knots classic knot vector to test
         * @param numPoles number of poles
         * @param order B-spline order
         * @param options for output type, extraneous knot removal
         * @returns open knots if legacy periodic B-spline input data is recognized; otherwise, undefined
         * @see Export.closeLegacyPeriodicKnots
         */
        static openLegacyPeriodicKnots(knots, numPoles, order, options) {
            const numKnots = knots.length;
            if (order < 2 || numPoles + 2 * order - 1 !== numKnots)
                return undefined; // not legacy periodic knots
            const startKnot = knots[order - 1];
            const endKnot = knots[numKnots - order];
            const iStart0 = Math.floor(order / 2); // index of first expected multiple of the start knot
            const iEnd0 = iStart0 + numPoles; // index of first expected multiple of the end knot
            const iEnd1 = iEnd0 + order; // one past index of last expected multiple of the end knot
            for (let i = 0; i < order; ++i) {
                if (Math.abs(knots[iStart0 + i] - startKnot) >= KnotVector.knotTolerance)
                    return undefined; // start knot multiplicity too small
                if (Math.abs(knots[iEnd0 + i] - endKnot) >= KnotVector.knotTolerance)
                    return undefined; // end knot multiplicity too small
            }
            return this.copyKnots(knots, options, iStart0, iEnd1);
        }
        /** Prepare imported B-spline curve data for eventual conversion to BSplineCurve3d | BSplineCurve3dH:
         * * Opens legacy "fake" periodic data by expanding knots
         * * Opens true periodic data by expanding poles and weights
         * @param data updated in place. If poles/weights/knots are updated, their respective arrays are reallocated.
         * @param options output specifications
         * @returns whether data was successfully prepared
         */
        static prepareBSplineCurveData(data, options) {
            let polesExpanded;
            let weightsExpanded;
            let knotsCorrected;
            data.params.wrapMode = undefined;
            if (true === data.params.closed) {
                knotsCorrected = this.openLegacyPeriodicKnots(data.params.knots, data.params.numPoles, data.params.order, options);
                if (undefined !== knotsCorrected) {
                    // legacy periodic knots removed, poles untouched
                    data.params.knots = knotsCorrected;
                    data.params.wrapMode = BSplineWrapMode.OpenByRemovingKnots;
                }
                else {
                    // wrap poles, knots untouched
                    if (!polesExpanded) {
                        const arrays = copyBSplineCurveDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesExpanded = arrays.poles;
                        data.weights = weightsExpanded = arrays.weights;
                    }
                    for (let i = 0; i < data.params.order - 1; ++i) {
                        const wraparoundPt = [];
                        for (let j = 0; j < data.dim; ++j)
                            wraparoundPt.push(polesExpanded[i][j]);
                        polesExpanded.push(wraparoundPt); // append degree wraparound poles
                    }
                    if (weightsExpanded) {
                        for (let i = 0; i < data.params.order - 1; ++i)
                            weightsExpanded.push(weightsExpanded[i]); // append degree wraparound weights
                    }
                    data.params.numPoles += data.params.order - 1;
                    data.params.wrapMode = BSplineWrapMode.OpenByAddingControlPoints;
                }
            }
            if (options?.removeExtraKnots) {
                if (!knotsCorrected)
                    data.params.knots = this.copyKnots(data.params.knots, options);
            }
            data.params.closed = undefined; // we are open
            convertBSplineCurveDataArrays(data, options);
            return true;
        }
        /** Prepare imported B-spline surface data for eventual conversion to BSplineSurface3d | BSplineSurface3dH:
         * * Opens legacy "fake" periodic data by expanding knots
         * * Opens true periodic data by expanding poles and weights
         * @param data updated in place. If poles/weights/knots are updated, their respective arrays are reallocated.
         * @param options output specifications
         * @returns whether data was successfully prepared
         */
        static prepareBSplineSurfaceData(data, options) {
            let polesExpanded;
            let weightsExpanded;
            let uKnotsCorrected;
            let vKnotsCorrected;
            data.uParams.wrapMode = data.vParams.wrapMode = undefined;
            if (true === data.uParams.closed) {
                uKnotsCorrected = this.openLegacyPeriodicKnots(data.uParams.knots, data.uParams.numPoles, data.uParams.order, options);
                if (undefined !== uKnotsCorrected) {
                    // legacy periodic knots removed, poles untouched
                    data.uParams.knots = uKnotsCorrected;
                    data.uParams.wrapMode = BSplineWrapMode.OpenByRemovingKnots;
                }
                else {
                    // wrap poles, knots untouched
                    if (!polesExpanded) {
                        const arrays = copyBSplineSurfaceDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesExpanded = arrays.poles;
                        data.weights = weightsExpanded = arrays.weights;
                    }
                    for (let i = 0; i < data.vParams.numPoles; ++i) { // #rows
                        for (let j = 0; j < data.uParams.order - 1; ++j) {
                            const wraparoundPt = [];
                            for (let k = 0; k < data.dim; ++k)
                                wraparoundPt.push(polesExpanded[i][j][k]);
                            polesExpanded[i].push(wraparoundPt); // append degreeU wraparound poles to each row
                        }
                    }
                    if (weightsExpanded) {
                        for (let i = 0; i < data.vParams.numPoles; ++i) // #rows
                            for (let j = 0; j < data.uParams.order - 1; ++j)
                                weightsExpanded[i].push(weightsExpanded[i][j]); // append degreeU wraparound weights to each row
                    }
                    data.uParams.numPoles += data.uParams.order - 1;
                    data.uParams.wrapMode = BSplineWrapMode.OpenByAddingControlPoints;
                }
            }
            if (true === data.vParams.closed) {
                vKnotsCorrected = this.openLegacyPeriodicKnots(data.vParams.knots, data.vParams.numPoles, data.vParams.order, options);
                if (undefined !== vKnotsCorrected) {
                    // legacy periodic knots removed, poles untouched
                    data.vParams.knots = vKnotsCorrected;
                    data.vParams.wrapMode = BSplineWrapMode.OpenByRemovingKnots;
                }
                else {
                    // wrap poles, knots untouched
                    if (!polesExpanded) {
                        const arrays = copyBSplineSurfaceDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesExpanded = arrays.poles;
                        data.weights = weightsExpanded = arrays.weights;
                    }
                    for (let i = 0; i < data.vParams.order - 1; ++i) {
                        const wrapAroundRow = [];
                        for (let j = 0; j < data.uParams.numPoles; ++j) { // #cols
                            const wrapAroundPt = [];
                            for (let k = 0; k < data.dim; ++k)
                                wrapAroundPt.push(polesExpanded[i][j][k]);
                            wrapAroundRow.push(wrapAroundPt);
                        }
                        polesExpanded.push(wrapAroundRow); // append degreeV wraparound rows of poles
                    }
                    if (weightsExpanded) {
                        for (let i = 0; i < data.vParams.order - 1; ++i) {
                            const wrapAroundRow = [];
                            for (let j = 0; j < data.uParams.numPoles; ++j) // #cols
                                wrapAroundRow.push(weightsExpanded[i][j]);
                            weightsExpanded.push(wrapAroundRow); // append degreeV wraparound rows of weights
                        }
                    }
                    data.vParams.numPoles += data.vParams.order - 1;
                    data.vParams.wrapMode = BSplineWrapMode.OpenByAddingControlPoints;
                }
            }
            if (options?.removeExtraKnots) {
                if (!uKnotsCorrected)
                    data.uParams.knots = this.copyKnots(data.uParams.knots, options);
                if (!vKnotsCorrected)
                    data.vParams.knots = this.copyKnots(data.vParams.knots, options);
            }
            data.uParams.closed = data.vParams.closed = undefined; // we are open
            convertBSplineSurfaceDataArrays(data, options);
            return true;
        }
    }
    SerializationHelpers.Import = Import;
    /** Helper class for preparing geometry data for export. */
    class Export {
        /**
         * Restore special legacy periodic B-spline knots opened via BSplineWrapMode.OpenByRemovingKnots logic.
         * @param knots modern knot vector: {start knot multiplicity d} {p-k interior knots} {end knot multiplicity d}
         * @param order B-spline order
         * @param options for output type
         * @param wrapMode wrap mode of the knots
         * @returns legacy periodic knots (with classic extraneous start/end knot) if wrapMode recognized; otherwise, undefined
         * @see Import.openLegacyPeriodicKnots
         */
        static closeLegacyPeriodicKnots(knots, order, options, wrapMode) {
            if (wrapMode === undefined || wrapMode !== BSplineWrapMode.OpenByRemovingKnots)
                return undefined;
            const degree = order - 1;
            const leftIndex = degree - 1;
            const rightIndex = knots.length - degree;
            const leftKnot = knots[leftIndex];
            const rightKnot = knots[rightIndex];
            const knotPeriod = rightKnot - leftKnot;
            const newNumKnots = knots.length + degree + 2;
            const newKnots = options?.jsonKnots ? new Array(newNumKnots) : new Float64Array(newNumKnots);
            let k = 0;
            for (let i = Math.floor(order / 2); i > 0; --i)
                newKnots[k++] = knots[rightIndex - i] - knotPeriod;
            newKnots[k++] = leftKnot; // extraneous start knot
            for (const knot of knots)
                newKnots[k++] = knot;
            newKnots[k++] = rightKnot; // extraneous end knot
            for (let i = 1; i <= Math.floor(degree / 2); ++i)
                newKnots[k++] = knots[leftIndex + i] + knotPeriod;
            return newKnots;
        }
        /**
         * Prepare data from a B-spline curve for export.
         *  * adds classic extraneous knot at start and end of knot vector
         *  * re-closes periodic data based on BSplineWrapMode
         * @param data updated in place. If poles/weights/knots are updated, their respective arrays are reallocated.
         * @param options output specifications
         * @returns whether data was successfully prepared
         */
        static prepareBSplineCurveData(data, options) {
            let polesTrimmed;
            let weightsTrimmed;
            let knotsCorrected;
            data.params.closed = undefined;
            switch (data.params.wrapMode) {
                case BSplineWrapMode.OpenByRemovingKnots: {
                    // add legacy periodic and extraneous knots, poles untouched
                    knotsCorrected = this.closeLegacyPeriodicKnots(data.params.knots, data.params.order, options, data.params.wrapMode);
                    if (undefined === knotsCorrected)
                        return false; // invalid input
                    data.params.knots = knotsCorrected;
                    data.params.closed = true;
                    break;
                }
                case BSplineWrapMode.OpenByAddingControlPoints: {
                    // unwrap poles, knots untouched
                    if (!polesTrimmed) {
                        const arrays = copyBSplineCurveDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesTrimmed = arrays.poles;
                        data.weights = weightsTrimmed = arrays.weights;
                    }
                    for (let i = 0; i < data.params.order - 1; ++i)
                        polesTrimmed.pop(); // remove last degree poles
                    if (weightsTrimmed) {
                        for (let i = 0; i < data.params.order - 1; ++i)
                            weightsTrimmed.pop(); // remove last degree weights
                    }
                    data.params.numPoles -= data.params.order - 1;
                    data.params.closed = true;
                    break;
                }
            }
            // always add extraneous knots
            if (!knotsCorrected)
                data.params.knots = KnotVector.copyKnots(data.params.knots, data.params.order - 1, true, data.params.wrapMode);
            convertBSplineCurveDataArrays(data, options);
            return true;
        }
        /**
         * Prepare data from a B-spline surface for export.
         *  * adds classic extraneous knot at start and end of knot vectors
         *  * re-closes periodic data based on BSplineWrapMode
         * @param data updated in place. If poles/weights/knots are updated, their respective arrays are reallocated.
         * @param options output specifications
         * @returns whether data was successfully prepared
         */
        static prepareBSplineSurfaceData(data, options) {
            let polesTrimmed;
            let weightsTrimmed;
            let uKnotsCorrected;
            let vKnotsCorrected;
            data.uParams.closed = data.vParams.closed = undefined;
            switch (data.uParams.wrapMode) {
                case BSplineWrapMode.OpenByRemovingKnots: {
                    // add legacy periodic and extraneous knots, poles untouched
                    uKnotsCorrected = this.closeLegacyPeriodicKnots(data.uParams.knots, data.uParams.order, options, data.uParams.wrapMode);
                    if (undefined === uKnotsCorrected)
                        return false; // invalid input
                    data.uParams.knots = uKnotsCorrected;
                    data.uParams.closed = true;
                    break;
                }
                case BSplineWrapMode.OpenByAddingControlPoints: {
                    // unwrap poles, knots untouched
                    if (!polesTrimmed) {
                        const arrays = copyBSplineSurfaceDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesTrimmed = arrays.poles;
                        data.weights = weightsTrimmed = arrays.weights;
                    }
                    for (let i = 0; i < data.vParams.numPoles; ++i) // #rows
                        for (let j = 0; j < data.uParams.order - 1; ++j)
                            polesTrimmed[i].pop(); // remove last degreeU poles from each row
                    if (weightsTrimmed) {
                        for (let i = 0; i < data.vParams.numPoles; ++i) // #rows
                            for (let j = 0; j < data.uParams.order - 1; ++j)
                                weightsTrimmed[i].pop(); // remove last degreeU weights from each row
                    }
                    data.uParams.numPoles -= data.uParams.order - 1;
                    data.uParams.closed = true;
                    break;
                }
            }
            switch (data.vParams.wrapMode) {
                case BSplineWrapMode.OpenByRemovingKnots: {
                    // add legacy periodic and extraneous knots, poles untouched
                    vKnotsCorrected = this.closeLegacyPeriodicKnots(data.vParams.knots, data.vParams.order, options, data.vParams.wrapMode);
                    if (undefined === vKnotsCorrected)
                        return false; // invalid input
                    data.vParams.knots = vKnotsCorrected;
                    data.vParams.closed = true;
                    break;
                }
                case BSplineWrapMode.OpenByAddingControlPoints: {
                    // unwrap poles, knots untouched
                    if (!polesTrimmed) {
                        const arrays = copyBSplineSurfaceDataPoles(data);
                        if (undefined === arrays.poles)
                            return false; // invalid input
                        data.poles = polesTrimmed = arrays.poles;
                        data.weights = weightsTrimmed = arrays.weights;
                    }
                    for (let i = 0; i < data.vParams.order - 1; ++i)
                        polesTrimmed.pop(); // remove last degreeV rows of poles
                    if (weightsTrimmed) {
                        for (let i = 0; i < data.vParams.order - 1; ++i)
                            weightsTrimmed.pop(); // remove last degreeV rows of weights
                    }
                    data.vParams.numPoles -= data.vParams.order - 1;
                    data.vParams.closed = true;
                    break;
                }
            }
            // always add extraneous knots
            if (!uKnotsCorrected)
                data.uParams.knots = KnotVector.copyKnots(data.uParams.knots, data.uParams.order - 1, true, data.uParams.wrapMode);
            if (!vKnotsCorrected)
                data.vParams.knots = KnotVector.copyKnots(data.vParams.knots, data.vParams.order - 1, true, data.vParams.wrapMode);
            convertBSplineSurfaceDataArrays(data, options);
            return true;
        }
    }
    SerializationHelpers.Export = Export;
})(SerializationHelpers || (SerializationHelpers = {}));
//# sourceMappingURL=SerializationHelpers.js.map