contract EaselyFixedCollection is EaselyMarketplaceCollection, IEaselyContractInitializer {  
    using ECDSA for bytes32;  
    using Strings for uint256;

    bool public burnable;
    bool public isRevealed = false;
    bool private hasInit = false;

    uint256 public constant MAX_SUPPLY_LIMIT = 10 ** 9;
    uint256 public maxSupply;
    // Limits how much any single transaction can be
    uint256 public transactionMax;
    // Limits how much any single wallet can mint on a collection.
    uint256 public maxMint;
    
    mapping(address => uint256) public mintCount;

    // Used to shuffle tokenURI upon reveal
    uint256 public offset;
    // Used when switching from incremental tokenId to 
    uint256 private randOffset;

    // Mapping to enable constant time onchain randomness
    uint256[MAX_SUPPLY_LIMIT] private indices;
    string private ipfsHash;

    event Minted(address indexed buyer, uint256 amount, uint256 unitPrice, bytes32 hash);
    event IpfsRevealed(string indexed ipfsHash, bool locked);

    /**
     * @dev Constructor function
     */
    constructor(
        bool[8] memory bools, address[8] memory addresses, uint256[32] memory uints, string[8] memory strings
    ) ERC721(strings[0], strings[1]) {
        addresses[0] = _msgSender();
        _init(bools, addresses, uints, strings);
    }

    function init(
        bool[8] memory bools, 
        address[8] memory addresses, 
        uint256[32] memory uints, 
        string[8] memory strings
    ) external override {
        _init(bools, addresses, uints, strings);
    }

    function _init(
        bool[8] memory bools, 
        address[8] memory addresses, 
        uint256[32] memory uints, 
        string[8] memory strings
    ) internal {
        require(!hasInit, "Already has be initiated");
        hasInit = true;

        burnable = bools[0];
        isRevealed = bools[1];
        if (isRevealed) {
            emit IpfsRevealed(ipfsHash, false);
        }

        _owner = addresses[0];
        address[4] memory royaltiesAddrs = [addresses[1], addresses[2], addresses[3], addresses[4]];
        alternateSignerAddress = addresses[5];
        
        payoutContractAddress = 0x68f5C1e24677Ac4ae845Dde07504EAaD98f82572; 

        /* Start at 1 because we reserve 0 for tokens that are not on auction */
        nextAuctionId = 1;
        _setSecondary(uints[0]);
        _setRoyalties(royaltiesAddrs, [uints[1], uints[2], uints[3], uints[4]]);
        maxSupply = uints[5];
        require(maxSupply < MAX_SUPPLY_LIMIT, "Collection is too big");

        if (uints[6] != 0) {
            timePerDecrement = uints[6];
        } else {
            timePerDecrement = 300;
        }

        if (uints[7] != 0){
            transactionMax = uints[7];
        } else {
            transactionMax = 10;
        }
        maxMint = uints[8];

        _name = strings[0];
        _symbol = strings[1];
        ipfsHash = strings[2];
    }

    /**
     * @dev If this collection was created with burnable on, owners of tokens
     * can use this method to burn their tokens. Easely will keep track of 
     * burns in case creators want to reward users for burning tokens.
     */
    function burn(uint256 tokenId) external {
        require(ownerOf(tokenId) == _msgSender(), "Cannot burn a token you do not own");
        require(burnable, "Tokens from this collection are not burnable");

        _burn(tokenId);
    }
    
    /**
     * @dev Method used if the creator wants to keep their collection hidden until
     * a later release date. On reveal, a creator can decide if they want to 
     * lock the unminted tokens or enable them for on-chain randomness minting.
     * 
     * IMPORTANT - this function can only be called ONCE, if a wrong IPFS hash
     * is submitted by the owner, it cannot ever be switched to a different one.
     */
    function lockTokenURI(string calldata revealIPFSHash, bool lockOnReveal) external onlyOwner {
        require(!isRevealed, "The token URI has already been set");
        offset = _random(maxSupply);
        ipfsHash = revealIPFSHash;
        isRevealed = true;

        if (!lockOnReveal) {
            // so we know what index to start generating random numbers from
            randOffset = totalSupply();
        } else {
            // This will lock the unminted tokens at reveal time
            maxSupply = totalSupply();
        }

        emit IpfsRevealed(revealIPFSHash, lockOnReveal);
    }

    /**
     * @dev tokenURI of a tokenId, will change to include the tokeId and an offset in
     * the URI once the collection has been revealed.
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        if (!isRevealed) {
            return string(abi.encodePacked(_baseURI(), ipfsHash));
        }

        require(_exists(tokenId), "URI query for nonexistent token");

        uint256 offsetId = (tokenId + offset) % maxSupply;
        return string(abi.encodePacked(_baseURI(), ipfsHash, "/", offsetId.toString()));
    }

    /**
     * @dev Hash that the owner or approved alternate signer then sign that the approved buyer
     * can use in order to call the {mintAllow} method.
     */
    function hashToSignForAllowList(
        address allowedAddress,
        uint256 version,
        uint256 nonce,
        uint256 price,
        uint256 amount
    ) external view returns (bytes32) {
        _checkValidSigner(_msgSender());
        return _hashForAllowList(allowedAddress, version, nonce, price, amount);
    }

    /**
     * @dev A way to invalidate a signature so the given params cannot be used in the {mintAllow} method.
     */
    function cancelAllowList(
        address allowedAddress,
        uint256 version,
        uint256 nonce,
        uint256 price,
        uint256 amount
    ) external {
        _checkValidSigner(_msgSender());
        bytes32 hash = _hashToCheckForAllowList(allowedAddress, version, nonce, price, amount);
        _cancelledOrFinalizedSales[hash] = true;
        emit SaleCancelled(_msgSender(), hash);
    }
    
    /**
     * @dev Allows a user with an approved signature to mint at a price and quantity specified by the
     * contract. A user is still limited by totalSupply, transactionMax, and mintMax if populated.
     */
    function mintAllow(
        address allowedAddress,
        uint256 version,
        uint256 nonce,
        uint256 price,
        uint256 amount,
        uint256 buyAmount,
        bytes memory signature
    ) external payable {
        require(totalSupply() + buyAmount <= maxSupply, "Over token supply limit");
        require(buyAmount <= amount, "Over signature amount");
        require(buyAmount <= transactionMax, "Over transaction limit");
        require(version == _addressToActiveVersion[owner()], "This presale version is disabled");
        require(allowedAddress == _msgSender(), "Invalid sender");

        uint256 totalPrice = price * buyAmount;
        uint256 msgValue = msg.value;
        require(msgValue >= totalPrice, "Msg value too small");

        bytes32 hash = _hashToCheckForAllowList(allowedAddress, version, nonce, price, amount);
        require(!_cancelledOrFinalizedSales[hash], "Signature no longer active");
        address signer = hash.recover(signature);
        _checkValidSigner(signer);
        _cancelledOrFinalizedSales[hash] = true;

        _mintRandom(_msgSender(), buyAmount);
        emit Minted(_msgSender(), buyAmount, price, hash);

        IEaselyPayout payoutContract = IEaselyPayout(payoutContractAddress);
        payoutContract.splitPayable{ value: totalPrice }(owner(), royalties, royaltiesBPS);
        payable(_msgSender()).transfer(msgValue - totalPrice);
    }

    /**
     * @dev Hash that the owner or approved alternate signer then sign that buyers use 
     * in order to call the {mint} method.
     */
    function hashToSignForMint(uint256 version, uint256 amount, uint256[4] memory pricesAndTimestamps) external view returns (bytes32) {
        _checkValidSigner(_msgSender());
        require(amount <= transactionMax, "Over transaction limit");

        return _hashForMint(version, amount, pricesAndTimestamps);
    }

    /**
     * @dev A way to invalidate a signature so the given params cannot be used in the {mint} method.
     */
    function cancelMint(uint256 version, uint256 amount, uint256[4] memory pricesAndTimestamps) external {
        _checkValidSigner(_msgSender());
        bytes32 hash = _hashToCheckForMint(version, amount, pricesAndTimestamps);
        _cancelledOrFinalizedSales[hash] = true;
        emit SaleCancelled(_msgSender(), hash);
    }

    /**
     * @dev Allows anyone to buy an amount of tokens at a price which matches
     * the signature that the owner or alternate signer has approved
     */
    function mint(
        uint256 version,
        uint256 amount,
        uint256[4] memory pricesAndTimestamps, 
        bytes memory signature
    ) external payable {
        require(totalSupply() + amount <= maxSupply, "Over token supply limit");
        require(amount <= transactionMax, "Over transaction limit");
        require(version == _addressToActiveVersion[owner()], "Invalid version");

        uint256 unitPrice = _currentPrice(pricesAndTimestamps);
        uint256 totalPrice = amount * unitPrice;
        uint256 msgValue = msg.value;
        require(msgValue >= totalPrice, "Msg value too small");

        bytes32 hash = _hashToCheckForMint(version, amount, pricesAndTimestamps);
        require(!_cancelledOrFinalizedSales[hash], "Signature no longer active");
        address signer = hash.recover(signature);
        _checkValidSigner(signer);

        _mintRandom(_msgSender(), amount);
        emit Minted(_msgSender(), amount, unitPrice, hash);

        IEaselyPayout payoutContract = IEaselyPayout(payoutContractAddress);
        payoutContract.splitPayable{ value: totalPrice }(owner(), royalties, royaltiesBPS);
        payable(_msgSender()).transfer(msgValue - totalPrice);
    }

    /**
     * @dev Hash an order that we need to check against the signature to see who the signer is.
     * see {_hashForAllowList} to see the hash that needs to be signed.
     */
    function _hashToCheckForAllowList(
        address allowedAddress,
        uint256 nonce,
        uint256 version,
        uint256 price,
        uint256 amount
    ) internal view returns (bytes32) {
        return _toEthSignedMessageHash(_hashForAllowList(allowedAddress, nonce, version, price, amount));
    }

    /**
     * @dev Hash that the owner or alternate wallet must sign to enable a {mintAllow} for a user
     * @return Hash of message prefix and order hash per Ethereum format
     */
    function _hashForAllowList(        
        address allowedAddress,
        uint256 nonce,
        uint256 version,
        uint256 price,
        uint256 amount
    ) internal view returns (bytes32) {
        return keccak256(abi.encode(address(this), block.chainid, owner(), allowedAddress, nonce, version, price, amount));
    }

    /**
     * @dev Hash an order that we need to check against the signature to see who the signer is.
    * see {_hashForMint} to see the hash that needs to be signed.
     */
    function _hashToCheckForMint(
        uint256 version,
        uint256 amount,
        uint256[4] memory pricesAndTimestamps
    ) internal view returns (bytes32) {
        return _toEthSignedMessageHash(_hashForMint(version, amount, pricesAndTimestamps));
    }

    /**
     * @dev Hash that the owner or alternate wallet must sign to enable {mint} for all users 
     */
    function _hashForMint(        
        uint256 version,
        uint256 amount,
        uint256[4] memory pricesAndTimestamps
    ) internal view returns (bytes32) {
        return keccak256(abi.encode(address(this), block.chainid, owner(), amount, pricesAndTimestamps, version));
    }

    /// @notice Generates a pseudo random index of our tokens that has not been used so far
    function _mintRandomIndex(address buyer, uint256 amount) internal {
        //  number of tokens left to create
        uint256 totalSize = maxSupply - totalSupply();

        for (uint256 i = 0; i < amount; i++) {
            // generate a random index
            uint256 index = _random(totalSize);
            uint256 tokenAtPlace = indices[index];

            uint256 tokenId;
            // if we havent stored a replacement token...
            if (tokenAtPlace == 0) {
                //... we just return the current index
                tokenId = index;
            } else {
                // else we take the replace we stored with logic below
                tokenId = tokenAtPlace;
            }

            // get the highest token id we havent handed out
            uint256 lastTokenAvailable = indices[totalSize - 1];
            // we need to store a replacement token for the next time we roll the same index
            // if the last token is still unused...
            if (lastTokenAvailable == 0) {
                // ... we store the last token as index
                indices[index] = totalSize - 1;
            } else {
                // ... we store the token that was stored for the last token
                indices[index] = lastTokenAvailable;
            }

            _safeMint(buyer, tokenId + randOffset);
            totalSize--;
        }
    }

    /// @notice Generates a pseudo random number based on arguments with decent entropy
    /// @param max The maximum value we want to receive
    /// @return A random number less than the max
    function _random(uint256 max) internal view returns (uint256) {
        if (max == 0) {
            return 0;
        }

        uint256 rand = uint256(
            keccak256(
                abi.encode(
                    msg.sender,
                    block.difficulty,
                    block.timestamp,
                    blockhash(block.number - 1)
                )
            )
        );
        return rand % max;
    }

    /**
     * @dev Wrapper around {_mintRandomIndex} that incrementally if the collection has not 
     * been revealed yet, which also checks the buyer has not exceeded maxMint count 
     */
    function _mintRandom(address buyer, uint256 amount) internal {
        require(maxMint == 0 || mintCount[buyer] + amount <= maxMint, "Buyer over mint maximum");
        mintCount[buyer] += amount;

        if (isRevealed) {
            _mintRandomIndex(buyer, amount);
            return;
        }

        uint256 nextTokenId = totalSupply();

        for (uint256 i = 0; i < amount; i++) {
            _safeMint(buyer, nextTokenId + i);
        }
    }
}
/**
 * @dev This implements a lazy-minted, fixed collection of EaselyMarketplaceCollection.
 * It requires that the creator knows the total number of NFTs they want and has an IPFS
 * hash that is a directory with all the tokenIds from 0 to the #NFTs - 1.
 *
 * It has two main methods to lazy-mint, one allows the owner to approve single use signatures
 * for specific wallet addresses and the other allows a general mint signature that anyone can use.
 *
 * Minting from this fixed collection is always random, this can be done either with a reveal
 * mechanism that has a random offset, or on-chain randomness if the collection is already revealed. 
 */
function _hashForAllowList(        
    address allowedAddress,
    uint256 nonce,
    uint256 version,
    uint256 price,
    uint256 amount
) internal view returns (bytes32) {
    return keccak256(abi.encode(address(this), block.chainid, owner(), allowedAddress, nonce, version, price, amount));
}

/**
 * @dev Hash that the owner or alternate wallet must sign to enable a {mintAllow} for a user
 * @return Hash of message prefix and order hash per Ethereum format
 */