contract MonsterMash is ERC721, Ownable {
    using SafeMath for uint256;
    uint256 public constant MAX_Supply = 10000;
    uint256 private scopeIndex = 0;
    uint256 public price = 80000000000000000; // 0.08 Eth
    uint256 public reservedTokens = 0; // reserve tokens go here
    uint256 public mintedPresaleTokens = 0;
    bool public flagreserve = false;

    bool public hasSaleStarted = false;
    bool public hasPresaleStarted = false;

    mapping(uint256 => uint256) swappedIDs;
    
    mapping(address => uint256) userCredit;
   
   // Whitelisting
   uint256 public allowListMaxMint = 5;
   uint256 public allowListMaxReserve = 50;
    
    mapping(address => bool) private _allowList;
    mapping(address => uint256) private _allowListClaimed;
    

    constructor(string memory baseURI) ERC721("MonsterMash","MM")  {
        setBaseURI(baseURI);
    }
    
    //   WHITELIST FNS
    
    function addToAllowList(address[] calldata addresses) external  onlyOwner {
      for (uint256 i = 0; i < addresses.length; i++) {
      require(addresses[i] != address(0), "Can't add the null address");

      _allowList[addresses[i]] = true;
      /**
      * @dev We don't want to reset _allowListClaimed count
      * if we try to add someone more than once.
      */
      _allowListClaimed[addresses[i]] > 0 ? _allowListClaimed[addresses[i]] : 0;
    }
  }

  function onAllowList(address addr) external view  returns (bool) {
    return _allowList[addr];
  }

  function removeFromAllowList(address[] calldata addresses) external  onlyOwner {
    for (uint256 i = 0; i < addresses.length; i++) {
      require(addresses[i] != address(0), "Can't add the null address");

      /// @dev We don't want to reset possible _allowListClaimed numbers.
      _allowList[addresses[i]] = false;
    }
  }

  /**
  * @dev We want to be able to distinguish tokens bought during isAllowListActive
  * and tokens bought outside of isAllowListActive
  */
  function allowListClaimedBy(address owner) external view  returns (uint256){
    require(owner != address(0), 'Zero address not on Allow List');

    return _allowListClaimed[owner];
  }
  
  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    function tokensOfOwner(address _owner) external view returns(uint256[] memory ) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function genID() private returns(uint256) {
        uint256 scope = MAX_Supply-scopeIndex;
        uint256 swap;
        uint256 result;

        uint256 i = randomNumber() % scope;

        //Setup the value to swap in for the selected ID
        if (swappedIDs[scope-1] == 0){
            swap = scope-1;
        } else {
            swap = swappedIDs[scope-1];
        }

        //Select a random ID, swap it out with an unselected one then shorten the selection range by 1
        if (swappedIDs[i] == 0){
            result = i;
            swappedIDs[i] = swap;
        } else {
            result = swappedIDs[i];
            swappedIDs[i] = swap;
        }
        return result+1;
    }

    
    function randomNumber() private view returns(uint256){
        return uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp)));
    }

    function mint(uint256 numberOfTokens) public payable  {
        require(hasSaleStarted == true, "Sale hasn't started");
        require(numberOfTokens <= 20, "Can only mint 20 tokens at a time");
        require(totalSupply().add(numberOfTokens) <= MAX_Supply, "Exceeds MAX_Supply");
        require(price.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
        
        
        for (uint i = 0; i < numberOfTokens; i++) {
            _safeMint(msg.sender, genID());
            scopeIndex++;
        }
    }
    
    // whitelist Mint function
    
    function mintWhitelist(uint256 numberOfTokens) public payable {
        require(hasPresaleStarted == true, "Presale hasn't started");
        require(totalSupply().add(numberOfTokens) <= MAX_Supply, "Exceeds MAX_Supply");
        require(price.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
        require(_allowList[msg.sender], 'You are not on the White List');
        require(numberOfTokens <= allowListMaxMint, 'Cannot purchase this many tokens');
        require(mintedPresaleTokens + numberOfTokens <= 4000, 'Total Purchase exceeds max allowed');
        
        for (uint i = 0; i < numberOfTokens; i++) {
            _allowListClaimed[msg.sender] += 1;
            _safeMint(msg.sender, genID());
            scopeIndex++;
            mintedPresaleTokens++;
        }
    }
    
    // Mint Reserved tokens
    
    function mintReservedTokens(address user) external  onlyOwner  {
      
        require(flagreserve == false, 'Reserved Allocated tokens');
        
        for (uint i = 0; i < 50; i++) {
            _safeMint(user, genID());
            scopeIndex++;
           
        }
        flagreserve =true;
    }
    
    function getScopeIndex() public view returns(uint256) {
        return scopeIndex;
    }
    
    function mintCredit() public {
        require(hasSaleStarted == true, "Sale hasn't started");
        require(totalSupply().add(1) <= MAX_Supply, "Exceeds MAX_Supply");
        require(userCredit[msg.sender] >= 1, "No Credits");

        _safeMint(msg.sender, genID());
        scopeIndex++;
        userCredit[msg.sender] -= 1;
    }

    function balanceOfCredit(address owner) public view virtual returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return userCredit[owner];
    }

    // Owner Functions

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

    function startSale() public onlyOwner {
        hasSaleStarted = true;
    }
    
    function pauseSale() public onlyOwner {
        hasSaleStarted = false;
    }
    
    function startPresale() public onlyOwner {
        hasPresaleStarted = true;
    }
    
    function pausePresale() public onlyOwner {
        hasPresaleStarted = false;
    }

    function withdrawAll() public onlyOwner {
        msg.sender.transfer(address(this).balance);
    }
    
    function addCredit(address owner, uint256 credits) public onlyOwner {
        userCredit[owner] += credits;
    }
}
/**
 * @title MonsterMash contract
 * @dev Extends ERC721 Non-Fungible Token Standard basic implementation
 */
function mintReservedTokens(address user) external  onlyOwner  {
      
    require(flagreserve == false, 'Reserved Allocated tokens');
        
    for (uint i = 0; i < 50; i++) {
        _safeMint(user, genID());
        scopeIndex++;
           
    }
    flagreserve =true;
}

// Mint Reserved tokens