library ShackledGenesis {
    uint256 constant MAX_N_ATTEMPTS = 150; // max number of attempts to find a valid triangle
    int256 constant ROT_XY_MAX = 12; // max amount of rotation in xy plane
    int256 constant MAX_CANVAS_SIZE = 32000; // max size of canvas

    /// a struct to hold vars in makeFacesVertsCols() to prevent StackTooDeep
    struct FacesVertsCols {
        uint256[3][] faces;
        int256[3][] verts;
        int256[3][] cols;
        uint256 nextColIdx;
        uint256 nextVertIdx;
        uint256 nextFaceIdx;
    }

    /** @dev generate all parameters required for the shackled renderer from a seed hash
    @param tokenHash a hash of the tokenId to be used in 'random' number generation
    */
    function generateGenesisPiece(bytes32 tokenHash)
        external
        view
        returns (
            ShackledStructs.RenderParams memory renderParams,
            ShackledStructs.Metadata memory metadata
        )
    {
        /// initial model paramaters
        renderParams.objScale = 1;
        renderParams.objPosition = [int256(0), 0, -2500];

        /// generate the geometry and colors
        (
            FacesVertsCols memory vars,
            ColorUtils.ColScheme memory colScheme,
            GeomUtils.GeomSpec memory geomSpec,
            GeomUtils.GeomVars memory geomVars
        ) = generateGeometryAndColors(tokenHash, renderParams.objPosition);

        renderParams.faces = vars.faces;
        renderParams.verts = vars.verts;
        renderParams.cols = vars.cols;

        /// use a perspective camera
        renderParams.perspCamera = true;

        if (geomSpec.id == 3) {
            renderParams.wireframe = false;
            renderParams.backfaceCulling = true;
        } else {
            /// determine wireframe trait (5% chance)
            if (GeomUtils.randN(tokenHash, "wireframe", 1, 100) > 95) {
                renderParams.wireframe = true;
                renderParams.backfaceCulling = false;
            } else {
                renderParams.wireframe = false;
                renderParams.backfaceCulling = true;
            }
        }

        if (
            colScheme.id == 2 ||
            colScheme.id == 3 ||
            colScheme.id == 7 ||
            colScheme.id == 8
        ) {
            renderParams.invert = false;
        } else {
            /// inversion (40% chance)
            renderParams.invert =
                GeomUtils.randN(tokenHash, "invert", 1, 10) > 6;
        }

        /// background colors
        renderParams.backgroundColor = [
            colScheme.bgColTop,
            colScheme.bgColBottom
        ];

        /// lighting parameters
        renderParams.lightingParams = ShackledStructs.LightingParams({
            applyLighting: true,
            lightAmbiPower: 0,
            lightDiffPower: 2000,
            lightSpecPower: 3000,
            inverseShininess: 10,
            lightColSpec: colScheme.lightCol,
            lightColDiff: colScheme.lightCol,
            lightColAmbi: colScheme.lightCol,
            lightPos: [int256(-50), 0, 0]
        });

        /// create the metadata
        metadata.colorScheme = colScheme.name;
        metadata.geomSpec = geomSpec.name;
        metadata.nPrisms = geomVars.nPrisms;

        if (geomSpec.isSymmetricX) {
            if (geomSpec.isSymmetricY) {
                metadata.pseudoSymmetry = "Diagonal";
            } else {
                metadata.pseudoSymmetry = "Horizontal";
            }
        } else if (geomSpec.isSymmetricY) {
            metadata.pseudoSymmetry = "Vertical";
        } else {
            metadata.pseudoSymmetry = "Scattered";
        }

        if (renderParams.wireframe) {
            metadata.wireframe = "Enabled";
        } else {
            metadata.wireframe = "Disabled";
        }

        if (renderParams.invert) {
            metadata.inversion = "Enabled";
        } else {
            metadata.inversion = "Disabled";
        }
    }

    /** @dev run a generative algorithm to create 3d geometries (prisms) and colors to render with Shackled
    also returns the faces and verts, which can be used to build a .obj file for in-browser rendering
     */
    function generateGeometryAndColors(
        bytes32 tokenHash,
        int256[3] memory objPosition
    )
        internal
        view
        returns (
            FacesVertsCols memory vars,
            ColorUtils.ColScheme memory colScheme,
            GeomUtils.GeomSpec memory geomSpec,
            GeomUtils.GeomVars memory geomVars
        )
    {
        /// get this geom's spec
        geomSpec = GeomUtils.generateSpec(tokenHash);

        /// create the triangles
        (
            int256[3][3][] memory tris,
            int256[] memory zFronts,
            int256[] memory zBacks
        ) = create2dTris(tokenHash, geomSpec);

        /// prismify
        geomVars = prismify(tokenHash, tris, zFronts, zBacks);

        /// generate colored faces
        /// get a color scheme
        colScheme = ColorUtils.getScheme(tokenHash, tris);

        /// get faces, verts and colors
        vars = makeFacesVertsCols(
            tokenHash,
            tris,
            geomVars,
            colScheme,
            objPosition
        );
    }

    /** @dev 'randomly' create an array of 2d triangles that will define each eventual 3d prism  */
    function create2dTris(bytes32 tokenHash, GeomUtils.GeomSpec memory geomSpec)
        internal
        view
        returns (
            int256[3][3][] memory, /// tris
            int256[] memory, /// zFronts
            int256[] memory /// zBacks
        )
    {
        /// initiate vars that will be used to store the triangle info
        GeomUtils.TriVars memory triVars;
        triVars.tris = new int256[3][3][]((geomSpec.maxPrisms + 5) * 2);
        triVars.zFronts = new int256[]((geomSpec.maxPrisms + 5) * 2);
        triVars.zBacks = new int256[]((geomSpec.maxPrisms + 5) * 2);

        /// 'randomly' initiate the starting radius
        int256 initialSize;

        if (geomSpec.forceInitialSize == 0) {
            initialSize = GeomUtils.randN(
                tokenHash,
                "size",
                geomSpec.minTriRad,
                geomSpec.maxTriRad
            );
        } else {
            initialSize = geomSpec.forceInitialSize;
        }

        /// 50% chance of 30deg rotation, 50% chance of 210deg rotation
        int256 initialRot = GeomUtils.randN(tokenHash, "rot", 0, 1) == 0
            ? int256(30)
            : int256(210);

        /// create the first triangle
        int256[3][3] memory currentTri = GeomUtils.makeTri(
            [int256(0), 0, 0],
            initialSize,
            initialRot
        );

        /// save it
        triVars.tris[0] = currentTri;

        /// calculate the first triangle's zs
        triVars.zBacks[0] = GeomUtils.calculateZ(
            currentTri,
            tokenHash,
            triVars.nextTriIdx,
            geomSpec,
            false
        );
        triVars.zFronts[0] = GeomUtils.calculateZ(
            currentTri,
            tokenHash,
            triVars.nextTriIdx,
            geomSpec,
            true
        );

        /// get the position to add the next triangle

        if (geomSpec.isSymmetricY) {
            /// override the first tri, since it is not symmetrical
            /// but temporarily save it as its needed as a reference tri
            triVars.nextTriIdx = 0;
        } else {
            triVars.nextTriIdx = 1;
        }

        /// make new triangles
        for (uint256 i = 0; i < MAX_N_ATTEMPTS; i++) {
            /// get a reference to a previous triangle
            uint256 refIdx = uint256(
                GeomUtils.randN(
                    tokenHash,
                    string(abi.encodePacked("refIdx", i)),
                    0,
                    int256(triVars.nextTriIdx) - 1
                )
            );

            /// ensure that the 'random' number generated is different in each while loop
            /// by incorporating the nAttempts and nextTriIdx into the seed modifier
            if (
                GeomUtils.randN(
                    tokenHash,
                    string(abi.encodePacked("adj", i, triVars.nextTriIdx)),
                    0,
                    100
                ) <= geomSpec.probVertOpp
            ) {
                /// attempt to recursively add vertically opposite triangles
                triVars = GeomUtils.makeVerticallyOppositeTriangles(
                    tokenHash,
                    i, // attemptNum (to create unique random seeds)
                    refIdx,
                    triVars,
                    geomSpec,
                    -1,
                    -1,
                    0 // depth (to create unique random seeds within recursion)
                );
            } else {
                /// attempt to recursively add adjacent triangles
                triVars = GeomUtils.makeAdjacentTriangles(
                    tokenHash,
                    i, // attemptNum (to create unique random seeds)
                    refIdx,
                    triVars,
                    geomSpec,
                    -1,
                    -1,
                    0 // depth (to create unique random seeds within recursion)
                );
            }

            /// can't have this many triangles
            if (triVars.nextTriIdx >= geomSpec.maxPrisms) {
                break;
            }
        }

        /// clip all the arrays to the actual number of triangles
        triVars.tris = GeomUtils.clipTrisToLength(
            triVars.tris,
            triVars.nextTriIdx
        );
        triVars.zBacks = GeomUtils.clipZsToLength(
            triVars.zBacks,
            triVars.nextTriIdx
        );
        triVars.zFronts = GeomUtils.clipZsToLength(
            triVars.zFronts,
            triVars.nextTriIdx
        );

        return (triVars.tris, triVars.zBacks, triVars.zFronts);
    }

    /** @dev prismify the initial 2d triangles output */
    function prismify(
        bytes32 tokenHash,
        int256[3][3][] memory tris,
        int256[] memory zFronts,
        int256[] memory zBacks
    ) internal view returns (GeomUtils.GeomVars memory) {
        /// initialise a struct to hold the vars we need
        GeomUtils.GeomVars memory geomVars;

        /// record the num of prisms
        geomVars.nPrisms = uint256(tris.length);

        /// figure out what point to put in the middle
        geomVars.extents = GeomUtils.getExtents(tris); // mins[3], maxs[3]

        /// scale the tris to fit in the canvas
        geomVars.width = geomVars.extents[1][0] - geomVars.extents[0][0];
        geomVars.height = geomVars.extents[1][1] - geomVars.extents[0][1];
        geomVars.extent = ShackledMath.max(geomVars.width, geomVars.height);
        geomVars.scaleNum = 2000;

        /// multiple all tris by the scale, then divide by the extent
        for (uint256 i = 0; i < tris.length; i++) {
            tris[i] = [
                ShackledMath.vector3DivScalar(
                    ShackledMath.vector3MulScalar(
                        tris[i][0],
                        geomVars.scaleNum
                    ),
                    geomVars.extent
                ),
                ShackledMath.vector3DivScalar(
                    ShackledMath.vector3MulScalar(
                        tris[i][1],
                        geomVars.scaleNum
                    ),
                    geomVars.extent
                ),
                ShackledMath.vector3DivScalar(
                    ShackledMath.vector3MulScalar(
                        tris[i][2],
                        geomVars.scaleNum
                    ),
                    geomVars.extent
                )
            ];
        }

        /// we may like to do some rotation, this means we get the shapes in the middle
        /// arrow up, down, left, right

        // 50% chance of x, y rotation being positive or negative
        geomVars.rotX = (GeomUtils.randN(tokenHash, "rotX", 0, 1) == 0)
            ? ROT_XY_MAX
            : -ROT_XY_MAX;

        geomVars.rotY = (GeomUtils.randN(tokenHash, "rotY", 0, 1) == 0)
            ? ROT_XY_MAX
            : -ROT_XY_MAX;

        // 50% chance to z rotation being 0 or 30
        geomVars.rotZ = (GeomUtils.randN(tokenHash, "rotZ", 0, 1) == 0)
            ? int256(0)
            : int256(30);

        /// rotate all tris around facing (z) axis
        for (uint256 i = 0; i < tris.length; i++) {
            tris[i] = GeomUtils.triRotHelp(2, tris[i], geomVars.rotZ);
        }

        geomVars.trisBack = GeomUtils.copyTris(tris);
        geomVars.trisFront = GeomUtils.copyTris(tris);

        /// front triangles need to come forward, back triangles need to go back
        for (uint256 i = 0; i < tris.length; i++) {
            for (uint256 j = 0; j < 3; j++) {
                for (uint256 k = 0; k < 3; k++) {
                    if (k == 2) {
                        /// get the z values (make sure the scale is applied)
                        geomVars.trisFront[i][j][k] = zFronts[i];
                        geomVars.trisBack[i][j][k] = zBacks[i];
                    } else {
                        /// copy the x and y values
                        geomVars.trisFront[i][j][k] = tris[i][j][k];
                        geomVars.trisBack[i][j][k] = tris[i][j][k];
                    }
                }
            }
        }

        /// rotate - order is import here (must come after prism splitting, and is dependant on z rotation)
        if (geomVars.rotZ == 0) {
            /// x then y
            (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
                0,
                geomVars.trisBack,
                geomVars.trisFront,
                geomVars.rotX
            );
            (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
                1,
                geomVars.trisBack,
                geomVars.trisFront,
                geomVars.rotY
            );
        } else {
            /// y then x
            (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
                1,
                geomVars.trisBack,
                geomVars.trisFront,
                geomVars.rotY
            );
            (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
                0,
                geomVars.trisBack,
                geomVars.trisFront,
                geomVars.rotX
            );
        }

        return geomVars;
    }

    /** @dev create verts and faces out of the geom and get their colors */
    function makeFacesVertsCols(
        bytes32 tokenHash,
        int256[3][3][] memory tris,
        GeomUtils.GeomVars memory geomVars,
        ColorUtils.ColScheme memory scheme,
        int256[3] memory objPosition
    ) internal view returns (FacesVertsCols memory vars) {
        /// the tris defined thus far are those at the front of each prism
        /// we need to calculate how many tris will then be in the final prisms (3 sides have 2 tris each, plus the front tri, = 7)
        uint256 numTrisPrisms = tris.length * 7; /// 7 tris per 3D prism (not inc. back)

        vars.faces = new uint256[3][](numTrisPrisms); /// array that holds indexes of verts needed to make each final triangle
        vars.verts = new int256[3][](tris.length * 6); /// the vertices for all final triangles
        vars.cols = new int256[3][](tris.length * 6); /// 1 col per final tri
        vars.nextColIdx = 0;
        vars.nextVertIdx = 0;
        vars.nextFaceIdx = 0;

        /// get some number of highlight triangles
        geomVars.hltPrismIdx = ColorUtils.getHighlightPrismIdxs(
            tris,
            tokenHash,
            scheme.hltNum,
            scheme.hltVarCode,
            scheme.hltSelCode
        );

        int256[3][2] memory frontExtents = GeomUtils.getExtents(
            geomVars.trisFront
        ); // mins[3], maxs[3]
        int256[3][2] memory backExtents = GeomUtils.getExtents(
            geomVars.trisBack
        ); // mins[3], maxs[3]
        int256[3][2] memory meanExtents = [
            [
                (frontExtents[0][0] + backExtents[0][0]) / 2,
                (frontExtents[0][1] + backExtents[0][1]) / 2,
                (frontExtents[0][2] + backExtents[0][2]) / 2
            ],
            [
                (frontExtents[1][0] + backExtents[1][0]) / 2,
                (frontExtents[1][1] + backExtents[1][1]) / 2,
                (frontExtents[1][2] + backExtents[1][2]) / 2
            ]
        ];

        /// apply translations such that we're at the center
        geomVars.center = ShackledMath.vector3DivScalar(
            ShackledMath.vector3Add(meanExtents[0], meanExtents[1]),
            2
        );

        geomVars.center[2] = 0;

        for (uint256 i = 0; i < tris.length; i++) {
            int256[3][6] memory prismCols;
            ColorUtils.SubScheme memory subScheme = ColorUtils.inArray(
                geomVars.hltPrismIdx,
                i
            )
                ? scheme.hlt
                : scheme.pri;

            /// get the colors for the prism
            prismCols = ColorUtils.getColForPrism(
                tokenHash,
                geomVars.trisFront[i],
                subScheme,
                meanExtents
            );

            /// save the colors (6 per prism)
            for (uint256 j = 0; j < 6; j++) {
                vars.cols[vars.nextColIdx] = prismCols[j];
                vars.nextColIdx++;
            }

            /// add 3 points (back)
            for (uint256 j = 0; j < 3; j++) {
                vars.verts[vars.nextVertIdx] = [
                    geomVars.trisBack[i][j][0],
                    geomVars.trisBack[i][j][1],
                    -geomVars.trisBack[i][j][2] /// flip the Z
                ];
                vars.nextVertIdx += 1;
            }

            /// add 3 points (front)
            for (uint256 j = 0; j < 3; j++) {
                vars.verts[vars.nextVertIdx] = [
                    geomVars.trisFront[i][j][0],
                    geomVars.trisFront[i][j][1],
                    -geomVars.trisFront[i][j][2] /// flip the Z
                ];
                vars.nextVertIdx += 1;
            }

            /// create the faces
            uint256 ii = i * 6;

            /// the orders are all important here (back is not visible)

            /// front
            vars.faces[vars.nextFaceIdx] = [ii + 3, ii + 4, ii + 5];

            /// side 1 flat
            vars.faces[vars.nextFaceIdx + 1] = [ii + 4, ii + 3, ii + 0];
            vars.faces[vars.nextFaceIdx + 2] = [ii + 0, ii + 1, ii + 4];

            /// side 2 rhs
            vars.faces[vars.nextFaceIdx + 3] = [ii + 5, ii + 4, ii + 1];
            vars.faces[vars.nextFaceIdx + 4] = [ii + 1, ii + 2, ii + 5];

            /// side 3 lhs
            vars.faces[vars.nextFaceIdx + 5] = [ii + 2, ii + 0, ii + 3];
            vars.faces[vars.nextFaceIdx + 6] = [ii + 3, ii + 5, ii + 2];

            vars.nextFaceIdx += 7;
        }

        for (uint256 i = 0; i < vars.verts.length; i++) {
            vars.verts[i] = ShackledMath.vector3Sub(
                vars.verts[i],
                geomVars.center
            );
        }
    }
}
/*
 dir codes:
 0: right-left
 1: left-right
 2: up-down
 3: down-up
 
 sel codes:
 0: random
 1: biggest-first
 2: smallest-first
 */
