contract DAOBnBMinter is Ownable {
    using Strings for uint16;
    using ECDSA for bytes32;

    /* DAObnb NFT contract */
    DAOBnBNFT nftContract;

    /* Is Pre Sale Active */
    bool public preSaleIsActive;
    /* Is Sale Active */
    bool public saleIsActive;
    
    /* If > 0 limit pre sale silver cards purchases per address */
    uint16 public maxPreSaleSilverCardsPerAddress = 2;
    /* If > 0 limit pre sale black cards purchases per address */
    uint16 public maxPreSaleBlackCardsPerAddress = 1;

    /* Limit how many silver cards can be purchased in single transaction for pre sale */
    uint16 public maxPreSaleSilverCardsPerTransaction = 2;
    /* Limit how many black cards can be purchased in single transaction for pre sale */
    uint16 public maxPreSaleBlackCardsPerTransaction = 1;
    
    /* Limit how many silver cards can be purchased in single transaction for public sale */
    uint16 public maxSilverCardsPerTransaction = 16;
    /* Limit how many black cards can be purchased in single transaction for public sale */
    uint16 public maxBlackCardsPerTransaction = 4;

    /* silver cards reserved for marketing */
    uint16 public reservedSilverCards = 640;
    /* black cards reserved for marketing */
    uint16 public reservedBlackCards = 270;

    /* Price for silver card during pre sale */
    uint256 public preSaleSilverCardPrice = 0.175 ether;
    /* Price for black card during pre sale */
    uint256 public preSaleBlackCardPrice = 0.7 ether;

    /* Price for silver card during public sale */
    uint256 public silverCardPrice = 0.175 ether;
    /* Price for black card during public sale */
    uint256 public blackCardPrice = 0.7 ether;
    
    /* PreSaleList Signature Addresses */
    mapping(address => bool) public preSaleListSignatureAddresses;
    /* PreSaleList Signature Nounces used */
    mapping(uint16 => address) public preSaleListSignatureUsedNounces;
    
    /* PreSaleListed addressess */
    mapping(address => bool) public preSaleListedAddresses;

    /* Silver cards minted per address during pre sale */
    mapping(address => uint16) public preSaleSilverCardPurchases;
    /* Black cards minted per address during pre sale */
    mapping(address => uint16) public preSaleBlackCardPurchases;

    constructor(
        DAOBnBNFT _nftContract,
        address[] memory _preSaleListSignatureAddresses
    ) {
        nftContract = _nftContract;

        for (uint256 i = 0; i < _preSaleListSignatureAddresses.length; i++) {
            preSaleListSignatureAddresses[_preSaleListSignatureAddresses[i]] = true;
        }
    }

    function getPreSaleListMessage(uint16 nounce, address sender)
        public
        pure
        returns (bytes32)
    {
        if (nounce > 0) {
            return keccak256(abi.encodePacked('ProjectId: 621285d3e200cdf5b5ef5704, Nounce: ', nounce));
        }
        
        return keccak256(abi.encodePacked('ProjectId: 621285d3e200cdf5b5ef5704, Address: ', sender));
    }
    
    function mintPreSale(uint16 silverCardsAmount, uint16 blackCardsAmount, uint16 nounce, bytes calldata signature) external payable {
        require(preSaleIsActive, 'Pre sale must be active to mint pre sale');
        require(!saleIsActive, 'Regular sale is already active');
        require(
            silverCardsAmount + blackCardsAmount > 0,
            'No amounts provided'
        );
        require(
            silverCardsAmount <= maxPreSaleSilverCardsPerTransaction,
            'Can not mint that many silver tokens in a single transaction during the pre sale'
        );
        require(
            blackCardsAmount <= maxPreSaleBlackCardsPerTransaction,
            'Can not mint that many black tokens in a single transaction during the pre sale'
        );
        require(
            silverCardsAmount <= nftContract.silverCardsLeft() - reservedSilverCards,
            'Sold Out'
        );
        require(
            blackCardsAmount <= nftContract.blackCardsLeft() - reservedBlackCards,
            'Sold Out'
        );
        require(
            preSaleSilverCardPurchases[msg.sender] + silverCardsAmount <= maxPreSaleSilverCardsPerAddress,
            'Can only mint so many silver cards during the presale'
        );
        require(
            preSaleBlackCardPurchases[msg.sender] + blackCardsAmount <= maxPreSaleBlackCardsPerAddress,
            'Can only mint so many black cards during the presale'
        );
        require(
            msg.value >= (preSaleSilverCardPrice * silverCardsAmount) + (preSaleBlackCardPrice * blackCardsAmount),
            'Ether value sent is not correct'
        );
        require(preSaleListedAddresses[_msgSender()] || signature.length > 0, 'Signature required for pre sale');
        require(nounce == 0 || preSaleListSignatureUsedNounces[nounce] == address(0) || preSaleListSignatureUsedNounces[nounce] == _msgSender(), 'Invalid or used nounce');

        if (!preSaleListedAddresses[_msgSender()] && preSaleListSignatureUsedNounces[nounce] != _msgSender()) {
            bytes32 message = getPreSaleListMessage(nounce, _msgSender());
            bytes32 messageHash = message.toEthSignedMessageHash();
            address signer = messageHash.recover(signature);

            require(preSaleListSignatureAddresses[signer], 'Signature invalid');

            if (nounce > 0) {
                preSaleListSignatureUsedNounces[nounce] = _msgSender();
            }
        }

        preSaleSilverCardPurchases[msg.sender] += silverCardsAmount;
        preSaleBlackCardPurchases[msg.sender] += blackCardsAmount;

        nftContract.mint(silverCardsAmount, blackCardsAmount, msg.sender);
    }
    
    function mint(uint16 silverCardsAmount, uint16 blackCardsAmount) external payable {
        require(saleIsActive, 'Regular sale is not active');
        require(
            silverCardsAmount + blackCardsAmount > 0,
            'No amounts provided'
        );
        require(
            silverCardsAmount <= maxSilverCardsPerTransaction,
            'Can not mint that many silver cards in a single transaction during the sale'
        );
        require(
            blackCardsAmount <= maxBlackCardsPerTransaction,
            'Can not mint that many black cards in a single transaction during the sale'
        );
        require(
            silverCardsAmount <= nftContract.silverCardsLeft() - reservedSilverCards,
            'Sold Out'
        );
        require(
            blackCardsAmount <= nftContract.blackCardsLeft() - reservedBlackCards,
            'Sold Out'
        );
        require(
            msg.value >= (silverCardPrice * silverCardsAmount) + (blackCardPrice * blackCardsAmount),
            'Ether value sent is not correct'
        );

        nftContract.mint(silverCardsAmount, blackCardsAmount, _msgSender());
    }
    
    /**
     * @dev Update a list of addresses to be allowed to be used for signature based preSaleList
     */
    function updatePreSaleListSignatureAddresses(address[] memory _preSaleListSignatureAddresses, bool allowed)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < _preSaleListSignatureAddresses.length; i++) {
            preSaleListSignatureAddresses[_preSaleListSignatureAddresses[i]] = allowed;
        }
    }
    
    /**
     * @dev Update a list of addresses to be allowed in preSaleList
     */
    function updatePreSaleListedAddresses(address[] memory _preSaleListedAddresses, bool allowed)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < _preSaleListedAddresses.length; i++) {
            preSaleListedAddresses[_preSaleListedAddresses[i]] = allowed;
        }
    }

    function flipPreSaleState() external onlyOwner {
        preSaleIsActive = !preSaleIsActive;
    }

    function flipSaleState() external onlyOwner {
        saleIsActive = !saleIsActive;
    }

    function setMaxPreSaleCardsPerAddress(uint16 _silverCardsAmount, uint16 _blackCardsAmount) external onlyOwner {
        maxPreSaleSilverCardsPerAddress = _silverCardsAmount;
        maxPreSaleBlackCardsPerAddress = _blackCardsAmount;
    }

    function setMaxPreSaleCardsPerTransaction(uint16 _silverCardsAmount, uint16 _blackCardsAmount) external onlyOwner {
        maxPreSaleSilverCardsPerTransaction = _silverCardsAmount;
        maxPreSaleBlackCardsPerTransaction = _blackCardsAmount;
    }

    function setMaxCardsPerTransaction(uint16 _silverCardsAmount, uint16 _blackCardsAmount, bool updatePreSaleAlso) external onlyOwner {
        maxSilverCardsPerTransaction = _silverCardsAmount;
        maxBlackCardsPerTransaction = _blackCardsAmount;

        if (updatePreSaleAlso) {
            maxPreSaleSilverCardsPerTransaction = _silverCardsAmount;
            maxPreSaleBlackCardsPerTransaction = _blackCardsAmount;
        }
    }

    function setReservedCards(uint16 _silverCardsAmount, uint16 _blackCardsAmount) external onlyOwner {
        reservedSilverCards = _silverCardsAmount;
        reservedBlackCards = _blackCardsAmount;
    }

    function setPreSalePrice(uint256 _silverCardPrice, uint256 _blackCardPrice) external onlyOwner {
        preSaleSilverCardPrice = _silverCardPrice;
        preSaleBlackCardPrice = _blackCardPrice;
    }

    function setPrice(uint256 _silverCardPrice, uint256 _blackCardPrice, bool updatePreSaleAlso) external onlyOwner {
        silverCardPrice = _silverCardPrice;
        blackCardPrice = _blackCardPrice;

        if (updatePreSaleAlso) {
            preSaleSilverCardPrice = _silverCardPrice;
            preSaleBlackCardPrice = _blackCardPrice;
        }
    }

    function reserveMint(uint16 silverCardsAmount, uint16 blackCardsAmount, address[] calldata to) external onlyOwner {
        require(
            (silverCardsAmount * to.length) <= reservedSilverCards,
            'Not enough reserve left for team'
        );
        require(
            (blackCardsAmount * to.length) <= reservedBlackCards,
            'Not enough reserve left for team'
        );
        require(
            silverCardsAmount + blackCardsAmount > 0,
            'No amounts provided'
        );

        for (uint16 i = 0; i < to.length; i++) {
            nftContract.mint(silverCardsAmount, blackCardsAmount, to[i]);
        }

        reservedSilverCards = uint16(reservedSilverCards - (silverCardsAmount * to.length));
        reservedBlackCards = uint16(reservedBlackCards - (blackCardsAmount * to.length));
    }
    
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }
}
function updatePreSaleListedAddresses(address[] memory _preSaleListedAddresses, bool allowed)
    external
    onlyOwner
{
    for (uint256 i = 0; i < _preSaleListedAddresses.length; i++) {
        preSaleListedAddresses[_preSaleListedAddresses[i]] = allowed;
    }
}

/**
 * @dev Update a list of addresses to be allowed in preSaleList
 */