contract Rocks721 is Context, ERC721, Ownable {

    address payable public withdrawalAddress;

    //Token sale control logic
    uint256 public maxNumberOfPieces;
    uint256 public tokenCounter;

    //Standard sale
    bool public standardSaleActive;
    uint256 public pricePerPiece;

    //Presale
    bool public preSaleActive;
    mapping (address => uint256) public whitelisted;
    mapping (address => uint256) public preSaleMinted;
    mapping (uint256 => uint256) public reservedTokens;
    mapping (address => uint256[]) public giftTokens;

    event Purchase(address buyer, uint256 price, uint256 [] tokenIds);
    event MetadataUpdated(string newTokenUriBase);

    constructor(
        uint256 givenPricePerPiece,
        address payable givenWithdrawalAddress,
        string memory givenTokenURIBase
    ) ERC721("UNSHADED ROCKS", "ROCK") {
        pricePerPiece = givenPricePerPiece;
        withdrawalAddress = givenWithdrawalAddress;
        _setBaseURI(givenTokenURIBase);
        maxNumberOfPieces = 9999;
        tokenCounter = 0;
    }

    //Change base uri 
     function updateBaseURI(string memory givenTokenURIBase) public onlyOwner{
        _setBaseURI(givenTokenURIBase);
    }

    function setWhitelistAddress(address[] memory users, uint256[][] memory givenGiftTokens) public onlyOwner {
        for (uint256 i = 0; i < users.length; i++) {
            //use the users address as a key and set the limit to 100
            whitelisted[users[i]] = 100;
            giftTokens[users[i]] = givenGiftTokens[i];
            for( uint256 j = 0; j< givenGiftTokens[i].length; j ++){
                reservedTokens[givenGiftTokens[i][j]] = 1;
            }
        }
    }

    function claimGift() public {
        require(whitelisted[msg.sender] > 0, "address not on whitelist ");
        for(uint256 i = 0; i < giftTokens[msg.sender].length; i ++){
            _safeMint( msg.sender, giftTokens[msg.sender][i]);
        }
    }

    //Input list of reserved tokens 
    function setReservedTokens(uint256[] memory givenReservedTokens) public onlyOwner{
        for(uint256 i = 0; i < givenReservedTokens.length; i ++){
            reservedTokens[givenReservedTokens[i]] = 1;
        }
    }

    //Sale logic
    function setPresaleActive(bool isActive) public onlyOwner{
        preSaleActive = isActive;
    }

    function setSaleActive(bool isActive) public onlyOwner {
        standardSaleActive = isActive;
    }

    //Price setting
    function setPrice(uint256 givenPrice) external onlyOwner {
        pricePerPiece = givenPrice;
    }

    //Withdrawal
    function setWithdrawalAddress(address payable givenWithdrawalAddress) public onlyOwner {
        withdrawalAddress = givenWithdrawalAddress;
    }

    function withdrawEth() public onlyOwner {
        Address.sendValue(withdrawalAddress, address(this).balance);
    }

    //Owner info
    function tokenInfo(uint256 tokenId) public view returns (address) {
        return (ownerOf(tokenId));
    }

    function getOwners(uint256 start, uint256 end) public view returns (address[] memory){
        address[] memory re = new address[](end - start);
        for (uint256 i = start; i < end; i++) {
                re[i - start] = ownerOf(i);
        }
        return re;
    }

    //Update the token counter position
    function setTokenCounter(uint256 givenTokenCounter) public onlyOwner{
        tokenCounter = givenTokenCounter;
    }

    function artistMint(uint256 tokenId) public onlyOwner {
        reservedTokens[tokenId] = 1;
        _safeMint(msg.sender, tokenId);
    }
    
    function earlyMint() public payable {
        require(preSaleActive, "early sale not open");
        require(whitelisted[msg.sender] > 0, "address not on whitelist ");
        require(whitelisted[msg.sender] > preSaleMinted[msg.sender], "exceeded number allowed");
        preSaleMinted[msg.sender] += 1;
        mintItem();
    }

    function mint() public payable {
        require(standardSaleActive || msg.sender == owner(), "sale not open");
        mintItem();
    }

    function mintItem() private returns (uint256 [] memory) {
        require(msg.value >= pricePerPiece, "not enough eth sent");
        require(msg.value <= 1000000000000000000, "cannot send in more than 1 eth");
                             
        uint256 amount = msg.value / pricePerPiece;  
        //amount will always be at least one so we need to add one to our max for proper comparison
        require(tokenCounter + amount <= maxNumberOfPieces + 1, " tokens sold out or too many tokens requested ");
        uint256 [] memory tokensMinted = new uint256[](amount);

        for(uint256 i = 0; i < amount; i ++){
            
            //If the token has been reserved or already minted then skip it 
            while(reservedTokens[tokenCounter] > 0 || _exists(tokenCounter)){
                tokenCounter += 1;
            }

            _safeMint(msg.sender, tokenCounter);
            //save minted tokens to list
            tokensMinted[i] = tokenCounter;
            tokenCounter += 1;
        }

        emit Purchase(msg.sender, msg.value, tokensMinted);
        return tokensMinted;
    }
}
/**
 * @dev {ERC721} token, including:
 *
 *  - ability for holders to burn (destroy) their tokens
 *  - a minter role that allows for token minting (creation)
 *  - token ID and URI autogeneration
 *
 * This contract uses {AccessControl} to lock permissioned functions using the
 * different roles - head to its documentation for details.
 *
 * The account that deploys the contract will be granted the minter and pauser
 * roles, as well as the default admin role, which will let it grant both minter
 * and pauser roles to other accounts.
 */
function setWithdrawalAddress(address payable givenWithdrawalAddress) public onlyOwner {
    withdrawalAddress = givenWithdrawalAddress;
}

//Withdrawal