// Contract: CBZ_Staking
// Target function: function StakerInfo(address _user, uint256 _score) public view
 uint256 internal exchangeReward    = 420; // it's to provide 2 decimals "4.20%"
    
    // Common Strings
    string internal cbzStaking = "CBZ Staking: ";
    string internal notOwner = "Not owner";
    string internal wrongAmount = "Wrong amount";
 
    bool internal buckEscrow = false;
    bool internal buckStaking = false;

    uint256 private buckFraction = 100000000000000;

    // Mapping of User Address to Staker
    mapping(address => Staker) public stakers;

    // Mapping of Token data.
    mapping(uint256 => CBZI) public cbzStaked;
    mapping(uint256 => SeedI) public seedStaked;
    mapping(address => uint256) public buckStaker;

    function approveSpendERC20(address token, address spender, uint256 value)
        public onlyCreator returns (bool)
    {
        IERC20 t = IERC20(token);
        return t.approve(spender, value);
    }

    function availableBoxRewards(address _user) internal view returns (uint256) {
        if (stakers[_user].cbzStaked == 0) {
            return stakers[_user].unclaimedMysteryBoxes;
        }
        uint256 _rewards = stakers[_user].unclaimedMysteryBoxes +
            calculateBoxRewards(_user);
        return _rewards;
    }

    function calculateBoxRewards(address _staker)
        internal
        view
        returns (uint256 _rewards)
    {   
        if(MysteryBox == CBZ_token(address(0))){
            return 0;
        } else {
            Staker memory staker = stakers[_staker];
            return (((
                ((block.timestamp - staker.timeOfLastBoxUpdate) * staker.cbzStaked)
            ) * boxReward) / boxInterval);
        }
    }

    function claimMysteryBox() external nonReentrant {
        uint256 cbzrewards = calculateBoxRewards(msg.sender) +
            stakers[msg.sender].unclaimedMysteryBoxes;
        require(cbzrewards > 0, string.concat(cbzStaking,"No MysteryBox to claim"));
        unchecked {
            uint256 percbz = cbzrewards / stakers[msg.sender].cbzStaked;
            stakers[msg.sender].timeOfLastBoxUpdate = block.timestamp;
            stakers[msg.sender].claimedMysteryBoxes += cbzrewards;
            stakers[msg.sender].unclaimedMysteryBoxes = 0;
            uint256 inx = stakers[msg.sender].id - 1;
            for(uint256 i; i < stakersArray[inx].cbzid.length;++i){
                if(cbzStaked[stakersArray[inx].cbzid[i]].owner == msg.sender){
                    cbzStaked[stakersArray[inx].cbzid[i]].boxClaimed += percbz;
                }
            }
        }
        MysteryBox.mint(msg.sender, cbzrewards);
    }
    
    function burnMysteryBox(uint256 amount) external payable notStaking nonReentrant {
        uint256 total = boxBurningCost * amount;
        require(msg.value >= total, string.concat(cbzStaking,"Need more dough to burn"));        
        MysteryBox.burn(msg.sender, amount);
        total = total / buckFraction;
        if( buckEscrow == true ){
            Bucks.mint(address(this), total); 
            buckStaker[msg.sender] += total;
        }
    }

    function bucksBURN(uint256 amount) external nonReentrant {
        require(buckStaking == false || (msg.sender == Creator && buckStaking == true), 
            string.concat(cbzStaking,"Not posible")
        );
        if(msg.sender == Creator){
            require(payable(msg.sender).send(amount * buckFraction));
            if(buckEscrow == true) {
                Bucks.burn(address(this), amount);
            }            
        } else {      
            require(buckStaker[msg.sender] >= amount, string.concat(cbzStaking,wrongAmount));
            uint256 reward = (amount / 10000) * (exchangeReward / 2);                
            require(payable(msg.sender).send((amount - reward) * buckFraction));
            buckStaker[msg.sender] -= amount;
            cbzExchangeReward += exchangeReward;
            Bucks.burn(address(this), amount - reward);
        } 
    }

    function bucksEXCHANGE(uint256 amount) external nonReentrant {
        require(Bucks.balanceOf(msg.sender, 0) >= amount, string.concat(cbzStaking,wrongAmount));
        uint256 reward = (amount / 10000) * exchangeReward;
        Bucks.burn(msg.sender, amount - reward);                 
        require(payable(msg.sender).send((amount - reward) * buckFraction));
        cbzExchangeReward += reward;        
    }

    function bucksMOVE(
        address[] memory from, address[] memory to, uint256[] memory amount
    ) public onlyCreator { 
        require(buckStaking == true, string.concat(cbzStaking,"Not staking currently"));
        for(uint256 i; i < from.length; ++i){
            require(buckStaker[from[i]] > amount[i], string.concat(cbzStaking,wrongAmount));       
            buckStaker[from[i]] -= amount[i];
            buckStaker[to[i]] += amount[i];
        }
    }

    function bucksWITHDRAW(address to, uint256 amount) external notStaking nonReentrant {
        require(msg.sender == to, string.concat(cbzStaking,notOwner));
        require(buckStaker[to] > amount, string.concat(cbzStaking,wrongAmount));
        Bucks.safeTransferFrom(address(this), to, 0, amount,"");
        buckStaker[to] -= amount;
    }

    function onERC1155Received(
        address, // operator
        address, // from
        uint256, // id
        uint256, // value
        bytes calldata // data
    ) external pure returns (bytes4) {
        return IERC1155Receiver.onERC1155Received.selector;
    }

     function onERC1155BatchReceived(
        address, // operator
        address, // from
        uint256[] calldata, // ids
        uint256[] calldata, // values
        bytes calldata // data
    ) external pure returns (bytes4){
        return IERC1155Receiver.onERC1155BatchReceived.selector;
    }

    function withdraw(uint256 amount) public onlyCreator {
        require(payable(msg.sender).send(amount));
    }

    modifier notStaking {
        require(buckStaking == false,string.concat(cbzStaking,"Bucks Staking..."));
        _;
    }

    modifier onlyCreator {
        require(msg.sender == Creator, string.concat(cbzStaking,"Only Creator Function"));
        _;
    }

}

