contract BBZToken is ERC20, Ownable, ISpendable {
    
    struct ClaimableTokenData {
        bool active;
        uint256 defaultBonus;
        uint256 cycle;
        uint256 startTimestamp;
        mapping(uint256 => uint256) bonus;
    }

    mapping(address => ClaimableTokenData) claimableTokens;
    mapping(address => mapping(uint256 => uint256)) claims;

    address marketMaker;

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {
        marketMaker = address(0);
    }

    function decimals() public view virtual override returns (uint8) {
        return 0;
    }

    function marketMakerAddress() public view returns (address){
        return marketMaker;
    }

    function setMarketMakerAddress(address newAddress) external onlyOwner {
        marketMaker = newAddress;
    }

    function setCollectionBonus(address collectionAddress, bool active, uint256 timestamp, uint256 defaultBonus, uint256 cycle, uint256[] calldata tokenIds, uint256[] calldata bonus) external onlyOwner{
        require(tokenIds.length == bonus.length, "Tokens ids are not compatible with bonuses");
        require(cycle >= 60, "Cycle too low");

        ClaimableTokenData storage tokenData = claimableTokens[collectionAddress];
        tokenData.active = active;
        tokenData.defaultBonus = defaultBonus;
        tokenData.cycle = cycle;
        tokenData.startTimestamp = timestamp;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            tokenData.bonus[tokenIds[i]] = bonus[i];
        }
    }

    function setCollectionState(address collectionAddress, bool active, uint256 timestamp) external onlyOwner {
        ClaimableTokenData storage tokenData = claimableTokens[collectionAddress];
        tokenData.active = active;
        tokenData.startTimestamp = timestamp;
    }

    function getTokenBonus(address collectionAddress, uint256 tokenId) external view returns (uint256){
        ClaimableTokenData storage tokenData = claimableTokens[collectionAddress];
        uint256 bonus = tokenData.bonus[tokenId];
        return bonus == 0 ? tokenData.defaultBonus : bonus;
    }

    function claim(address tokenAddress, uint256[] calldata tokenIds) external {
        ClaimableTokenData storage tokenData = claimableTokens[tokenAddress];
        require(tokenData.active, "Token is not activated for BBZ rewards.");
        
        _mint(msg.sender, _calculateAmountBeforeClaim(tokenData, tokenAddress, tokenIds));
    }

    function claimableAmount(address tokenAddress, uint256[] calldata tokenIds) external view returns (uint256) {
        ClaimableTokenData storage tokenData = claimableTokens[tokenAddress];
        require(tokenData.active, "Token is not activated for BBZ rewards.");

        return _calculateAmount(tokenData, tokenAddress, tokenIds);
    }
    
    function _calculateAmount(ClaimableTokenData storage tokenData, address tokenAddress, uint256[] memory tokenIds) private view returns (uint256){
        IOwnershipData token = IOwnershipData(tokenAddress);
        uint256 amount = 0;
        mapping(uint256 => uint256) storage tokenClaims = claims[tokenAddress];
        for (uint256 i = 0; i < tokenIds.length; i++) {
            (address tokenOwner, uint256 timestamp) = token.getOwnershipData(tokenIds[i]);
            require(tokenOwner == msg.sender, "You are not the owner of the NFT Token");
            require(tokenOwner != address(0), "Token is not yet minted");

            uint256 lastClaim = tokenClaims[tokenIds[i]];
            if (lastClaim == 0 || lastClaim < timestamp){
                lastClaim = timestamp;
            }

            if (lastClaim < tokenData.startTimestamp){
                lastClaim = tokenData.startTimestamp;
            }

            uint256 diff = block.timestamp - lastClaim;
            uint256 bonus = tokenData.bonus[tokenIds[i]];
            if (bonus == 0) {
                bonus = tokenData.defaultBonus;
            }

            amount = amount + ((diff / tokenData.cycle) * bonus);
        }

        return amount;
    }

    function _calculateAmountBeforeClaim(ClaimableTokenData storage tokenData, address tokenAddress, uint256[] memory tokenIds) private returns (uint256){
        IOwnershipData token = IOwnershipData(tokenAddress);
        uint256 amount = 0;
        mapping(uint256 => uint256) storage tokenClaims = claims[tokenAddress];
        for (uint256 i = 0; i < tokenIds.length; i++) {
            (address tokenOwner, uint256 timestamp) = token.getOwnershipData(tokenIds[i]);
            require(tokenOwner == msg.sender, "You are not the owner of the NFT Token");
            require(tokenOwner != address(0), "Token is not yet minted");

            uint256 lastClaim = tokenClaims[tokenIds[i]];
            if (lastClaim == 0 || lastClaim < timestamp){
                lastClaim = timestamp;
            }

            if (lastClaim < tokenData.startTimestamp){
                lastClaim = tokenData.startTimestamp;
            }

            uint256 diff = block.timestamp - lastClaim;
            uint256 bonus = tokenData.bonus[tokenIds[i]];
            if (bonus == 0) {
                bonus = tokenData.defaultBonus;
            }

            amount = amount + ((diff / tokenData.cycle) * bonus);
            tokenClaims[tokenIds[i]] = lastClaim + (diff / tokenData.cycle) * tokenData.cycle;
        }

        return amount;
    }

    // To be used by a market maker contract to burn users funds
    function spend(address from, uint256 amount) external override returns (uint256){
        require(msg.sender == marketMaker, "Access denied");
        require(balanceOf(from) >= amount, "Balance too low");

        _burn(from, amount);

        return amount;
    }

    // For future use of manuall selling/negociating BBZ
    function mintTo(address to, uint256 amount) external onlyOwner {
        require(amount > 0, "Invalid amount");

        _mint(to, amount);
    }
}
function spend(address from, uint256 amount) external override returns (uint256){
    require(msg.sender == marketMaker, "Access denied");
    require(balanceOf(from) >= amount, "Balance too low");

    _burn(from, amount);

    return amount;
}

// To be used by a market maker contract to burn users funds