contract HighRoller is ERC721, Ownable{
    using SafeMath for uint256;
    using Counters for Counters.Counter;

    // constants
    uint256 constant MAX_ELEMENTS = 7777;
    uint256[] public PURCHASE_LIMIT = [8, 5];
    uint256[] public STAGE_PRICE = [0.0777 ether, 0.085 ether, 0.098 ether];
    uint256 constant BOUNTY_AMOUNT = 0.259 ether;
    uint256 constant NUM_OF_BOUNTY = 77;
    enum STAGE { PRE_SALE, PUBLIC_SALE }

    // state variable
    STAGE public CURRENT_STAGE = STAGE.PRE_SALE;
    bool public MINTING_PAUSED = true;
    string public _contractURI = "";
    uint256 randNonce = 0;

    Counters.Counter private _tokenIdTracker;

    // used to validate whitelists
    bytes32 public patronListMerkleRoot;
    bytes32 public goldListMerkleRoot;
    bytes32 public silverListMerkleRoot;
    bytes32 public ownerMerkleRoot;
    
    mapping(uint256 => address) private claimedList;
    mapping(address => uint256) private _allowListClaimed1;
    mapping(address => uint256) private _allowListClaimed2;
    mapping(address => uint256) private _allowListClaimed3;

    // bounty whitelist
    mapping(address => bool) bountyList;

    constructor() ERC721("HighRoller", "HighRoller") public{
    }

    modifier isValidMerkleProof(bytes32[] memory merkleProof, bytes32 root) {
        require(
            MerkleProof.verify(
                merkleProof,
                root,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "Address does not exist in list"
        );
        _;
    }

    modifier onlyowner(bytes32[] memory merkleProof) {
        require(
            MerkleProof.verify(
                merkleProof,
                ownerMerkleRoot,
                keccak256(abi.encodePacked(msg.sender))
            ),
            "This function can be called only by owner"
        );
        _;
    }

    function setPauseMinting(bool _pause) public onlyOwner{
        MINTING_PAUSED = _pause;
    }

    function setPurchaseLimit(uint256 _case, uint256 _limit) public onlyOwner{
        PURCHASE_LIMIT[_case] = _limit;
    }

    function setGoldListMerkleRoot(bytes32 merkleRoot) public onlyOwner {
        goldListMerkleRoot = merkleRoot;
    }

    function setSilverListMerkleRoot(bytes32 merkleRoot) public onlyOwner {
        silverListMerkleRoot = merkleRoot;
    }

    function setPatronListMerkleRoot(bytes32 merkleRoot) public onlyOwner {
        patronListMerkleRoot = merkleRoot;
    }

    function setOwnerMerkleRoot(bytes32 merkleRoot) public onlyOwner {
        ownerMerkleRoot = merkleRoot;
    }

    function ownerSale(uint256 numberOfTokens) public onlyOwner {
        require(totalSupply() < MAX_ELEMENTS, 'All tokens have been minted');
        require(totalSupply() + numberOfTokens < MAX_ELEMENTS, 'Purchase would exceed max supply');

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }
    }

    function goldListSale(bytes32[] memory merkleProof, uint256 numberOfTokens) public payable isValidMerkleProof(merkleProof, goldListMerkleRoot) {
        require(!MINTING_PAUSED, "Minting is not active");
        require(CURRENT_STAGE == STAGE.PRE_SALE, "Current stage should be PRE_SALE");
        require(totalSupply() < MAX_ELEMENTS, 'All tokens have been minted');
        require(totalSupply() + numberOfTokens < MAX_ELEMENTS, 'Purchase would exceed max supply');
        require(_allowListClaimed1[msg.sender] + numberOfTokens <= PURCHASE_LIMIT[0], 'Purchase exceeds max allowed');
        require(STAGE_PRICE[0] * numberOfTokens <= msg.value, 'ETH amount is not sufficient');

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _allowListClaimed1[msg.sender] += 1;
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }
    }

    function silverListSale(bytes32[] memory merkleProof, uint256 numberOfTokens) public payable isValidMerkleProof(merkleProof, silverListMerkleRoot) {
        require(!MINTING_PAUSED, "Minting is not active");
        require(CURRENT_STAGE == STAGE.PRE_SALE, "Current stage should be PRE_SALE");
        require(totalSupply() < MAX_ELEMENTS, 'All tokens have been minted');
        require(totalSupply() + numberOfTokens < MAX_ELEMENTS, 'Purchase would exceed max supply');
        require(_allowListClaimed2[msg.sender] + numberOfTokens <= PURCHASE_LIMIT[1], 'Purchase exceeds max allowed');
        require(STAGE_PRICE[1] * numberOfTokens <= msg.value, 'ETH amount is not sufficient');

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _allowListClaimed2[msg.sender] += 1;
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }
    }

    function patronListSale(bytes32[] memory merkleProof, uint256 numberOfTokens) public payable isValidMerkleProof(merkleProof, patronListMerkleRoot) {
        require(!MINTING_PAUSED, "Minting is not active");
        require(CURRENT_STAGE == STAGE.PRE_SALE, "Current stage should be PRE_SALE");
        require(totalSupply() < MAX_ELEMENTS, 'All tokens have been minted');
        require(totalSupply() + numberOfTokens < MAX_ELEMENTS, 'Purchase would exceed max supply');
        require(_allowListClaimed3[msg.sender] + numberOfTokens <= PURCHASE_LIMIT[0], 'Purchase exceeds max allowed');
        require(STAGE_PRICE[0] * numberOfTokens <= msg.value, 'ETH amount is not sufficient');

        // give one nft free for patron rollerlist
        if (_allowListClaimed3[msg.sender] == 0) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _allowListClaimed3[msg.sender] += 1;
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _allowListClaimed3[msg.sender] += 1;
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }
    }

    function publicMint(uint256 numberOfTokens) public payable {
        require(!MINTING_PAUSED, "Minting is not active");
        require(CURRENT_STAGE == STAGE.PUBLIC_SALE, "Current stage should be PUBLIC_SALE");
        require(totalSupply() < MAX_ELEMENTS, 'All tokens have been minted');
        require(totalSupply() + numberOfTokens < MAX_ELEMENTS, 'Purchase would exceed max supply');
        require(STAGE_PRICE[2] * numberOfTokens <= msg.value, 'ETH amount is not sufficient');

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _tokenIdTracker.increment();

            claimedList[_tokenIdTracker.current()] = msg.sender; // should be checked
            _safeMint(msg.sender, _tokenIdTracker.current());
            _setTokenURI(_tokenIdTracker.current(), uint2str(_tokenIdTracker.current()));
        }
    }

    function openBounty() public onlyOwner {
        uint256 index = 0;
        uint256 totalSupply = totalSupply();

        while(true) {
            uint256 selectedIdx = randMod(totalSupply);
            address selectedClaimer = claimedList[selectedIdx + 1];
            if (!bountyList[selectedClaimer]) {
                bountyList[selectedClaimer] = true;
                payable(selectedClaimer).transfer(BOUNTY_AMOUNT);
                index ++;
                if (index >= NUM_OF_BOUNTY)
                    break;
            }
        }
    }

    // returns random number between 0 to _modulus
    function randMod(uint256 _modulus) internal returns(uint256)
    {
        // increase nonce
        randNonce++;
        return uint256(keccak256(abi.encodePacked(block.timestamp,
                                                msg.sender,
                                                randNonce))) %
                                                _modulus;
    }

    function setStage(uint256 _stage) public onlyOwner {
        CURRENT_STAGE = STAGE(_stage);
    }

    function withdraw(bytes32[] memory merkleProof) public onlyowner(merkleProof) {
        uint256 balance = address(this).balance;

        payable(msg.sender).transfer(balance);
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        _setBaseURI(baseURI);
    }

    function setContractURI(string memory URI) public onlyOwner {
        _contractURI = URI;
    }

    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
        if (_i == 0) {
            return "0";
        }
        uint j = _i;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len;
        while (_i != 0) {
            k = k-1;
            uint8 temp = (48 + uint8(_i - _i / 10 * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }
}
function randMod(uint256 _modulus) internal returns(uint256)
{
    // increase nonce
    randNonce++;
    return uint256(keccak256(abi.encodePacked(block.timestamp,
                                            msg.sender,
                                            randNonce))) %
                                            _modulus;
}

// returns random number between 0 to _modulus