// Contract: ZenYield
// Target function: function updateReward(address from_, address to_, uint256 tokenId_) public
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

///////////////////////////////////////
//   ____       __  ___     __   __  //
//  /_  / ___ __\ \/ (_)__ / /__/ /  //
//   / /_/ -_) _ \  / / -_) / _  /   //
//  /___/\__/_//_/_/_/\__/_/\_,_/    //
//               by 0xInuarashi.eth  //
///////////////////////////////////////

abstract contract Ownable {
    address public owner; 
    constructor() { owner = msg.sender; }
    modifier onlyOwner { require(owner == msg.sender, "Not Owner!"); _; }
    function transferOwnership(address new_) external onlyOwner { owner = new_; }
}

interface iZen {
    function mintAsController(address to_, uint256 amount_) external;
}

interface iZenApe {
    function balanceOf(address address_) external view returns (uint256);
}

contract ZenYield is Ownable {
    
    // Interfaces
    iZen public Zen = iZen(0x0fAD2A1F9aB421C1740c0456ec62c155518824aF); 
    function setZen(address address_) external onlyOwner { 
        Zen = iZen(address_); 
    }

    iZenApe public ZenApe = iZenApe(0x838804a3dd7c717396a68F94E736eAf76b911632);
    function setZenApe(address address_) external onlyOwner {
        ZenApe = iZenApe(address_);
    }

    // Times
    uint40 public yieldStartTime = 1651327200; // Apr 30 2022 14:00:00 GMT+0000
    uint40 public yieldEndTime = 1682863200; // Apr 30 2023 14:00:00 GMT+0000
    function setYieldEndTime(uint40 yieldEndTime_) external onlyOwner { 
        yieldEndTime = yieldEndTime_; }

    // Yield Info
    uint256 public globalModulus = 10e14; // Round up 14 Decimals
    uint256 public yieldRatePerToken = 5 ether / globalModulus; // 5 Zen per Day
    struct Yield { uint40 lastUpdateTime; uint216 pendingRewards; }
    mapping(address => Yield) public addressToYield;

    // Events
    event Claim(address to_, uint256 amount_, uint256 time_);
    event CreditsDeducted(address from_, uint256 amount_);
    event CreditsAdded(address to_, uint256 amount_);

    // Internal Calculators
    function _getSmallerValueUint40(uint40 a, uint40 b) internal pure returns (uint40) {
        return a < b ? a : b;
    }
    function _getTimestamp() internal view returns (uint40) {
        return _getSmallerValueUint40( uint40(block.timestamp), yieldEndTime );
    }
    function _getYieldRate(address address_) internal view returns (uint40) {
        return uint40(ZenApe.balanceOf(address_) * yieldRatePerToken);
    }

    // Internal Accountants
    function _getPendingRewards(address address_) internal view returns (uint216) {
        uint256 _totalYieldRate = uint256(_getYieldRate(address_));
        if (_totalYieldRate == 0) return 0;
        uint256 _time = uint256(_getTimestamp());
        uint256 _lastUpdate = uint256(addressToYield[address_].lastUpdateTime);

        if (_lastUpdate > yieldStartTime) {
            return uint216( (_totalYieldRate * (_time - _lastUpdate) / 1 days) );
        } 
        // Has ZenApe before Zen Token
        else if (_lastUpdate == 0 && _totalYieldRate > 0) {
            return uint216( (_totalYieldRate * (_time - yieldStartTime) / 1 days) );
        } else { return 0; }
    }
    function _updateReward(address address_) internal {
        uint40 _lastUpdate = addressToYield[address_].lastUpdateTime;

        if (_lastUpdate > 0) {
            addressToYield[address_].pendingRewards += _getPendingRewards(address_);
        }
        // Has ZenApe before Zen Token
        else if (_lastUpdate == 0 && ZenApe.balanceOf(address_) != 0) {
            addressToYield[address_].pendingRewards += _getPendingRewards(address_);
        }
        
        if (_lastUpdate != yieldEndTime) {
            addressToYield[address_].lastUpdateTime = _getTimestamp();
        }
    }
    function _claimReward(address address_) internal {
        // This does not update before claim. So, in internal flows, update first.
        uint216 _pendingRewards = addressToYield[address_].pendingRewards;
        
        if (_pendingRewards > 0) {
            uint256 _expandedReward = uint256(_pendingRewards * globalModulus);

            addressToYield[address_].pendingRewards = 0;

            Zen.mintAsController(address_, _expandedReward);
        }
    }