function prismify(
    bytes32 tokenHash,
    int256[3][3][] memory tris,
    int256[] memory zFronts,
    int256[] memory zBacks
) internal view returns (GeomUtils.GeomVars memory) {
    /// initialise a struct to hold the vars we need
    GeomUtils.GeomVars memory geomVars;

    /// record the num of prisms
    geomVars.nPrisms = uint256(tris.length);

    /// figure out what point to put in the middle
    geomVars.extents = GeomUtils.getExtents(tris); // mins[3], maxs[3]

    /// scale the tris to fit in the canvas
    geomVars.width = geomVars.extents[1][0] - geomVars.extents[0][0];
    geomVars.height = geomVars.extents[1][1] - geomVars.extents[0][1];
    geomVars.extent = ShackledMath.max(geomVars.width, geomVars.height);
    geomVars.scaleNum = 2000;

    /// multiple all tris by the scale, then divide by the extent
    for (uint256 i = 0; i < tris.length; i++) {
        tris[i] = [
            ShackledMath.vector3DivScalar(
                ShackledMath.vector3MulScalar(
                    tris[i][0],
                    geomVars.scaleNum
                ),
                geomVars.extent
            ),
            ShackledMath.vector3DivScalar(
                ShackledMath.vector3MulScalar(
                    tris[i][1],
                    geomVars.scaleNum
                ),
                geomVars.extent
            ),
            ShackledMath.vector3DivScalar(
                ShackledMath.vector3MulScalar(
                    tris[i][2],
                    geomVars.scaleNum
                ),
                geomVars.extent
            )
        ];
    }

    /// we may like to do some rotation, this means we get the shapes in the middle
    /// arrow up, down, left, right

    // 50% chance of x, y rotation being positive or negative
    geomVars.rotX = (GeomUtils.randN(tokenHash, "rotX", 0, 1) == 0)
        ? ROT_XY_MAX
        : -ROT_XY_MAX;

    geomVars.rotY = (GeomUtils.randN(tokenHash, "rotY", 0, 1) == 0)
        ? ROT_XY_MAX
        : -ROT_XY_MAX;

    // 50% chance to z rotation being 0 or 30
    geomVars.rotZ = (GeomUtils.randN(tokenHash, "rotZ", 0, 1) == 0)
        ? int256(0)
        : int256(30);

    /// rotate all tris around facing (z) axis
    for (uint256 i = 0; i < tris.length; i++) {
        tris[i] = GeomUtils.triRotHelp(2, tris[i], geomVars.rotZ);
    }

    geomVars.trisBack = GeomUtils.copyTris(tris);
    geomVars.trisFront = GeomUtils.copyTris(tris);

    /// front triangles need to come forward, back triangles need to go back
    for (uint256 i = 0; i < tris.length; i++) {
        for (uint256 j = 0; j < 3; j++) {
            for (uint256 k = 0; k < 3; k++) {
                if (k == 2) {
                    /// get the z values (make sure the scale is applied)
                    geomVars.trisFront[i][j][k] = zFronts[i];
                    geomVars.trisBack[i][j][k] = zBacks[i];
                } else {
                    /// copy the x and y values
                    geomVars.trisFront[i][j][k] = tris[i][j][k];
                    geomVars.trisBack[i][j][k] = tris[i][j][k];
                }
            }
        }
    }

    /// rotate - order is import here (must come after prism splitting, and is dependant on z rotation)
    if (geomVars.rotZ == 0) {
        /// x then y
        (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
            0,
            geomVars.trisBack,
            geomVars.trisFront,
            geomVars.rotX
        );
        (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
            1,
            geomVars.trisBack,
            geomVars.trisFront,
            geomVars.rotY
        );
    } else {
        /// y then x
        (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
            1,
            geomVars.trisBack,
            geomVars.trisFront,
            geomVars.rotY
        );
        (geomVars.trisBack, geomVars.trisFront) = GeomUtils.triBfHelp(
            0,
            geomVars.trisBack,
            geomVars.trisFront,
            geomVars.rotX
        );
    }

    return geomVars;
}

/** @dev prismify the initial 2d triangles output */