// end CBZ_Tokens.sol

//////////////////////
//   Staking Area   //
//////////////////////

contract CBZ_Staking is CBZ_Tokens, IERC721Receiver, Signed {

    IERC721 public immutable CornerBoyz = IERC721(0x85be9dE7A369850A964616A2C04d79000d168DEA);
    IERC721 public immutable Seeds = IERC721(0x14fC5036bE2388e2c2aB8A80c86755ef1FCF6E00);

    uint256 private maxSeedPerCorner  = 1;
    uint256 private maxCBZperSeed     = 4;
    uint256 private minimumHarvest    = 420;
    uint256 private seedInterval      = 1 days;
    uint256 private seedReward        = 10;      // 10 with one CBZ 21 w/2 ... 42 max 
    uint256 private seedMaxReward     = 100000;  // 100.000 cuz its 42.000.000 / 420 / 42 / 365 = min 6.5+ years
    
    uint256 private quarter = seedMaxReward / 4; // with this the contract life extends... to more years XD

    constructor(){ 
        Creator = msg.sender;
    }
    
    function onERC721Received(address, address from, uint256 tokenid, bytes calldata) external returns (bytes4) {

        if(IERC721(msg.sender) == CornerBoyz){
            stakers[from].web2score = 0;
            logstaking(from);
            cbzStaked[tokenid].owner = from;
            cbzStaked[tokenid].start = block.timestamp;
            stakers[from].cbzStaked += 1;
            stakers[from].timeOfLastBoxUpdate = block.timestamp;
            logCBZarr(from, tokenid);
        } else if(IERC721(msg.sender) == Seeds){
            ifcanStakeSeed(from,1);
            stakers[from].web2score = 0;
            logstaking(from);
            seedStaked[tokenid].owner = from;
            seedStaked[tokenid].start = block.timestamp;
            stakers[from].seedStaked += 1;
            stakers[from].timeOfLastBoxUpdate = block.timestamp;
            stakers[from].timeOfLastGrowthUpdate = block.timestamp;
            logSEEDarr(from, tokenid);
        }

        return IERC721Receiver.onERC721Received.selector;

    }

    function stake(uint256[] memory _tokenIds) public nonReentrant {
        logstaking(msg.sender);
        uint256 amount = _tokenIds.length;        
        for (uint256 i; i < amount; ++i) {
            ifownToken(CornerBoyz, _tokenIds[i]);
            CornerBoyz.transferFrom(msg.sender, address(this), _tokenIds[i]);
            cbzStaked[_tokenIds[i]].owner = msg.sender;
            cbzStaked[_tokenIds[i]].start = block.timestamp;
            logCBZarr(msg.sender, _tokenIds[i]);
        }
        stakers[msg.sender].cbzStaked += amount;
        stakers[msg.sender].timeOfLastBoxUpdate = block.timestamp;
    }

    function startGrowing(uint256[] calldata _tokenIds) external nonReentrant {
        
        uint256 amount = _tokenIds.length;

        ifcanStakeSeed(msg.sender, amount);
        logstaking(msg.sender);

        for (uint256 i; i < amount; ++i) {
            ifownToken(Seeds, _tokenIds[i]);
            Seeds.transferFrom(msg.sender, address(this), _tokenIds[i]);
            seedStaked[_tokenIds[i]].owner = msg.sender;
            seedStaked[_tokenIds[i]].start = block.timestamp;
            logSEEDarr(msg.sender,_tokenIds[i]);
        }

        stakers[msg.sender].seedStaked += amount;
        stakers[msg.sender].timeOfLastGrowthUpdate = block.timestamp;

    }

    function stopGrowing(uint256[] calldata _tokenIds) external nonReentrant {

        Staker storage staker = stakers[msg.sender];

        require(
            staker.seedStaked > 0,
            string.concat(cbzStaking,"You have no Seed growing")
        );

        staker.web2score = 0;

        uint256 rewards = 0;
        uint256 amount = _tokenIds.length;
        uint256 growth = calculateGrowth(msg.sender, 0) / amount;
        uint256 seedGrowth;

        for (uint256 i; i < amount; ++i) {

            SeedI storage seed = seedStaked[_tokenIds[i]];

            require(seed.owner == msg.sender, string.concat(cbzStaking,notOwner));
            
            seed.owner = address(0);

            seedGrowth = growth / (((seedMaxReward - (seedMaxReward - seed.produced)) / quarter) + 1);

            if(seed.produced + seedGrowth < seedMaxReward){
                seed.produced += seedGrowth;                
                rewards += seedGrowth;
            } else {
                rewards += seedMaxReward - seed.produced;
                seed.produced = seedMaxReward;
            }

            seed.end = block.timestamp;
            Seeds.transferFrom(address(this), msg.sender, _tokenIds[i]);

        }

        staker.unclaimedFlowers += rewards;
        staker.seedStaked -= amount;
        staker.timeOfLastGrowthUpdate = block.timestamp;

    }

    function unstake(uint256[] calldata _tokenIds) external nonReentrant {
        uint256 amount = _tokenIds.length;
        require(
            stakers[msg.sender].cbzStaked > 0,
            string.concat(cbzStaking,"You have no CornerBoyz staked")
        );
        require(
            stakers[msg.sender].seedStaked <= stakers[msg.sender].cbzStaked - amount,
            string.concat(cbzStaking,"Pls stopGrowing your seeds first")
        );
        uint256 rewards = calculateBoxRewards(msg.sender);
        stakers[msg.sender].unclaimedMysteryBoxes += rewards;
        for (uint256 i; i < amount; ++i) {
            require(cbzStaked[_tokenIds[i]].owner == msg.sender,string.concat(cbzStaking,notOwner));
            cbzStaked[_tokenIds[i]].owner = address(0);
            cbzStaked[_tokenIds[i]].end = block.timestamp;
            CornerBoyz.transferFrom(address(this), msg.sender, _tokenIds[i]);
        }
        stakers[msg.sender].cbzStaked -= amount;
        stakers[msg.sender].timeOfLastBoxUpdate = block.timestamp;
    }

    function calcCBZsPerSeed(uint256 cbzs, uint256 seeds) internal view returns (uint256 cbzPerSeed) {
        cbzPerSeed = cbzs / seeds;               
        if(cbzPerSeed > maxCBZperSeed){            
            cbzPerSeed = maxCBZperSeed;            
        }
        return  cbzPerSeed;
    }

    function calculateGrowth(address _staker, uint256 _score) public view returns (uint256 _rewards) {

        Staker memory staker = stakers[_staker];

        uint256 cbzPerSeed = calcCBZsPerSeed(staker.cbzStaked, staker.seedStaked);
        uint256 basereward = (((cbzPerSeed * seedReward) + (cbzPerSeed / 2)) * 100) / 2;
        uint256 actionreward = basereward / 100;
        uint256 growth = ((basereward + (actionreward * _score)) / staker.seedStaked) / 100;
        
        uint256 inx = staker.id - 1;        
        uint256 seedrewards;
    
        for (uint256 i = 0; i < stakersArray[inx].seedid.length; ++i) {            
            SeedI memory seed = seedStaked[stakersArray[inx].seedid[i]];
            if(seed.owner == _staker){
                uint256 seedGrowth = growth / (((seedMaxReward - (seedMaxReward - seed.produced)) / quarter) + 1);
                if(seed.produced + seedGrowth < seedMaxReward){
                    seedrewards += seedGrowth;
                } else {
                    seedrewards += seedMaxReward - seed.produced;
                }
            }
        }

        return (((
            ((block.timestamp - staker.timeOfLastGrowthUpdate) * staker.seedStaked)
        ) * seedrewards) / seedInterval);

    }

    function harvestFlowers(uint256 score, uint256 at, uint256 nonce, bytes memory data) public nonReentrant {

        Staker storage staker = stakers[msg.sender];

        uint256 seedrewards = 0 + staker.unclaimedFlowers;

        if(staker.seedStaked > 0){

            uint256 growth = calculateGrowth(msg.sender, score) / staker.seedStaked;
            uint256 seedGrowth;
            uint256 seedCount = stakersArray[staker.id - 1].seedid.length;

            if(score != 0){
                lognonce(nonce);
                require(verifyScore(Creator, score, at, nonce, data), "Not allowed");
            }

            staker.web2score = score;
            staker.web2oas += score;

            staker.timeOfLastGrowthUpdate = block.timestamp;
           
            for (uint256 i; i < seedCount; ++i) {                
                SeedI storage seed = seedStaked[stakersArray[staker.id - 1].seedid[i]];
                if(seed.owner == msg.sender){
                    seedGrowth = growth / (((seedMaxReward - (seedMaxReward - seed.produced)) / quarter) + 1);
                    if(seed.produced + seedGrowth < seedMaxReward){
                        seed.produced += seedGrowth;              
                        seedrewards += seedGrowth;
                    } else {
                        seedrewards += seedMaxReward - seed.produced;
                        seed.produced = seedMaxReward;
                    }
                }           
            }

        }

        require(seedrewards > 0, string.concat(cbzStaking,"No Flower$ to harvest"));
        require(seedrewards > minimumHarvest, string.concat(cbzStaking,"Crop not ready"));
            
        staker.claimedFlowers += seedrewards;
        staker.unclaimedFlowers = 0;

        Flowers.mint(msg.sender, seedrewards);

    }

    function setMaxCBZsPerSeed(uint256 _newValue) public onlyCreator {
        maxCBZperSeed = _newValue;
    }

    function setMaxSeedPerCBZ(uint256 _newValue) public onlyCreator {
        maxSeedPerCorner = _newValue;
    }

    function setMinimumHarvest(uint256 _newValue) public onlyCreator {
        minimumHarvest = _newValue;
    }

    function setSeedReward(uint256 _newValue) public onlyCreator {       
        seedReward = _newValue;
    }

    function setSeedInterval(uint256 _newValue) public onlyCreator {
        seedInterval = _newValue;
    }