//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../interfaces/IN.sol";
import "./SeedPhrasePricing.sol";
import "../libraries/Randomize.sol";
import "../libraries/NilProtocolUtils.sol";
import "../libraries/SeedPhraseUtils.sol";

/**
 * @title Seed Phrases
 * @author maximonee (twitter.com/maximonee_)
 * @notice This contract provides minting for the Seed Phrases NFT by Sean Elliott (twitter.com/seanelliottoc)
 */
contract SeedPhrase is SeedPhrasePricing {
    using Strings for uint256;
    using Strings for uint16;
    using Strings for uint8;
    using Counters for Counters.Counter;
    using Randomize for Randomize.Random;

    Counters.Counter private _tokenIds;
    Counters.Counter private _doublePanelTokens;

    // TODO: Check if this is the correct way to store double tokens
    // Idea is to map double panel tokens (let's say they go from 3000 - 3100)
    // In the tokenUri we can check if a token is >= 3000
    // If it is check that it exists in this mapping which should contain the original two (now burned tokens)
    mapping(uint256 => uint256[2]) burnedTokensPairings;

    event Burnt(address to, uint256 firstBurntToken, uint256 secondBurntToken, uint256 doublePaneledToken);

    DerivativeParameters params = DerivativeParameters(false, false, 0, 2048, 4);

    constructor(
        address _n,
        address masterMint,
        address dao
    ) SeedPhrasePricing("NFT #1337", "LEET", IN(_n), params, 40000000000000000, 80000000000000000, masterMint, dao) {
        // Start token IDs at 1
        _tokenIds.increment();

        preSaleLimits[PreSaleType.N] = 1044;
        preSaleLimits[PreSaleType.GenesisSketch] = 40;
        preSaleLimits[PreSaleType.GM] = 500;
    }

    mapping(address => bool) genesisSketchAddresses;
    mapping(address => bool) gmAddresses;
    mapping(PreSaleType => uint256) preSaleLimits;

    bool public preSaleActive = false;
    bool public publicSaleActive = false;
    bool public isSaleHalted = false;

    uint8 public constant MAX_PUBLIC_MINT = 8;
    uint8 public constant MAX_BURNS = 100;
    uint16 public constant OWNER_LIMIT = 4;

    // Time TBD
    uint32 preSaleLaunchTime = 1633723200;
    // Time TBD
    uint32 publicSaleLaunchTime = 1633723200;

    // NFT creation vars
    uint8 constant gridSize = 16;
    uint16 constant viewBox = 1600;
    uint16 constant segmentSize = 100;
    uint16 constant radius = 50;
    uint16 constant r = 40;
    uint16 constant padding = 10;
    uint16 constant panelWidth = segmentSize * 4;
    uint16 constant panelHeight = segmentSize * 10;
    uint16 constant singlePanelX = (segmentSize * 6);
    uint16 constant doublePanel1X = (segmentSize * 3);
    uint16 constant doublePanel2X = doublePanel1X + (segmentSize * 6);
    uint16 constant panelY = (segmentSize * 3);
    uint8 constant strokeWeight = 7;

    // Map token to their unique seed
    mapping(uint256 => bytes32) internal tokenSeed;

    struct Colors {
        string background;
        string panel;
        string panelStroke;
        string selectedCircleStroke;
        string selectedCircleFill;
        string negativeCircleStroke;
        string negativeCircleFill;
        string blackOrWhite;
        string bOrWStroke;
    }

    struct Attrs {
        bool showStroke;
        bool showPanel;
        bool backgroundCircles;
        bool showGrid;
    }

    enum PreSaleType {
        N,
        GenesisSketch,
        GM,
        None
    }

    function getTokenSeed(uint256 tokenId) public view returns (bytes32) {
        return tokenSeed[tokenId];
    }

    function tokenSVG(uint256 tokenId) public view virtual returns (string memory, string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return _render(tokenId, getTokenSeed(tokenId));
    }

    function tokenSVG(uint256 tokenId) public view virtual returns (string memory svg, bytes memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        Randomize.Random memory random = Randomize.Random({ seed: uint256(getTokenSeed(tokenId)), offsetBit: 0 });
        (SeedPhraseUtils.Attrs memory attributes, bytes memory traits) = tokenAttributes(tokenId, random);

        return (_render(tokenId, random, attributes), traits);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        SeedPhraseUtils.Attrs memory attributes = tokenAttributes(tokenId);

        // We might also want to return the attributes string from tokenSVG also (as all the info is already in there)
        (string memory output, bytes memory traits) = tokenSVG(tokenId);

        string memory json = NilProtocolUtils.base64encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "Seed Phrases #',
                        NilProtocolUtils.stringify(tokenId),
                        '", "image": "data:image/svg+xml;base64,',
                        NilProtocolUtils.base64encode(bytes(output)),
                        '", "attributes": ',
                        traits,
                        "}"
                    )
                )
            )
        );
        output = string(abi.encodePacked("data:application/json;base64,", json));

        return output;
    }

    /**
    Updates the presale state for n holders
     */
    function setPreSaleState(bool _preSaleActiveState) public onlyAdmin {
        preSaleActive = _preSaleActiveState;
    }

    /**
    Updates the public sale state for non-n holders
     */
    function setPublicSaleState(bool _publicSaleActiveState) public onlyAdmin {
        publicSaleActive = _publicSaleActiveState;
    }

    /**
    Give the ability to halt the sale if necessary due to automatic sale enablement based on time
     */
    function setSaleHaltedState(bool _saleHaltedState) public onlyAdmin {
        isSaleHalted = _saleHaltedState;
    }

    function setGenesisSketchAllowList(address[] calldata addresses) external onlyAdmin {
        for (uint256 i = 0; i < addresses.length; i++) {
            genesisSketchAddresses[addresses[i]] = true;
        }
    }

    function setGmAllowList(address[] calldata addresses) external onlyAdmin {
        for (uint256 i = 0; i < addresses.length; i++) {
            gmAddresses[addresses[i]] = true;
        }
    }

    function isPreSaleActive() public view returns (bool) {
        if ((block.timestamp >= preSaleLaunchTime || preSaleActive) && !isSaleHalted) {
            return true;
        }

        return false;
    }

    function isPublicSaleActive() public view returns (bool) {
        if ((block.timestamp >= publicSaleLaunchTime || publicSaleActive) && !isSaleHalted) {
            return true;
        }

        return false;
    }

    function canMintPresale(address addr, uint256 amount) internal view returns (bool, PreSaleType) {
        if (genesisSketchAddresses[addr] && preSaleLimits[PreSaleType.GenesisSketch] - amount > 0) {
            return (true, PreSaleType.GenesisSketch);
        }

        if (gmAddresses[addr] && preSaleLimits[PreSaleType.GM] - amount > 0) {
            return (true, PreSaleType.GM);
        }

        if (n.balanceOf(addr) > 0 && preSaleLimits[PreSaleType.N] - amount > 0) {
            return (true, PreSaleType.N);
        }

        return (false, PreSaleType.None);
    }

    /**
     * @notice Allow a n token holder to bulk mint tokens with id of their n tokens' id
     * @param recipient Recipient of the mint
     * @param tokenIds Ids to be minted
     * @param paid Amount paid for the mint
     */
    function mintWithN(
        address recipient,
        uint256[] calldata tokenIds,
        uint256 paid
    ) public virtual override nonReentrant {
        uint256 maxTokensToMint = tokenIds.length;
        (bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, maxTokensToMint);

        require(isPreSaleActive() && preSaleEligible, "SeedPhrase:SALE_NOT_ACTIVE");
        require(
            balanceOf(recipient) + maxTokensToMint <= _getMaxMintPerWallet(),
            "NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE"
        );
        require(totalSupply() + maxTokensToMint <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");

        require(paid == getNextPriceForNHoldersInWei(maxTokensToMint), "NilPass:INVALID_PRICE");

        for (uint256 i = 0; i < maxTokensToMint; i++) {
            uint256 tokenId = _tokenIds.current();

            tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);

            _safeMint(recipient, tokenId);
            _tokenIds.increment();
        }

        if (preSaleEligible) {
            preSaleLimits[presaleType] -= maxTokensToMint;
        }
    }

    /**
     * @notice Allow anyone to mint a token with the supply id if this pass is unrestricted.
     *         n token holders can use this function without using the n token holders allowance,
     *         this is useful when the allowance is fully utilized.
     * @param recipient Recipient of the mint
     * @param amount Amount of tokens to mint
     * @param paid Amount paid for the mint
     */
    function mint(
        address recipient,
        uint8 amount,
        uint256 paid
    ) public virtual override nonReentrant {
        (bool preSaleEligible, PreSaleType presaleType) = canMintPresale(recipient, amount);

        require(isPublicSaleActive() || (isPreSaleActive() && preSaleEligible), "SeedPhrase:SALE_NOT_ACTIVE");
        require(!params.supportsTokenId, "NilPass:NON_TOKENID_MINTING_DISABLED");
        require(balanceOf(msg.sender) + amount <= _getMaxMintPerWallet(), "NilPass:MINT_ABOVE_MAX_MINT_ALLOWANCE");
        require(totalSupply() + amount <= params.maxTotalSupply, "NilPass:MAX_ALLOCATION_REACHED");

        require(paid == getNextPriceForOpenMintInWei(amount), "NilPass:INVALID_PRICE");

        for (uint256 i = 0; i < amount; i++) {
            uint256 tokenId = _tokenIds.current();
            tokenSeed[tokenId] = SeedPhraseUtils.generateSeed(tokenId);

            _safeMint(recipient, tokenId);
            _tokenIds.increment();
        }

        if (preSaleEligible) {
            preSaleLimits[presaleType] -= amount;
        }
    }

    /**
     * @notice Allow anyone to burn two single panels they own in order to mint
     *         a double paneled token.
     * @param firstTokenId Token ID of the first token
     * @param secondTokenId Token ID of the second token
     */
    function burnForDoublePanel(uint256 firstTokenId, uint256 secondTokenId) public nonReentrant {
        require(_doublePanelTokens.current() < MAX_BURNS, "SeedPhrase:MAX_BURNS_REACHED");
        require(
            ownerOf(firstTokenId) == msg.sender && ownerOf(secondTokenId) == msg.sender,
            "SeedPhrase:INCORRECT_OWNER"
        );
        require(firstTokenId != secondTokenId, "SeedPhrase:EQUAL_TOKENS");
        // Ensure two owned tokens are in Burnable token pairings
        require(
            doubleWordPairings[abi.encode(string(firstTokenId), "-", string(secondTokenId))],
            "SeedPhrase:INVALID_TOKEN_PAIRING"
        );

        _burn(firstTokenId);
        _burn(secondTokenId);

        // Any Token ID of 3000 or greater indicates it is a double panel e.g. 3000, 3001, 3002...
        uint256 doublePaneledTokenId = 3000 + _doublePanelTokens;

        _mint(msg.sender, doublePaneledTokenId);

        // Add burned tokens to burnedTokensPairings mapping so we can use them to render the double panels later
        burnedTokensPairings[doublePaneledTokenId] = [firstTokenId, secondTokenId];
        _doublePanelTokens.increment();

        emit Burnt(msg.sender, firstTokenId, secondTokenId, doublePaneledTokenId);
    }

    /**
     * @notice Calculate the total available number of mints
     * @return total mint available
     */
    function totalMintsAvailable() public view override returns (uint256) {
        return derivativeParams.maxTotalSupply - totalSupply();
    }

    function _getMaxMintPerWallet() internal view returns (uint128) {
        return isPublicSaleActive() ? MAX_PUBLIC_MINT : params.maxMintAllowance;
    }

    function canMint(address account) public view virtual override returns (bool) {
        uint256 balance = balanceOf(account);

        if (isPublicSaleActive() && (totalMintsAvailable() > 0) && balance < MAX_PUBLIC_MINT) {
            return true;
        }

        if (isPreSaleActive()) {
            (bool preSaleEligible, ) = canMintPresale(account, 1);
            if (preSaleEligible) {
                return true;
            }
        }

        return false;
    }

    function tokenAttributes(uint256 tokenId, Randomize.Random memory random)
        public
        view
        virtual
        returns (SeedPhraseUtils.Attrs memory attributes, bytes memory traits)
    {
        attributes = SeedPhraseUtils.Attrs({
            showStroke: random.boolPercentage(70), // Show stroke 70% of the time
            showPanel: random.boolPercentage(80), // Show panel 80% of the time
            backgroundCircles: random.boolPercentage(2), // Show background circles 2% of the time
            doublePanel: (tokenId >= 3000), // To render a double parameter second token must not be 0
            bigBackgroundCircle: random.boolPercentage(15),
            border: false,
            backgroundSquare: false,
            showGrid: false,
            greyscale: false
        });

        // Only give option of background square if bigBackgroundCircle is not true
        if (!attributes.bigBackgroundCircle) {
            attributes.backgroundSquare = random.boolPercentage(15);
            // Border should only be on if not using background shapes
            if (!attributes.backgroundSquare) {
                attributes.border = random.boolPercentage(40);
            }
        }
        if (attributes.showStroke) {
            attributes.greyscale = random.boolPercentage(2); // Greyscale should only be seen if stroke is shown
            if (!attributes.backgroundCircles) {
                attributes.showGrid = random.boolPercentage(3); // Only set use grid if stroke is true and background circles is not true
            }
        }
        // Start Traits JSON
        traits = abi.encodePacked('[{"trait_type": "BIP39", "value": "', tokenId.toString(), '"},');

        if (!attributes.showStroke) {
            traits = abi.encodePacked(traits, '{"value": "No Stroke"},');
        } else if (attributes.greyscale) {
            traits = abi.encodePacked(traits, '{"value": "Greyscale"},');
        }
        if (!attributes.showPanel) {
            traits = abi.encodePacked(traits, '{"value": "No Panel"},');
        }
        if (attributes.backgroundCircles) {
            traits = abi.encodePacked(traits, '{"value": "Background Circles"},');
        } else if (attributes.showGrid) {
            traits = abi.encodePacked(traits, '{"value": "Grid"},');
        }
        if (attributes.bigBackgroundCircle) {
            traits = abi.encodePacked(traits, '{"value": "Giant Circle Background"},');
        } else if (attributes.backgroundSquare) {
            traits = abi.encodePacked(traits, '{"value": "Window Pane"},');
        } else if (attributes.border) {
            traits = abi.encodePacked(traits, '{"value": "Border"},');
        }

        traits = abi.encodePacked(traits, "]");
    }

    /// @param tokenId the tokenId (only double panels will have two)
    /// @return the json
    function _render(
        uint256 tokenId,
        Randomize.Random memory random,
        SeedPhraseUtils.Attrs memory attributes
    ) internal view virtual returns (string memory) {
        // Get color pallet
        SeedPhraseUtils.Colors memory pallet = SeedPhraseUtils._getPalette(random, attributes);

        //  Start SVG (viewbox & static patterns)
        bytes memory svg = abi.encodePacked(
            "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ",
            viewBox.toString(),
            " ",
            viewBox.toString(),
            "'><path fill='",
            pallet.background,
            "' ",
            SeedPhraseUtils._getStrokeStyle(attributes.border, pallet.blackOrWhite, "0.3", 50),
            " d='M0 0h",
            viewBox.toString(),
            "v",
            viewBox.toString(),
            "H0z'/>",
            "  <pattern id='panelCircles' x='0' y='0' width='.25' height='.1' patternUnits='objectBoundingBox'>",
            "<circle cx='",
            radius.toString(),
            "' cy='",
            radius.toString(),
            "' r='",
            r.toString(),
            "' fill='",
            pallet.negativeCircleFill,
            "' ",
            SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.negativeCircleStroke, "1", strokeWeight),
            " /></pattern>"
        );
        // Render optional patterns (grid OR background circles)
        if (attributes.backgroundCircles) {
            svg = abi.encodePacked(
                svg,
                "<pattern id='backgroundCircles' x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' patternUnits='userSpaceOnUse'><circle cx='",
                radius.toString(),
                "' cy='",
                radius.toString(),
                "' r='",
                r.toString(),
                "' fill='",
                pallet.blackOrWhite,
                "' style='fill-opacity: ",
                pallet.dynamicOpacity,
                ";'></circle></pattern><path fill='url(#backgroundCircles)' d='M0 0h",
                viewBox.toString(),
                "v",
                viewBox.toString(),
                "H0z'/>"
            );
        } else if (attributes.showGrid) {
            svg = abi.encodePacked(
                svg,
                "<pattern id='grid' x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' patternUnits='userSpaceOnUse'><rect x='0' y='0' width='",
                segmentSize.toString(),
                "' height='",
                segmentSize.toString(),
                "' fill='none' ",
                SeedPhraseUtils._getStrokeStyle(true, pallet.blackOrWhite, pallet.dynamicOpacity, strokeWeight),
                "'/></pattern><path fill='url(#grid)' d='M0 0h",
                viewBox.toString(),
                "v",
                viewBox.toString(),
                "H0z'/>"
            );
        }
        if (attributes.bigBackgroundCircle) {
            (uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
            uint16 centerCircle = (viewBox / 2);
            svg = abi.encodePacked(
                svg,
                "<circle cx='",
                centerCircle.toString(),
                "' cy='",
                centerCircle.toString(),
                "' r='",
                (shapeSize / 2).toString(),
                "' fill='",
                pallet.backgroundCircle,
                "' stroke='",
                pallet.negativeCircleStroke,
                "' style='stroke-width:",
                stroke.toString(),
                ";stroke-opacity:0.3'/>"
            );
        } else if (attributes.backgroundSquare) {
            (uint16 shapeSize, uint16 stroke) = SeedPhraseUtils._backgroundShapeSizing(random, attributes);
            uint16 centerSquare = ((viewBox - shapeSize) / 2);
            svg = abi.encodePacked(
                svg,
                "<rect x='",
                centerSquare.toString(),
                "' y='",
                centerSquare.toString(),
                "' width='",
                shapeSize.toString(),
                "' height='",
                shapeSize.toString(),
                "' fill='",
                pallet.backgroundCircle,
                "' stroke='",
                pallet.negativeCircleStroke,
                "' style='stroke-width:",
                stroke.toString(),
                ";stroke-opacity:0.3'/>"
            );
        }

        // Double panel (only if holder has burned two tokens from the defined pairings)
        // TODO: Add further checking here to confirm we are safe to render a double panel
        if (attributes.doublePanel) {
            uint256[2] memory tokens = burnedTokensPairings[tokenId];
            (uint8[4] memory firstBipIndexArray, string memory firstBipIndexStr) = SeedPhraseUtils.transformTokenId(
                tokens[0]
            );
            (uint8[4] memory secondBipIndexArray, string memory secondBipIndexStr) = SeedPhraseUtils.transformTokenId(
                tokens[1]
            );

            svg = abi.encodePacked(
                svg,
                _renderSinglePanel(firstBipIndexArray, attributes, pallet, doublePanel1X, false),
                _renderSinglePanel(secondBipIndexArray, attributes, pallet, doublePanel2X, true)
            );

            // Create text
            bytes memory combinedText = abi.encodePacked(firstBipIndexStr, " - #", secondBipIndexStr);
            svg = abi.encodePacked(
                svg,
                SeedPhraseUtils._renderText(string(combinedText), pallet.blackOrWhite),
                "</svg>"
            );
        }
        // Single Panel
        else {
            (uint8[4] memory bipIndexArray, string memory bipIndexStr) = SeedPhraseUtils.transformTokenId(tokenId);
            svg = abi.encodePacked(svg, _renderSinglePanel(bipIndexArray, attributes, pallet, singlePanelX, false));

            // Add closing text and svg element
            svg = abi.encodePacked(svg, SeedPhraseUtils._renderText(bipIndexStr, pallet.blackOrWhite), "</svg>");
        }

        return string(svg);
    }

    function _renderSinglePanel(
        uint8[4] memory bipIndexArray,
        SeedPhraseUtils.Attrs memory attributes,
        SeedPhraseUtils.Colors memory pallet,
        uint16 panelX,
        bool secondPanel
    ) internal pure returns (bytes memory panelSvg) {
        // Draw panels
        bool squareEdges = (attributes.doublePanel && attributes.backgroundSquare);
        if (attributes.showPanel) {
            panelSvg = abi.encodePacked(
                "<rect x='",
                (panelX - padding).toString(),
                "' y='",
                (panelY - padding).toString(),
                "' width='",
                (panelWidth + (padding * 2)).toString(),
                "' height='",
                (panelHeight + (padding * 2)).toString(),
                "' rx='",
                (squareEdges ? 0 : radius).toString(),
                "' fill='",
                (secondPanel ? pallet.panel2 : pallet.panel),
                "' ",
                SeedPhraseUtils._getStrokeStyle(attributes.showStroke, pallet.panelStroke, "1", strokeWeight),
                "/>"
            );
        }
        // Fill panel with negative circles, should resemble M600 300h400v1000H600z
        panelSvg = abi.encodePacked(
            panelSvg,
            "<path fill='url(#panelCircles)' d='M",
            panelX.toString(),
            " ",
            panelY.toString(),
            "h",
            panelWidth.toString(),
            "v",
            panelHeight.toString(),
            "H",
            panelX.toString(),
            "z'/>"
        );
        // Draw selected circles
        panelSvg = abi.encodePacked(
            panelSvg,
            _renderSelectedCircles(bipIndexArray, pallet, attributes.showStroke, panelX, secondPanel)
        );
    }

    function _renderSelectedCircles(
        uint8[4] memory bipIndexArray,
        SeedPhraseUtils.Colors memory pallet,
        bool showStroke,
        uint16 panelX,
        bool secondPanel
    ) internal pure returns (bytes memory svg) {
        for (uint8 i = 0; i < bipIndexArray.length; i++) {
            svg = abi.encodePacked(
                svg,
                "<circle cx='",
                (panelX + (segmentSize * i) + radius).toString(),
                "' cy='",
                (panelY + (segmentSize * bipIndexArray[i]) + radius).toString(),
                "' r='",
                (radius - padding).toString(),
                ".1' fill='", // Increase the size a tiny bit here (0.1) to hide negative circle outline
                (secondPanel ? pallet.selectedCircleFill2 : pallet.selectedCircleFill),
                "' ",
                SeedPhraseUtils._getStrokeStyle(showStroke, pallet.selectedCircleStroke, "1", strokeWeight),
                " />"
            );
        }
    }

    function validPairingKey(string memory key) public view returns (bool) {
        return pairings[key];
    }

    // TODO: Make this function only accessible by admin (not sure how to do this)
    function ammendPairings(string memory key, bool value) private {
        pairings[key] = value;
    }

    // Mapping should be a string containing both tokens e.g. "tokenId1-tokenId2"
    mapping(string => bool) doubleWordPairings;
}
Loading...