contract WeirdElves is ERC721Enumerable, Ownable, CybermonValues {
    using Address for address;
    using Strings for uint256;
    
    uint256 public NFT_PRICE_PUBLIC = 35000000000000000; // 0.035 ETH
    uint256 public NFT_PRICE_WHITELIST = 30000000000000000; // 0.03 ETH
    uint256 public constant MAX_NFT_PURCHASE = 20;
    uint256 public constant MAX_NFT_WHITELIST = 10;
     uint256 public constant MAX_SUPPLY = 10000;
    
    bool public whitelistPhase = false;
    bool public freePhase = false;    
    bool public publicSalePhase = false;
    bool public fusionPhase = false;

    string private _baseTokenURI;
    address private cybermonContract;
    uint256 public mintedEliteElves = 0;
    mapping(uint256 => bool) _cybermonTokensMinted;
    mapping (uint256 => string) _tokenURIs;
    mapping(address => bool) _whiteList;
   
    constructor() ERC721("WeirdElves","WRDELVS"){
        cybermonContract = 0x31e88992C8F25417B11c72E1344941c9113b4998;
    }
    
    function toggleWhitelistPhase() public onlyOwner {
        whitelistPhase = !whitelistPhase;
    }
    
    function toggleFreePhase() public onlyOwner {
        freePhase = !freePhase;
    } 
    
    function togglePublicSalePhase() public onlyOwner {
        publicSalePhase = !publicSalePhase;
    }
    
    function toggleFusionPhase() public onlyOwner {
        fusionPhase = !fusionPhase;
    }
    
    function airdrop(address to, uint256 numberOfTokens) public onlyOwner {
        uint256 totalSupply = totalSupply();
        
        for (uint256 i = 0; i < numberOfTokens; i++) {
            _safeMint(to, totalSupply + i);
        }
    }

    function mintChecks(uint256 numberOfTokens, uint256 msgVal, address sender, bool whiteList) private view {
        uint256 totalSupply = totalSupply();
        require(numberOfTokens > 0, "Amount too small");
        require((totalSupply + numberOfTokens) <= MAX_SUPPLY, "Purchase exceeding max supply");        

        if(whiteList) {
            require(whitelistPhase, "You are not able to mint");
            require(isWhitelisted(sender), "You are not whitelisted");
            require((MAX_NFT_WHITELIST - balanceOf(msg.sender)) >= numberOfTokens, "Cant mint that much");
            require(NFT_PRICE_WHITELIST * numberOfTokens == msgVal, "Sent ether val is incorrect");
        }
        else {
            require(publicSalePhase, "You are not able to mint");
            require(numberOfTokens <= MAX_NFT_PURCHASE, "Max 20 tokens per trx");     
            require(NFT_PRICE_PUBLIC * numberOfTokens == msgVal, "Sent ether val is incorrect");       
        }
    }
    
    function freeMint(uint256 numberOfTokens) public {
        uint256 totalSupply = totalSupply();
        require(freePhase, "Free sale not started yet");
        require(numberOfTokens > 0, "Amount too small");
        require((totalSupply + numberOfTokens) <= MAX_SUPPLY, "Purchase exceeding max supply");
        
        uint256[] memory mintableTokens = getMintableTokens(msg.sender, numberOfTokens);
        
        for (uint256 i = 0; i < numberOfTokens; i++) {
           _safeMint(msg.sender, totalSupply + i);
           _cybermonTokensMinted[mintableTokens[i]] = true;
        }
    }

    function mint(uint256 numberOfTokens, bool whitelist) public payable {
        uint256 totalSupply = totalSupply();
        mintChecks(numberOfTokens, msg.value, msg.sender, whitelist);        

        for (uint256 i = 0; i < numberOfTokens; i++) {
            _safeMint(msg.sender, totalSupply + i);
        }
    }
    
    function fuse(uint256 tokenId1, uint256 tokenId2) public {
       require(fusionPhase, "Cant fuse elves at the moment");
       require(msg.sender == ownerOf(tokenId1), "This isnt your NFT");
       require(msg.sender == ownerOf(tokenId2), "This isnt your NFT");
       require(tokenId1 < MAX_SUPPLY , "Cant fuse elite elve");
       require(tokenId2 < MAX_SUPPLY , "Cant fuse elite elve");
       
       _burn(tokenId1);
       _burn(tokenId2);
       
       _safeMint(msg.sender, MAX_SUPPLY + mintedEliteElves);
       mintedEliteElves++;
    }
    
    function getMintableTokens(address sender, uint256 numberOfTokens) private returns(uint256[] memory) {
        Cybermon c = Cybermon(cybermonContract);
        creatureStat[] memory creatures = c.walletOfOwner(sender);
        uint256[] memory mintableTokens = new uint256[](creatures.length);
        
        uint256 idx = 0;
        for (uint256 i = 0; i < creatures.length; i++) {
            if(_cybermonTokensMinted[creatures[i].tokenId] == false) {
                mintableTokens[idx] = creatures[i].tokenId;
                idx++;
            }
        }
        require(idx >= numberOfTokens, "You dont own enough Cybereggs");
        return mintableTokens;
    }
    
    function isWhitelisted(address sender) public view returns (bool) {
        if(_whiteList[sender] && whitelistPhase)
            return true;
        
        return false;
    }
   
    function walletOfOwner(address _owner) public view returns (uint256[] memory)
    {
        uint256 tokenCount = balanceOf(_owner);
        uint256[] memory tokenIds = new uint256[](tokenCount);
        for (uint256 i = 0; i < tokenCount; i++) {
            tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
        }
        return tokenIds;
    }

    function eliteHolders() public view onlyOwner returns (address[] memory)
    {
        uint256 tokenCount = totalSupply() - MAX_SUPPLY;
        address[] memory owners = new address[](tokenCount);
        for (uint256 i = 0; i < tokenCount; i++) {
            owners[i] = ownerOf(MAX_SUPPLY + i);
        }
        return owners;
    }
    
    function addToWhiteList(address[] memory accounts) external onlyOwner{
        require(accounts.length > 0, "Invalid input");
        for(uint256 index = 0; index < accounts.length; index++){
            _whiteList[accounts[index]] = true;
        }
    }
    
    function removeFromWhiteList(address[] memory accounts) external onlyOwner{
        require(accounts.length > 0, "Invalid input");
        for(uint256 index = 0; index < accounts.length; index++){           
            _whiteList[accounts[index]] = false;
        }
    }
    
    function withdraw() public payable onlyOwner {
        uint256 balance = address(this).balance;
        payable(msg.sender).transfer(balance);
    }    

    function setPrice(uint256 price, bool whitelist) public onlyOwner {
        if(whitelist)
            NFT_PRICE_WHITELIST = price;
        else
            NFT_PRICE_PUBLIC = price;
    }
    
    function getBalance() public view onlyOwner returns (uint256){
        return address(this).balance;
    } 

    // Sets base URI for all tokens, only able to be called by contract owner
    function setBaseURI(string memory baseURI_) public onlyOwner {
        _baseTokenURI = baseURI_;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        // Concatenate the tokenID to the baseURI.
        return string(abi.encodePacked(_baseTokenURI, tokenId.toString()));
    }
}
function setBaseURI(string memory baseURI_) public onlyOwner {
    _baseTokenURI = baseURI_;
}

// Sets base URI for all tokens, only able to be called by contract owner