contract ConsoleNFT_Cyber_Upgrades is ERC721Enumerable, ReentrancyGuard, Ownable, VRFConsumerBase {

	address dataContract;
    address percentageContract;
    address vaultContract;
    address levelUpContract;

    mapping(uint => bool) private _usedVaults;

    // Mapping for wallet addresses that have previously minted
    mapping(address => uint) private _whitelistMinters;

    // Merkle Proof Hashes for tokens
    bytes32[] public rootHashes;

	uint public constant maxTokens = 5000;

    uint public vaultHolderReserves = 500;

    uint[] public rarities; // mapping of pool rarities in constructor

    uint public total = 0;

	uint mintCost = 0.05 ether;
    uint publicMintCost = 0.1 ether;

    uint public whitelistMintStart;
    uint public publicMintStart;

	bool sysAdminMinted;
	bool error404Minted;
	bool code200Minted;
	bool giveawaysMinted;
	
	address error404Address;
	address code200Address;
	address giveawaysAddress;

    mapping(uint => uint) public levels; // tokenID -> level
    mapping(uint => uint) public rarity; // tokenID -> rarity

    function setMerkleHashes(bytes32[] memory _rootHash) external onlyOwner {
        rootHashes = new bytes32[](0);
        for (uint i=0; i < _rootHash.length; i++) {
            rootHashes.push(_rootHash[i]);
        }
    }

    function setVaultContractAddress(address _vaultContract) external onlyOwner {
        vaultContract = _vaultContract;
    }

    function setPercentageContractAddress(address _percentageContract) external onlyOwner {
        percentageContract = _percentageContract;
    }

    function setWhitelistMintStart(uint _timestamp) external onlyOwner {
        whitelistMintStart = _timestamp;
    }
	
    function setPublicMintStart(uint _timestamp) external onlyOwner {
        publicMintStart = _timestamp;
    }

	function setSourceData(address _dataContract) external onlyOwner {
        dataContract = _dataContract;
    }

    function setLevelUpContract(address _levelUpContract) external onlyOwner {
        levelUpContract = _levelUpContract;
    }

	string[] private rarityText = [
        "Common",
        "Uncommon",
        "Rare",
        "Epic",
        "Legendary"
    ];

    function getRarity(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Nonexistent token");
		
        string memory output;
		
        output = rarityText[rarity[tokenId]];
		
        return output;
    }

    string[] private firstStat = [
        "Health",
        "Strength",
        "Speed",
        "Accuracy",
        "Intelligence",
        "Tech",
        "Hack speed",
        "SQL skill",
        "Network",
        "DNA",
        "De-auth",
        "Pwd cracking",
        "XSS forgery",
        "Decrypt",
        "Cryptography"
    ];

    function getFirstStat(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Nonexistent token");
		
        string memory output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = firstStat[source_data.getFirstStat(tokenId, rarity[tokenId])];
		
        return output;
    }

    function getFirstStatValue(uint256 tokenId) public view returns (uint) {
        require(_exists(tokenId), "Nonexistent token");
		
        uint output;
		
		SourceData source_data = SourceData(dataContract);
		
		uint firstStatValue = source_data.getFirstStatValue(tokenId, rarity[tokenId]);
		
        output = (firstStatValue * getItemLevel(tokenId)) + 1;
		
        return output;
    }

    string[] private secondStat = [
        "Lockpicking",
        "Regeneration",
        "Armor",
        "Backpack",
        "Intelligence",
        "Charge",
        "Damage boost",
        "Reflexes",
        "Radar",
        "Keymaster",
        "Pickpocket",
        "EMP shield",
        "EMP power",
        "Run silent",
        "Nanotech"
    ];

    function getSecondStat(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Nonexistent token");
		
        string memory output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = secondStat[source_data.getSecondStat(tokenId, rarity[tokenId])];
		
        return output;
    }

    function getSecondStatValue(uint256 tokenId) public view returns (uint) {
        require(_exists(tokenId), "Nonexistent token");
		
        uint output;
		
		SourceData source_data = SourceData(dataContract);
		
		uint secondStatValue = source_data.getSecondStatValue(tokenId, rarity[tokenId]);
		
        output = (secondStatValue * getItemLevel(tokenId)) + 1;
		
        return output;
    }

    string[] private firstAugment = [
        "Scrapper",
        "Bionic Arms",
        "Detoxifier",
        "Bionic Lungs",
        "Hardened Bones",
        "Berserk",
        "Blood pump",
        "Edge runner",
        "Adrenaline Pump",
        "Bloodware",
        "Cyber Joints",
        "Cloaking",
        "Nanobots",
        "Synthetic Heart",
        "?"
    ];

    function getFirstAugment(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Nonexistent token");
		
        string memory output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = firstAugment[source_data.getFirstAugment(tokenId, rarity[tokenId])];
		
        return output;
    }

    string[] private secondAugment = [
        "Nightvision",
        "Titan Knuckles",
        "Cyber Legs",
        "Cyber Arms",
        "Reflex Boost",
        "Lizard skin",
        "Titanium Bones",
        "Echolocation",
        "Thermal vision",
        "X-ray Vision",
        "Shapeshifter",
        "Exoskeleton",
        "Stealth kit",
        "Double Heart",
        "?"
    ];

    function getSecondAugment(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Nonexistent token");
		
        string memory output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = secondAugment[source_data.getSecondAugment(tokenId, rarity[tokenId])];
		
        return output;
    }

    function getAirdrops(uint256 tokenId) public view returns (uint) {
        require(_exists(tokenId), "Nonexistent token");
		
        uint output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = source_data.getAirdrops(tokenId, rarity[tokenId]) + getItemLevel(tokenId);
		
        return output;
    }
	
    string internal baseTokenURI;

	function setBaseTokenURI(string memory _uri) external onlyOwner {
        baseTokenURI = _uri;
    }

    string internal externalURI;

	function setExternalURI(string memory _uri) external onlyOwner {
        externalURI = _uri;
    }

	function tokenURI(uint _tokenId) public view override returns (string memory) {
        require(_exists(_tokenId), "Query for non-existent token!");

        string[7] memory json;

        json[0] = string(
			abi.encodePacked('{',
                '"name": "Cyber Upgrade #', t(_tokenId), ' (Level: ', t(getItemLevel(_tokenId)), ')",',
                '"image": "', baseTokenURI, t(rarity[_tokenId]), '.jpg",',
                '"external_url": "', externalURI, '?dna=', getDna(_tokenId) ,'&id=', t(_tokenId) ,'",',  
                '"description": "Upgrades waiting to be connected to players.",',
                '"attributes": ['
            )
		);

        json[1] = string(
			abi.encodePacked(
				'{',
					'"trait_type": "Rarity",',
					'"value": "', getRarity(_tokenId) ,'"',
                '},',
                '{',
                    '"trait_type": "Level",',
                    '"value": "', t(getItemLevel(_tokenId)) ,'"',
                '},'
            )
		);
        json[2] = string(
			abi.encodePacked(
				'{',
                    '"trait_type": "Upgrade #1",',
                    '"value": "', getFirstStat(_tokenId) , " +", t(getFirstStatValue(_tokenId)) ,'"',
                '},'
            )
		);

        json[3] = string(
			abi.encodePacked(
				'{',
                    '"trait_type": "Upgrade #2",',
                    '"value": "', getSecondStat(_tokenId) ," +", t(getSecondStatValue(_tokenId)) ,'"',
                '},'
            )
		);
        json[4] = string(
			abi.encodePacked(
				'{',
                    '"trait_type": "Augmentation #1",',
                    '"value": "', getFirstAugment(_tokenId) ,'"',
                '},'
            )
		);
        json[5] = string(
			abi.encodePacked(
                '{',
                    '"trait_type": "Augmentation #2",',
                    '"value": "', getSecondAugment(_tokenId) ,'"',
                '},',
                '{',
                    '"trait_type": "Airdrops",',
					'"value": "x', t(getAirdrops(_tokenId)) ,'"',
                '}'
            )
		);
        json[6] = string(
			abi.encodePacked('],',
                '"animation_url": "', externalURI, '?dna=', getDna(_tokenId) , '&id=', t(_tokenId) , '",',
                '"iframe_url": "', externalURI, '?dna=', getDna(_tokenId) , '&id=', t(_tokenId) , '"',
            '}')
		);

        string memory result = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        json[0],
                        json[1],
                        json[2],
                        json[3],
                        json[4],
                        json[5],
                        json[6]
                    )
                )
            )
        );

        return string(abi.encodePacked("data:application/json;base64,", result));
    }


	
	// helper
	function t(uint _tokenId) public pure returns (string memory) {
		return Strings.toString(_tokenId);
	}


    // Getters
    function getDna(uint256 tokenId) public view returns (string memory) {
        require(_exists(tokenId), "Query for non-existent token!");

        SourceData source_data = SourceData(dataContract);

        // First stat index
        uint firstStatIndex = source_data.getFirstStat(tokenId, rarity[tokenId]);

        // First stat index
        uint secondStatIndex = source_data.getSecondStat(tokenId, rarity[tokenId]);

        string[2] memory dna;

        dna[0] = string(
            abi.encodePacked(
                t(rarity[tokenId]), "-",
                t(levels[tokenId]), "-",
                t(firstStatIndex), "-",
                t(getFirstStatValue(tokenId)), "-"
            )
        );
        
        dna[1] = string(
			abi.encodePacked(
                t(secondStatIndex), "-",
                t(getSecondStatValue(tokenId)), "-",
                t(source_data.getFirstAugment(tokenId, rarity[tokenId])), "-",
                t(source_data.getSecondAugment(tokenId, rarity[tokenId])), "-",
                t(getAirdrops(tokenId))
            )
		);

        string memory result = string(
                    abi.encodePacked(
                        dna[0],
                        dna[1]
                    )
                );

        return string(abi.encodePacked(result));
    }

    function getItemLevel(uint256 tokenId) public view returns (uint) {
        return levels[tokenId];
    }

    function setItemLevel(uint tokenId, uint level) external {
        require(msg.sender == levelUpContract, "You can not call this");
        levels[tokenId] = level;
    }

	function whitelistClaim(bytes32[] memory proof, uint proofNumber) public nonReentrant payable {
        require(whitelistMintStart < block.timestamp, "Whitelist mint not started yet");
        require(maxTokens - total > vaultHolderReserves, "Minting over vault holder limit");
        require(total < maxTokens, "All tokens are already minted");
		require(msg.value == mintCost, "Incorrect mint cost value");
        require(_whitelistMinters[_msgSender()] < 1, "You've already minted");
		
		// Merkle tree validation
		bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
		require(MerkleProof.verify(proof, rootHashes[proofNumber], leaf), "Invalid proof");
		
		uint userTokens = proofNumber * 1000;
                
		// Proceed in minting process
		uint tokenId = getVRFRandomIndex(false, 0, userTokens);
		total++;

		// Set the _whitelistMinters value to tokenId for this address as it has minted
		_whitelistMinters[_msgSender()] = tokenId;

		_safeMint(_msgSender(), tokenId);
		
    }

    function publicClaim(uint numberOfTokens) public nonReentrant payable {
        require(publicMintStart < block.timestamp, "Public mint did not start yet");
        require(maxTokens - total > vaultHolderReserves, "Minting over vault holder limit");
        require(total + numberOfTokens < maxTokens, "All tokens are already minted");
        require(numberOfTokens > 0, "Can not mint zero tokens");
        require(numberOfTokens < 11, "Request exceeds max tokens");
		require(msg.value == publicMintCost * numberOfTokens, "Incorrect mint cost value");

        for (uint i=1; i < numberOfTokens; i++) {
            uint tokenId = getVRFRandomIndex(false, 0, 0);
            total++;
            _safeMint(_msgSender(), tokenId);
        }

    }

    function vaultHolderClaim(bytes32[] memory proof, uint proofNumber, uint[] memory vaultIds) public nonReentrant {
        require(whitelistMintStart < block.timestamp, "Whitelist mint not started yet");
        require(total + vaultIds.length < maxTokens, "Mint over the max tokens limit");

        for (uint i=0; i < vaultIds.length; i++) {
            require(validateVaultOwnership(vaultIds[i]), "Not Vault owner");
            require(!checkUsedVault(vaultIds[i]), "Vault was already used");
        }
		
		// This sender does not necessary need to have tokens, it is optional
        uint userTokens = 0;

        // Merkle tree for getting tokens
		bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
		
		if (MerkleProof.verify(proof, rootHashes[proofNumber], leaf) ) {

			userTokens = proofNumber * 1000;

		}

        for (uint i=0; i < vaultIds.length; i++) {

            // get vault rarity
            uint tokenId = getVRFRandomIndex(true, getVaultRarity(vaultIds[i]), userTokens);
            _usedVaults[vaultIds[i]] = true;

            total++;
            vaultHolderReserves--;
            _safeMint(_msgSender(), tokenId);
        }

    }
    
    function vaultHolderClaimCombined(bytes32[] memory proof, uint proofNumber, uint[] memory vaultIds) public nonReentrant payable {
        require(whitelistMintStart < block.timestamp, "Whitelist mint not started yet");
        require(total + 1 + vaultIds.length < maxTokens, "Mint over the max tokens limit");
        require(msg.value == mintCost, "Incorrect mint cost value");
        require(_whitelistMinters[_msgSender()] < 1, "You've already minted");

        for (uint i=0; i < vaultIds.length; i++) {
            require(validateVaultOwnership(vaultIds[i]), "Not Vault owner");
            require(!checkUsedVault(vaultIds[i]), "Vault was already used");
        }

        uint userTokens = 0;

        // Merkle tree for getting tokens
		bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
		require(MerkleProof.verify(proof, rootHashes[proofNumber], leaf), "Invalid proof");
		
        userTokens = proofNumber * 1000;

        // Continue minting process
        for (uint i=0; i < vaultIds.length; i++) {
            uint tokenId = getVRFRandomIndex(true, getVaultRarity(vaultIds[i]), userTokens);
            _usedVaults[vaultIds[i]] = true;

            total++;
            vaultHolderReserves--;
            _safeMint(_msgSender(), tokenId);
        }

        // + 1 for whitelist
        uint additionalTokenId = getVRFRandomIndex(false, 0, userTokens);
        total++;
        _whitelistMinters[_msgSender()] = additionalTokenId;
        _safeMint(_msgSender(), additionalTokenId);

    }

    function validateVaultOwnership(uint _vaultID) internal view returns(bool) {
        ERC721 vaultContractData = ERC721(vaultContract);
        if(vaultContractData.ownerOf(_vaultID) == msg.sender) {
            return true;
        }
        return false;
    }

    function checkUsedVault(uint _vaultID) internal view returns(bool) {
        if (_usedVaults[_vaultID] == true) {
            return true;
        }
        return false;
    }

    function vaultCheck(uint _vaultID) public view returns (bool) {
        if (_usedVaults[_vaultID] == true) {
            return true;
        }
        return false;
    }
	
    function getVaultRarity(uint _vaultID) public view returns (uint) {

		uint output;
		
		SourceData source_data = SourceData(dataContract);
		
        output = source_data.getVaultRarity(_vaultID);
		
        return output;

    }

	function sysAdminClaim() public onlyOwner nonReentrant {
		require(sysAdminMinted == false, "Already minted!");
		
		sysAdminMinted = true;
		uint tokenId = getVRFRandomIndex(false, 0, 0);
        total++;
        _safeMint(owner(), tokenId);
    }

	function error404Claim() public nonReentrant {
		require(msg.sender == error404Address, "Not Error 404");
		require(error404Minted == false, "Already minted!");
		
		error404Minted = true;
		uint tokenId = getVRFRandomIndex(false, 0, 0);
        total++;
        _safeMint(msg.sender, tokenId);
    }
	
	function code200Claim() public nonReentrant {
		require(msg.sender == code200Address, "Not Code 200");
		require(code200Minted == false, "Already minted!");
		
		code200Minted = true;
		uint tokenId = getVRFRandomIndex(false, 0, 0);
        total++;
        _safeMint(msg.sender, tokenId);
    }
	
	function giveawaysClaim() public nonReentrant {
		require(msg.sender == giveawaysAddress, "Not Giveaways wallet");
		require(giveawaysMinted == false, "Already minted!");
		
		giveawaysMinted = true;

        for (uint i=0; i < 20; i++) {
            uint tokenId = getVRFRandomIndex(false, 0, 0);
            total++;
            _safeMint(msg.sender, tokenId);
        }
		
    }
	
    constructor() ERC721("Console NFT Cyber Upgrades", "Cnsl-NFT-U") VRFConsumerBase(
        0xf0d54349aDdcf704F77AE15b96510dEA15cb7952,
        0x514910771AF9Ca656af840dff83E8264EcF986CA
    ) 
	Ownable() {
        keyHash = 0xAA77729D3466CA35AE8D28B3BBAC7CC36A5031EFDC430821C02BC31A238AF445;	
        fee = 2 * (10**18);
		
		error404Address = 0x24Db9e45f6aC29175030A083B985C184A02c2d64;
		code200Address = 0x1C0d3B190B18b4452BD4d0928D7f425eA9A0B3F9;
		giveawaysAddress = 0x7e95c71bDF0E0526eA534Fb5191ceD999190c117;

        whitelistMintStart = 1644330600; // 8. february 2022 - 14:30 UTC
        publicMintStart = 1644359400; // 8. february 2022 - 23:30 UTC (+8 hours)
        
        rarities.push(3250); // Common - 65%
        rarities.push(1000); // Uncommon - 20%
        rarities.push(475); // Rare - 9.5%
        rarities.push(225); // Epic - 4.5%
        rarities.push(50); // Legendary - 1%
    }


	////////////////////////// VRF /////////////////////////////
	
    bytes32 internal keyHash;
    uint internal fee;
    uint internal randomResult;
    
    // VRF Functions
    function getRandomNumber() public onlyOwner returns (bytes32 requestId) {
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK!");
        return requestRandomness(keyHash, fee);
    }
    function fulfillRandomness(bytes32 requestId, uint randomness) internal override {
        randomResult = randomness;
    }
    function getRandomResult() public view onlyOwner returns (uint) {
        return randomResult;
    }

    // >>> Now, the VRF is stored in [uint internal randomResult]
    ////////////////////////// VRF /////////////////////////////

    /////////////////////// Token ID Generator ////////////////////////

    uint[maxTokens] internal indices;
    uint32 internal nonce;

    function getVRFRandomIndex(bool hasVault, uint vaultRarity, uint _userTokens) internal returns (uint) {
        require(randomResult != 0, "VRF Random Result has not been set!");

        // Get the random token ID
        uint _tokensRemaining = maxTokens - total; // require that this calculation is possible from all caller functions
        uint _maxIndex = _tokensRemaining == 0 ? 0 : _tokensRemaining - 1; // shorthand if for safety
        uint _rand = uint(keccak256(abi.encodePacked(randomResult, nonce, msg.sender, block.difficulty, block.timestamp))) % _tokensRemaining;
    
        uint _output = 0;

        _output = indices[_rand] != 0 ? indices[_rand] :_rand;
        indices[_rand] = indices[_maxIndex] == 0 ? _maxIndex : indices[_maxIndex];

        uint32 _nonceAdd = uint32(uint256(keccak256(abi.encodePacked(randomResult, nonce, msg.sender, block.difficulty, block.timestamp)))) % 10;
        nonce += _nonceAdd; 


        // Get the data from Percentage Contract
        PercentageGenerator percentage = PercentageGenerator(percentageContract);
        uint[] memory percentageArray = percentage.calculatePercentage(hasVault, vaultRarity, _userTokens);

        // Construct view array of 10k items to pick from
        // Because of rounding errors there might be close but not exactly 10k items, this should be safe
        uint totalItems;
        uint[] memory percentageNumbers = new uint[](5);
        for (uint i=0; i < percentageArray.length; i++) {
            totalItems += percentageArray[i];
            percentageNumbers[i] = totalItems;
        }

        // VRF guess the number in 10k items to get the rarity
        uint _randRarity = uint(keccak256(abi.encodePacked(randomResult, nonce, msg.sender, block.difficulty, block.timestamp))) % totalItems;

        uint rarityResult = 0; // Common as base

        // Get the rarity based on the VRF result
        if (_randRarity >= percentageNumbers[0] && _randRarity < percentageNumbers[1]) {
            rarityResult = 1; // Uncommon
        }
        if (_randRarity >= percentageNumbers[1] && _randRarity < percentageNumbers[2]) {
            rarityResult = 2; // Rare
        }
        if (_randRarity >= percentageNumbers[2] && _randRarity < percentageNumbers[3]) {
            rarityResult = 3; // Rpic
        }
        if (_randRarity >= percentageNumbers[3] && _randRarity <= percentageNumbers[4]) {
            rarityResult = 4; // Legendary
        }

        // If rarity no longer exist in the pool, give lower rarity
        if (rarities[rarityResult] > 0) {
            
            rarities[rarityResult]--;

            // There is still enough in the rarity pool
            // Assign rarity to this token ID
            rarity[_output] = rarityResult;

            // Assign starting level
            levels[_output] = 1;
        }
        else {
            // give other rarity, up or down
            rarity[_output] = pickOtherRarity(rarityResult);
        }

        return _output;
    }

    function pickOtherRarity(uint rarityResult) internal returns (uint) {
		
		bool direction; // default false = down
		
        if (rarities[rarityResult] > 0) {
            return rarityResult;
        }
        else {
            if(rarityResult == 4){	
                direction = false;
            }
            if(rarityResult == 0){
                direction = true;
        	}
            if(direction == true){ 
                rarityResult++;
            } else {
                rarityResult--;
            }
            if( rarities[rarityResult] > 0 ) {	
                return rarityResult;
            } else {
                return pickOtherRarity(rarityResult);
            }
        }

    }

    /////////////////////// Token ID Generator ////////////////////////
	
	// Withdraw Ether
    function withdrawEther() public onlyOwner {
        payable(msg.sender).transfer(address(this).balance); 
    }

}
function withdrawEther() public onlyOwner {
    payable(msg.sender).transfer(address(this).balance); 
}

// Withdraw Ether