contract KONGS is IKONGS, ERC721Enumerable, Ownable, Pausable {
    uint256 public MINT_PRICE = .02 ether;
    uint256 public MAX_TOKENS;
    uint256 public PAID_TOKENS;
    uint16 public minted;
    uint256 reserved;
    uint256 reserveLimit;
    uint256 nonReserved;

    struct whitelist{
        uint256 amount;
        }

    mapping (address => whitelist) whitelisted;
    mapping (address => whitelist) reservedMinters;
    bool whitelistMode;

 
    mapping(uint256 => uint256) private mintBlock;
    mapping(uint256 => KaijuKong) private tokenTraits;
    uint8[][19] public rarities;
    uint8[][19] public aliases;
    // reference to the KongIsland for choosing random Kong thieves
    IKongIsland public kongIsland;
    // reference to $FOOD for burning on mint
    IFOOD public food;
    // reference to Traits
    ITraits public traits;


    /**
     * instantiates contract and rarity tables
     */
    constructor(
        address _food,
        address _traits,
        uint256 _maxTokens,
        uint256 _reserved
    ) ERC721("Kong Game", "KGAME") {
        food = IFOOD(_food);
        traits = ITraits(_traits);
        MAX_TOKENS = _maxTokens;
        PAID_TOKENS = _maxTokens / 5;
        nonReserved = _reserved;
        reserveLimit = _reserved;
        rarities[0] = [255, 200, 150, 100, 50];
        aliases[0] = [0, 1, 2, 3, 4];
        rarities[1] = [255,200,160,130,96,63];
        aliases[1] = [0,1,3,3,2,0];
        rarities[2] = [255,72,52,12,6];
        aliases[2] = [0,1,2,1,0];
        rarities[3] = [255];
        aliases[3] = [0];

        rarities[4] = [255];
        aliases[4] = [0];
        rarities[5] = [255];
        aliases[5] = [0];
        rarities[6] = [255];
        aliases[6] = [0];
        rarities[7] = [255];
        aliases[7] = [0];
        rarities[8] = [255];
        aliases[8] = [0];
        rarities[9] = [255];
        aliases[9] = [0];
        rarities[10] = [255];
        aliases[10] = [0];
        rarities[11] = [255];
        aliases[11] = [0];
        rarities[12] = [255];
        aliases[12] = [0];

        rarities[13] = [255,165,51,43,18,5];
        aliases[13] = [0,1,1,3,4,0];


        rarities[14] = [255,37,15];
        aliases[14] = [0,1,0];

        rarities[15] = [255,57,23];
        aliases[15] = [0,1,0];

        rarities[16] = [255,70];
        aliases[16] = [0,1];


        rarities[17] = [255,37,15];
        aliases[17] = [0,1,0];

        rarities[18] = [255,165,51,43,18,5];
        aliases[18] = [0,1,2,3,4,0];
    }

    /** EXTERNAL */
    function mint(uint256 amount) external payable whenNotPaused {
    require(amount <= 5, "Maxmimum 5 mints at a time");
    require(tx.origin == _msgSender(), "Only EOA");
    require(minted + amount <= MAX_TOKENS, "All tokens minted");
    if(whitelistMode){require(amount <= whitelisted[_msgSender()].amount, "Whitelisted can mint maximum of 3");}
    if (minted < PAID_TOKENS) {
      require(minted + amount <= PAID_TOKENS, "All tokens on-sale already sold");
      require(amount * MINT_PRICE >= msg.value, "Invalid payment amount");
    } else {
      require(msg.value == 0);
    }
    uint256 totalFOODCost = 0;
    uint256 seed;
        for (uint256 i = 0; i < amount; i++) {
            minted++;
            nonReserved++;
            if(whitelistMode){whitelisted[ _msgSender()].amount--;}
	        mintBlock[nonReserved] = block.number;
            seed = random(nonReserved);
            generate(nonReserved, seed);
            address recipient = selectRecipient(seed);
            totalFOODCost += mintCost(nonReserved);
            _safeMint(recipient, nonReserved);
        }
        if (totalFOODCost > 0) food.burn(_msgSender(), totalFOODCost);
    }

    function reservedMint(uint256 amount) external {
    require(amount <= 5, "Maxmimum 5 mints at a time");
    require(amount <= reservedMinters[msg.sender].amount);
    uint256 seed;
        for (uint256 i = 0; i < amount; i++) {
            minted++;
            reserved++;
	        mintBlock[reserved] = block.number;
            seed = random(reserved);
            generate(reserved, seed);
            _safeMint(msg.sender,reserved);
        }
    }

  function mintCost(uint256 tokenId) public view returns (uint256) {
    if (tokenId <= PAID_TOKENS) return 0;
    if (tokenId <= MAX_TOKENS * 2 / 5) return 20000 ether;
    if (tokenId <= MAX_TOKENS * 3 / 5) return 40000 ether;
    if (tokenId <= MAX_TOKENS * 4 / 5) return 80000 ether;
    return 100000 ether;
  }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        if (_msgSender() != address(kongIsland)) {
            require(
                _isApprovedOrOwner(_msgSender(), tokenId),
                "ERC721: transfer caller is not owner nor approved"
            );
        }
        _transfer(from, to, tokenId);
    }

    /** INTERNAL */

    function generate(uint256 tokenId, uint256 seed)
        internal
        returns (KaijuKong memory t)
    {
        t = selectTraits(seed);
        tokenTraits[tokenId] = t;
        return t;
    }

    /**
     * uses A.J. Walker's Alias algorithm for O(1) rarity table lookup
     * ensuring O(1) instead of O(n) reduces mint cost by more than 50%
     * probability & alias tables are generated off-chain beforehand
     * @param seed portion of the 256 bit seed to remove trait correlation
     * @param traitType the trait type to select a trait for
     * @return the ID of the randomly selected trait
     */
    function selectTrait(uint16 seed, uint8 traitType)
        internal
        view
        returns (uint8)
    {
        uint8 trait = uint8(seed) % uint8(rarities[traitType].length);
        if (seed >> 9 < rarities[traitType][trait]) return trait;
        return aliases[traitType][trait];
    }

    /**
     * the first 20% (ETH purchases) go to the minter
     * the remaining 80% have a 10% chance to be given to a random staked Kong
     * @param seed a random value to select a recipient from
     * @return the address of the recipient (either the minter or the Kong thief's owner)
     */

     
  function selectRecipient(uint256 seed) internal view returns (address) {
    if (minted <= PAID_TOKENS || minted < 30000 && ((seed >> 245) % 10) != 0 || minted >30000 && ((seed >> 245) % 10) >=2) return _msgSender(); // top 10 bits haven't been used
    address thief = kongIsland.randomKongOwner(seed >> 144); // 144 bits reserved for trait selection
    if (thief == address(0x0)) return _msgSender();
    return thief;
  }

    /**
     * selects the species and all of its traits based on the seed value
     * @param seed a pseudorandom 256 bit number to derive traits from
     * @return t -  a struct of randomly selected traits
     */
    function selectTraits(uint256 seed)
        internal
        view
        returns (KaijuKong memory t)
    {
        t.isKaiju = (seed & 0xFFFF) % 10 != 0;
        uint8 shift = t.isKaiju ? 0 : 9;
        seed >>= 16;
        t.background = selectTrait(uint16(seed & 0xFFFF), 0);
        seed >>= 16;
        t.species = selectTrait(uint16(seed & 0xFFFF), 1 + shift);
        seed >>= 16;
        t.companion = selectTrait(uint16(seed & 0xFFFF), 2 + shift);
        seed >>= 16;
        t.kingScore = selectTrait(uint16(seed & 0xFFFF), 9 + shift);
        seed >>= 16;
        t.hat = selectTrait(uint16(seed & 0xFFFF), 5 + shift);
        seed >>= 16;
        t.face = selectTrait(uint16(seed & 0xFFFF), 6 + shift);
        seed >>= 16;
        t.weapons = selectTrait(uint16(seed & 0xFFFF), 7 + shift);
        seed >>= 16;
        t.accessories = selectTrait(uint16(seed & 0xFFFF), 8 + shift);
        seed >>= 16;
        t.fur = t.kingScore;
    }

    function structToHash(KaijuKong memory s) internal pure returns (uint256) {
        return
            uint256(
                bytes32(
                    abi.encodePacked(
                        s.isKaiju,
                        s.background,
                        s.species,
                        s.companion,
                        s.fur,
                        s.hat,
                        s.face,
                        s.weapons,
                        s.accessories,
                        s.kingScore
                    )
                )
            );
    }

  function random(uint256 seed) internal view returns (uint256) {
    return uint256(keccak256(abi.encodePacked(
      tx.origin,
      blockhash(block.number - 1),
      block.timestamp,
      seed
    )));
  }

    /** READ */

    function getTokenTraits(uint256 tokenId)
        external
        view
        override
        returns (KaijuKong memory)
    {
        require(mintBlock[tokenId] + 1 < block.number);
        return tokenTraits[tokenId];
    }

    function getPaidTokens() external view override returns (uint256) {
        return PAID_TOKENS;
    }

    function isKaiju(uint256 tokenId) external view returns (bool){
        require(mintBlock[tokenId] + 1 < block.number);
        return(tokenTraits[tokenId].isKaiju);
    }

    function KingScore(uint256 tokenId) external view returns (uint8) {
        require(mintBlock[tokenId] + 1 < block.number);
        return(tokenTraits[tokenId].kingScore);
    }

    function getIDsOwnedby(address _address) external view returns(uint256[] memory) {
        uint256[] memory tokensOwned = new uint256[](balanceOf(_address));
        for(uint256 i = 0; i < tokensOwned.length; i++) {
            tokensOwned[i] = tokenOfOwnerByIndex(_address, i);
        }
        return tokensOwned;
    }

    /** ADMIN */

    function setKongIsland(address _KongIsland) external onlyOwner {
        kongIsland = IKongIsland(_KongIsland);
    }

    function addToWhitelist(address _whitelisted) external onlyOwner {
        whitelisted[_whitelisted].amount = 3;
    }

    function bulkWhitelist(address[] calldata _whitelisted) external onlyOwner {
        for (uint256 i = 0; i < _whitelisted.length; i++) {
        whitelisted[_whitelisted[i]].amount = 3;
        }
    }

    function addToReservedMinters(address _reserved, uint256 _amount) external onlyOwner {
        reservedMinters[_reserved].amount = _amount;
    }

    function bulkWhitelist(address[] calldata _reserved,uint256[] calldata _amount) external onlyOwner {
        for (uint256 i = 0; i < _reserved.length; i++) {
        reservedMinters[_reserved[i]].amount = _amount[i];
        }
    }

    function withdraw() external onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    function whitelistMinting(bool _whitelistMode)external onlyOwner {
        whitelistMode = _whitelistMode;
    }

    function setPaidTokens(uint256 _paidTokens) external onlyOwner {
        PAID_TOKENS = _paidTokens;
    }

    function setPaused(bool _paused) external onlyOwner {
        if (_paused) _pause();
        else _unpause();
    }

  function setMintPrice(uint256 mintprice) external onlyOwner{
        MINT_PRICE = mintprice;
    }

  function updateFoodContract(address _food) external onlyOwner {
        food = IFOOD(_food);
    }

    /** RENDER */

  function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    require(mintBlock[tokenId] + 1 < block.number);
    return traits.tokenURI(tokenId);
  }

}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    require(mintBlock[tokenId] + 1 < block.number);
    return traits.tokenURI(tokenId);
  }
/** RENDER */