contract Pencil is ERC20, Ownable {
    TPT public TPTGen1;
    TPT public TPTGen2;
    TPTM public TPTGen3;

    bool public accumulationActive = false;
    bool public accumulationMultiplier = false;
    uint48 public start;
    uint192 public cap;

    struct Level {
        uint64 dailyReward;
        uint64 multiplier;
        uint128 baseClaim;
    }

    struct TokenLengths {
        uint8 level0;
        uint8 level1;
        uint8 level2;
        uint8 level3;
        uint8 level4;
        uint8 level5;
    }

    bool[1953] private _gen1BaseClaimed;
    bool[1953] private _gen2BaseClaimed;
    uint8[1953] private _rewardLevel = [4,4,3,2,4,5,5,1,1,3,2,5,5,5,3,0,4,5,4,4,5,4,1,4,4,0,2,3,0,3,3,3,2,3,2,5,4,5,5,5,3,3,0,2,1,4,4,2,0,3,4,5,5,0,4,3,5,1,4,3,2,3,5,1,2,5,5,5,3,5,5,1,3,0,5,4,3,2,5,5,5,5,0,5,5,2,2,5,5,4,5,3,3,3,4,3,4,4,1,5,3,4,4,2,1,4,0,3,5,3,2,3,5,4,3,4,3,5,5,2,5,4,4,5,5,4,3,5,3,3,1,1,0,5,5,4,5,3,4,4,5,5,4,5,5,2,3,1,3,1,4,2,1,3,3,5,4,5,3,5,5,2,2,5,3,3,4,4,3,1,4,4,2,4,0,5,3,4,2,0,0,3,5,2,5,5,4,3,5,4,4,4,5,5,5,4,3,5,4,5,5,3,2,1,5,2,3,4,4,2,5,4,4,5,5,3,3,4,5,5,1,1,3,5,4,1,3,4,3,5,3,2,4,1,2,5,4,0,0,4,1,0,0,2,5,5,5,4,2,3,5,5,3,4,5,4,3,2,2,4,4,1,4,3,4,4,3,4,5,3,4,4,3,2,5,2,3,3,4,4,4,4,1,5,4,0,4,4,2,3,1,5,3,5,4,3,1,3,5,2,4,1,5,4,2,5,4,3,3,1,5,1,3,4,3,2,5,2,5,5,3,5,1,3,1,5,4,4,4,5,4,1,5,5,0,4,5,1,3,5,2,3,1,4,4,5,4,4,4,5,4,2,3,1,3,4,2,3,1,5,2,3,1,3,4,1,5,2,4,2,5,5,5,1,4,1,4,5,5,4,2,3,2,4,4,3,4,3,0,2,2,4,1,2,1,3,2,3,1,4,4,4,3,3,3,1,3,4,4,4,1,5,3,2,4,4,3,5,4,1,4,2,2,3,3,0,5,5,4,5,5,4,0,4,5,3,3,3,4,4,5,4,2,2,2,1,2,3,3,5,4,5,3,4,4,5,5,5,3,4,1,4,3,4,4,5,4,5,5,3,3,2,2,5,5,4,2,3,3,4,1,4,4,0,2,3,5,5,3,4,4,3,3,5,5,2,4,0,4,2,2,2,5,2,4,1,5,5,4,5,0,5,5,4,5,5,3,0,4,1,5,3,5,1,3,5,5,5,2,3,1,1,2,3,3,5,4,5,2,5,2,1,1,1,3,2,5,3,1,4,2,5,5,2,5,4,4,5,2,5,5,4,5,5,5,1,4,4,5,3,5,4,3,2,1,2,5,5,2,4,0,3,3,4,1,3,3,4,2,5,5,5,4,5,5,4,4,5,3,4,5,4,2,4,3,3,2,0,5,0,3,5,3,3,5,0,3,2,3,3,5,4,1,2,4,2,5,3,0,4,1,1,5,5,3,5,1,3,5,5,4,3,0,1,5,0,5,2,5,4,1,1,5,4,5,3,1,3,5,5,1,4,4,4,1,2,3,1,3,3,3,4,1,5,0,2,5,2,4,5,5,5,3,4,2,0,2,3,3,2,4,3,3,4,2,0,1,4,5,5,4,4,3,1,1,4,3,5,4,1,5,5,4,0,4,4,2,2,5,4,5,4,5,4,3,2,5,4,2,5,5,3,5,1,1,5,3,4,1,5,2,4,5,2,5,2,2,4,5,5,0,1,3,1,4,5,5,5,4,5,3,2,3,4,3,4,2,3,4,2,5,1,2,4,3,4,4,2,4,4,5,4,2,4,2,4,5,5,4,4,4,1,5,1,1,2,1,2,5,5,4,3,2,5,5,3,4,5,0,1,5,3,4,5,5,2,1,5,4,4,5,3,0,1,2,2,1,5,1,5,4,3,5,5,5,4,2,4,4,1,3,3,5,4,0,5,5,5,4,2,2,1,5,4,2,5,4,3,1,3,3,5,2,4,3,5,2,4,5,5,3,2,4,2,5,3,5,5,4,2,4,3,5,3,5,4,0,3,3,4,3,5,2,5,4,1,3,4,0,4,1,5,5,5,4,3,4,4,0,2,5,5,1,3,4,3,5,4,3,2,3,3,2,4,2,4,4,0,5,2,4,2,5,5,4,2,5,0,3,4,5,5,3,5,2,5,2,5,2,5,5,5,5,2,3,4,4,5,1,5,4,4,5,5,5,1,3,5,4,2,1,5,3,3,2,2,5,2,5,0,3,3,4,3,5,3,4,5,5,0,5,5,2,4,5,5,5,4,1,5,5,4,3,5,3,2,5,5,5,3,2,3,5,3,3,5,2,4,1,5,5,3,3,3,4,4,3,5,1,5,3,5,1,3,5,2,5,1,4,5,2,4,3,3,2,3,4,2,5,4,4,0,4,5,1,3,4,4,0,5,5,4,4,4,4,2,3,4,4,0,5,5,3,2,5,3,1,3,3,5,3,5,4,3,5,2,5,5,1,4,3,5,4,5,4,3,4,3,1,3,2,4,3,4,3,4,2,1,1,1,1,2,1,3,2,5,0,3,4,1,3,2,4,4,4,5,4,5,0,2,4,3,2,4,3,3,4,5,5,2,3,4,2,3,1,3,2,3,2,2,1,2,4,3,4,5,2,5,0,4,2,4,5,1,4,2,5,5,4,0,4,4,4,5,4,3,2,4,5,3,3,2,3,5,5,3,4,5,5,5,5,2,2,1,3,5,2,4,2,3,5,0,4,3,5,0,5,4,3,4,4,1,4,4,5,3,2,2,3,2,5,3,1,5,5,5,1,1,0,1,3,2,1,4,2,2,3,2,1,5,2,4,3,3,2,3,0,5,5,5,0,1,5,5,5,3,5,3,4,5,2,0,4,3,3,5,5,2,4,2,3,4,3,3,2,2,1,5,2,5,4,5,5,5,1,3,4,1,5,2,2,5,2,5,2,4,3,3,3,5,4,2,4,4,3,5,4,5,5,5,1,5,4,4,2,3,4,2,3,5,4,3,1,4,3,3,5,4,4,4,5,3,3,3,4,3,4,3,5,5,5,3,3,2,5,3,3,5,2,4,3,1,0,2,5,2,3,4,5,2,5,3,4,4,2,2,5,1,3,5,4,5,0,5,3,4,4,4,2,2,3,5,5,4,3,3,4,2,3,4,3,2,4,2,4,4,4,2,5,5,4,4,2,1,2,2,3,4,3,1,4,4,5,2,5,3,5,5,4,0,4,5,4,5,4,4,5,4,4,5,1,4,2,4,2,0,2,4,3,3,3,1,2,4,4,4,5,5,5,3,4,4,4,5,5,5,2,3,3,2,3,5,4,4,3,0,3,5,3,2,2,1,1,5,3,4,4,5,5,1,3,5,2,4,2,4,5,3,4,5,4,3,0,2,3,2,1,3,5,2,3,5,5,2,2,4,1,3,4,0,4,4,5,2,4,1,5,4,2,3,2,4,1,2,4,3,4,5,2,1,4,3,3,2,3,4,2,3,0,5,4,5,3,3,0,4,4,5,1,4,2,5,4,1,4,3,3,0,0,3,2,4,1,2,5,5,1,5,1,5,3,4,3,5,5,5,5,4,3,5,5,4,5,3,5,2,5,2,5,5,5,4,3,4,4,4,1,1,5,5,4,4,4,4,5,4,4,1,5,0,5,3,5,4,4,4,4,0,0,4,5,5,4,4,3,4,3,5,5,2,3,3,1,3,5,3,0,1,4,3,0,3,5,1,4,3,5,4,5,5,4,1,5,2,1,5,3,4,4,2,4,1,0,2,0,1,4,4,2,4,2,5,5,1,4,3,4,1,2,3,5,5,4,5,4,3,1,3,5,2,3,2,5,1,3,1,4,5,4,4,4,4,3,0,2,2,0,1,2,0,2,5,4,5,2,1,2,4,3,3,2,0,3,2,2,4,5,2,4,3,2,5,1,4,2,4,4,4,4,2,5,4,5,4,4,3,3,5,5,5,1,4,1,5,2,1,4,3,5,1,3,4,3,4,1,0,3,2,5,3,3,4,5,0,5,5,3,3,5,5,3,5,2,4,3,2,3,5,3,3,4,2,4,5,4,5,5,5,1,5,5,4,2,4,1,5,4,5,3,5,4,4,4,4,3,4,2,5,4,5,5,0,0,2,1,1,2,5,4,3,3,5,4,4,5,4,2,3,2,2,3,4,4,3,5,3,3,5,5,1,1,4,1,2,5,2,2,5,3,3,3,4,4,5,4,5,0,5,5,4,5,5,1,4,0,4,4,3,4,2,4,2,5,3,5,4,4,5,5,3,2,4,5,2,4,1,0,2,4,5,3,3,5,1,5,4,5,3,4,5,2,4,3,5,3,3,5,2,4,3,4,4,4,2,2,4,2,3,4,3,5,3,3,5,4,5,5,5,5,5,0,5,3,2,2,4,4,5,5,4,3,4,1];
    uint48[1953] private _lastClaimed;

    mapping(uint256 => Level) public levels;
    mapping(address => bool) public allowedToBurn;

    event BaseRewardClaimed(uint256 gen1TokenId, uint256 gen2TokenId, uint256 amount);
    event RewardClaimed(uint256 gen1TokenId, uint256 amount);

    constructor(address gen1, address gen2) ERC20("Pencil", "PENCIL") {
        TPTGen1 = TPT(gen1);
        TPTGen2 = TPT(gen2);
        cap = 20000000 * 10 ** 18;

        _setLevel(0, 10, 60, 400);  // Einstein
        _setLevel(1,  9, 50, 360);  // Pythagoras
        _setLevel(2,  8, 40, 320);  // Euclid
        _setLevel(3,  7, 30, 280);  // Archimedes
        _setLevel(4,  6, 20, 240);  // Aristotle
        _setLevel(5,  5, 10, 200);  // Gauss

        _mint(_msgSender(), 20000 * 10 ** 18);
    }

    function burn(address user, uint256 amount) external {
        require(allowedToBurn[msg.sender], "Address does not have permission to burn");
        _burn(user, amount);
    }

    // Checks an address for the total amount of base rewards they can claim for owning one or more Gen1 & Gen2 NFTs.
    // Note these NFTs may have already been claimed by a previous owner, as the base claim is tracked by token id.
    function checkBaseReward(address recipient) external view returns (uint256) {
        uint256 gen1Count = TPTGen1.balanceOf(recipient);
        require(gen1Count > 0, "Wallet must own a Genesis NFT");

        uint256 gen2Count = TPTGen2.balanceOf(recipient);
        require(gen2Count > 0, "Wallet must own a Gen2 NFT");

        uint256[] memory gen2TokenIds = new uint256[](gen2Count);
        uint256 gen2TokenIdsLength;

        for (uint256 i; i < gen2Count; i++) {
            uint256 gen2TokenId = TPTGen2.tokenOfOwnerByIndex(recipient, i);
            if (_gen2BaseClaimed[gen2TokenId] == false) {
                gen2TokenIds[gen2TokenIdsLength] = gen2TokenId;
                gen2TokenIdsLength++;
            }
        }

        require(gen2TokenIdsLength > 0, "No unclaimed Gen2 NFTs available");

        uint256 total;

        for (uint256 i; i < gen1Count; i++) {
            uint256 gen1TokenId = TPTGen1.tokenOfOwnerByIndex(recipient, i);
            if (_gen1BaseClaimed[gen1TokenId] == false && gen2TokenIdsLength > 0) {
                gen2TokenIdsLength--;
                total += levels[_rewardLevel[gen1TokenId]].baseClaim;
            }
        }

        return total;
    }

    // Check if the base reward has been claimed for the given token id on either the gen1 or gen2 NFTs
    function checkBaseRewardByTokenId(uint256 nftId, uint8 gen) external view returns (bool) {
        require(nftId < 1953, "Invalid Token ID");

        if (gen == 2) {
            return _gen1BaseClaimed[nftId];
        }

        return _gen2BaseClaimed[nftId];
    }

    // Checks an address for the total amount of accumulated daily rewards, but without any multipliers added
    function checkReward(address recipient) external view returns (uint256) {
        require(accumulationActive == true, "Reward claiming not active");

        uint256 gen1Count = TPTGen1.balanceOf(recipient);
        require(gen1Count > 0, "Wallet must own a Genesis NFT");

        uint256 total;
        for (uint256 i; i < gen1Count; i++) {
            uint256 gen1TokenId = TPTGen1.tokenOfOwnerByIndex(recipient, i);
            total += levels[_rewardLevel[gen1TokenId]].dailyReward * (block.timestamp - (_lastClaimed[gen1TokenId] > 0 ? _lastClaimed[gen1TokenId] : start)) / 86400;
        }

        return total;
    }

    // Claim the base $PENCIL rewards for all unclaimed Gen1 + Gen2 NFTs owned by the sender. Each NFT token must not
    // have already been claimed, and rewards based on Gen1 rarity level paired with an available Gen2.
    function claimBaseReward() external {
        uint256 gen1Count = TPTGen1.balanceOf(msg.sender);
        require(gen1Count > 0, "Wallet must own a Genesis NFT");

        uint256 gen2Count = TPTGen2.balanceOf(msg.sender);
        require(gen2Count > 0, "Wallet must own a Gen2 NFT");

        uint256[] memory gen2TokenIds = new uint256[](gen2Count);
        uint256 gen2TokenIdsLength;

        for (uint256 i; i < gen2Count; i++) {
            uint256 gen2TokenId = TPTGen2.tokenOfOwnerByIndex(msg.sender, i);
            if (_gen2BaseClaimed[gen2TokenId] == false) {
                gen2TokenIds[gen2TokenIdsLength] = gen2TokenId;
                gen2TokenIdsLength++;
            }
        }

        require(gen2TokenIdsLength > 0, "No unclaimed Gen2 NFTs available");

        bool rewarded;
        for (uint256 i; i < gen1Count; i++) {
            uint256 gen1TokenId = TPTGen1.tokenOfOwnerByIndex(msg.sender, i);
            if (_gen1BaseClaimed[gen1TokenId] == false && gen2TokenIdsLength > 0) {
                gen2TokenIdsLength--;
                uint256 amount = levels[_rewardLevel[gen1TokenId]].baseClaim;

                _mint(_msgSender(), amount);
                _gen1BaseClaimed[gen1TokenId] = true;
                _gen2BaseClaimed[gen2TokenIds[gen2TokenIdsLength]] = true;
                rewarded = true;

                emit BaseRewardClaimed(gen1TokenId, gen2TokenIds[gen2TokenIdsLength], amount);
            }
        }

        require(rewarded == true, "No unclaimed Gen1 NFTs available");
    }

    function claimReward() external {
        require(accumulationActive == true, "Reward claiming not active");

        uint8[] memory multipliers = new uint8[](6);
        TokenLengths memory gen1TokenLengths;
        uint256 gen1Count = TPTGen1.balanceOf(msg.sender);
        uint256 gen3Count;
        uint256 total;

        uint256[] memory gen1TokenIdsLevel0 = new uint256[](gen1Count);
        uint256[] memory gen1TokenIdsLevel1 = new uint256[](gen1Count);
        uint256[] memory gen1TokenIdsLevel2 = new uint256[](gen1Count);
        uint256[] memory gen1TokenIdsLevel3 = new uint256[](gen1Count);
        uint256[] memory gen1TokenIdsLevel4 = new uint256[](gen1Count);
        uint256[] memory gen1TokenIdsLevel5 = new uint256[](gen1Count);

        require(gen1Count > 0, "Wallet must own a Genesis NFT");

        // Capped due to possible stack limitations later on
        if (gen1Count > 40) {
            gen1Count = 40;
        }

        if (accumulationMultiplier == true) {
            gen3Count = TPTGen3.balanceOf(msg.sender);
            if (gen3Count > 0) {
                multipliers = TPTGen3.balancesOfOwnerByRarity(msg.sender);
            }
        }

        for (uint256 i; i < gen1Count; i++) {
            uint256 gen1TokenId = TPTGen1.tokenOfOwnerByIndex(msg.sender, i);

            if (_rewardLevel[gen1TokenId] == 5) {
                gen1TokenIdsLevel5[gen1TokenLengths.level5] = gen1TokenId;
                gen1TokenLengths.level5++;
            }
            else if (_rewardLevel[gen1TokenId] == 4) {
                gen1TokenIdsLevel4[gen1TokenLengths.level4] = gen1TokenId;
                gen1TokenLengths.level4++;
            }
            else if (_rewardLevel[gen1TokenId] == 3) {
                gen1TokenIdsLevel3[gen1TokenLengths.level3] = gen1TokenId;
                gen1TokenLengths.level3++;
            }
            else if (_rewardLevel[gen1TokenId] == 2) {
                gen1TokenIdsLevel2[gen1TokenLengths.level2] = gen1TokenId;
                gen1TokenLengths.level2++;
            }
            else if (_rewardLevel[gen1TokenId] == 1) {
                gen1TokenIdsLevel1[gen1TokenLengths.level1] = gen1TokenId;
                gen1TokenLengths.level1++;
            }
            else {
                gen1TokenIdsLevel0[gen1TokenLengths.level0] = gen1TokenId;
                gen1TokenLengths.level0++;
            }
        }

        // Einstein rewards
        if (gen1TokenLengths.level0 > 0) {
            total += _getClaim(gen1TokenIdsLevel0, multipliers, gen1TokenLengths, gen3Count, 0);
        }

        // Pythagoras rewards
        if (gen1TokenLengths.level1 > 0) {
            total += _getClaim(gen1TokenIdsLevel1, multipliers, gen1TokenLengths, gen3Count, 1);
        }

        // Euclid rewards
        if (gen1TokenLengths.level2 > 0) {
            total += _getClaim(gen1TokenIdsLevel2, multipliers, gen1TokenLengths, gen3Count, 2);
        }

        // Archimedes rewards
        if (gen1TokenLengths.level3 > 0) {
            total += _getClaim(gen1TokenIdsLevel3, multipliers, gen1TokenLengths, gen3Count, 3);
        }

        // Aristotle rewards
        if (gen1TokenLengths.level4 > 0) {
            total += _getClaim(gen1TokenIdsLevel4, multipliers, gen1TokenLengths, gen3Count, 4);
        }

        // Gauss rewards
        if (gen1TokenLengths.level5 > 0) {
            total += _getClaim(gen1TokenIdsLevel5, multipliers, gen1TokenLengths, gen3Count, 5);
        }

        _mint(_msgSender(), total);
    }

    function flipState() external onlyOwner {
        accumulationActive = !accumulationActive;
        if (start == 0) {
            start = uint48(block.timestamp);
        }
    }

    function flipStateMultiplier() external onlyOwner {
        accumulationMultiplier = !accumulationMultiplier;
    }

    function _getClaim(uint256[] memory _gen1TokenIds, uint8[] memory _multipliers, TokenLengths memory _gen1TokenLengths, uint256 _gen3Count, uint256 _levelIdx) internal returns (uint256) {
        uint256 total;
        uint256 gen1Count;
        uint256 multiplierIdx;

        if (_levelIdx == 5) {
            multiplierIdx = _gen1TokenLengths.level4 + _gen1TokenLengths.level3 + _gen1TokenLengths.level2 + _gen1TokenLengths.level1 + _gen1TokenLengths.level0;
            gen1Count = _gen1TokenLengths.level5;
        }
        else if (_levelIdx == 4) {
            multiplierIdx = _gen1TokenLengths.level3 + _gen1TokenLengths.level2 + _gen1TokenLengths.level1 + _gen1TokenLengths.level0;
            gen1Count = _gen1TokenLengths.level4;
        }
        else if (_levelIdx == 3) {
            multiplierIdx = _gen1TokenLengths.level2 + _gen1TokenLengths.level1 + _gen1TokenLengths.level0;
            gen1Count = _gen1TokenLengths.level3;
        }
        else if (_levelIdx == 2) {
            multiplierIdx = _gen1TokenLengths.level1 + _gen1TokenLengths.level0;
            gen1Count = _gen1TokenLengths.level2;
        }
        else if (_levelIdx == 1) {
            multiplierIdx = _gen1TokenLengths.level0;
            gen1Count = _gen1TokenLengths.level1;
        }
        else {
            gen1Count = _gen1TokenLengths.level0;
        }

        for (uint256 i; i < gen1Count; i++) {
            uint256 amount = levels[_levelIdx].dailyReward * (block.timestamp - (_lastClaimed[_gen1TokenIds[i]] > 0 ? _lastClaimed[_gen1TokenIds[i]] : start)) / 86400;

            if (multiplierIdx < _gen3Count) {
                for (uint256 l; l < 6; l++) {
                    if (_multipliers[l] > 0) {
                        amount += (amount * uint256(levels[l].multiplier)) / 100;
                        _multipliers[l]--;
                        break;
                    }
                }
                multiplierIdx++;
            }

            total += amount;
            _lastClaimed[_gen1TokenIds[i]] = uint48(block.timestamp);
            emit RewardClaimed(_gen1TokenIds[i], amount);
        }

        return total;
    }

    function _mint(address account, uint256 amount) internal virtual override {
        require(totalSupply() + amount <= cap, "Cap exceeded");
        super._mint(account, amount);
    }

    function setCap(uint192 amount) external onlyOwner {
        require(amount > 0, "Invalid cap");
        cap = amount * 10 ** 18;
    }

    function setAllowedToBurn(address account, bool allowed) public onlyOwner {
        allowedToBurn[account] = allowed;
    }

    function setLevel(uint256 idx, uint64 dailyReward, uint64 multiplier, uint128 baseClaim) external onlyOwner {
        require(idx >= 0 && idx <= 6, "Invalid level index");
        require(baseClaim > 0, "Base claim must be greater than 0");
        require(dailyReward > 0, "Daily reward must be greater than 0");
        require(multiplier >= 0, "Invalid multiplier bonus");
        _setLevel(idx, dailyReward, multiplier, baseClaim);
    }

    function _setLevel(uint256 _idx, uint64 _dailyReward, uint64 _multiplier, uint128 _baseClaim) internal {
        levels[_idx] = Level(_dailyReward * 10 ** 18, _multiplier, _baseClaim * 10 ** 18);
    }

    function setMultiplierAddress(address account) external onlyOwner {
        TPTGen3 = TPTM(account);
        setAllowedToBurn(account, true);
    }
}
/**
 * $PENCIL utility token for 2+2 ecosystem.
 *
 * Rewards system based on 2+2 Genesis NFT (addition) rarity levels and Gen2 (subtraction) NFT ownership.
 * When Gen3 (multiplication) NFT is deployed, multipliers will be applied based upon those rarity levels.
 * Future 2+2 ecosystem contracts will be authorized to burn tokens as part of the claiming process.
 *
 * We have followed the Solidity general layout of state variables in storage to compact the data and reduce gas.
 * Multiple, contiguous items that need less than 256 bits are packed into single storage slots when possible.
 */
function checkReward(address recipient) external view returns (uint256) {
        require(accumulationActive == true, "Reward claiming not active");

        uint256 gen1Count = TPTGen1.balanceOf(recipient);
        require(gen1Count > 0, "Wallet must own a Genesis NFT");

        uint256 total;
        for (uint256 i; i < gen1Count; i++) {
            uint256 gen1TokenId = TPTGen1.tokenOfOwnerByIndex(recipient, i);
            total += levels[_rewardLevel[gen1TokenId]].dailyReward * (block.timestamp - (_lastClaimed[gen1TokenId] > 0 ? _lastClaimed[gen1TokenId] : start)) / 86400;
        }

        return total;
    }
// Checks an address for the total amount of accumulated daily rewards, but without any multipliers added