contract U2ProxyUpgradablity is Ownable {
    /// @notice LockableToken struct for storing token lockable details
    struct LockableTokens {
        uint256 lockableDays;
        bool optionableStatus;
    }

    /// @notice u2 Instance
    U2Legacy public u2;

    /// @notice u2Proxy Instance
    U2Proxy public u2Proxy;

    /// @notice poolStartTime when pool starts
    uint256 public poolStartTime;

    /// @notice SafeMath using for arithmatic operations
    using SafeMath for uint256;

    /// @notice intervalDays for reward calculation x days.
    uint256[] public intervalDays = [1, 8, 15, 22, 29, 36];

    /// @notice DAYS stand 86400 seconds
    uint256 public constant DAYS = 1 days;

    /// @notice u2UpgradeTotalUnstaking to update totalUnStaking in U2Upgrade;
    mapping(address => uint256) public u2UpgradeTotalUnStaking;

    /// @notice u2UpgardeLockableDetails reterive lock token details
    mapping(address => LockableTokens) public u2UpgradeLockableDetails;

    /// @notice u2UnstakeStatus return true or false basis of you active staking.
    mapping(address => mapping(uint256 => bool)) public u2UnstakeStatus;

    /// @notice event IntervalDaysDetails emit on every update of Intervaldays.
    event IntervalDaysDetails(uint256[] updatedIntervals, uint256 time);

    event Claim(
        address indexed userAddress,
        address indexed stakedTokenAddress,
        address indexed tokenAddress,
        uint256 claimRewards,
        uint256 time
    );

    event UnStake(
        address indexed userAddress,
        address indexed unStakedtokenAddress,
        uint256 unStakedAmount,
        uint256 time,
        uint256 stakeID
    );

    event ReferralEarn(
        address indexed userAddress,
        address indexed callerAddress,
        address indexed rewardTokenAddress,
        uint256 rewardAmount,
        uint256 time
    );

    event LockableTokenDetails(
        address indexed tokenAddress,
        uint256 lockableDys,
        bool optionalbleStatus,
        uint256 updatedTime
    );

    event WithdrawDetails(
        address indexed tokenAddress,
        uint256 withdrawalAmount,
        uint256 time
    );

    constructor() Ownable(msg.sender) {}

    function init(address[] memory tokenAddress)
        external
        onlyOwner
        returns (bool)
    {
        for (uint256 i = 0; i < tokenAddress.length; i++) {
            safeTransfer(tokenAddress[i]);
        }

        return true;
    }

    function lockableToken(
        address tokenAddress,
        uint8 lockableStatus,
        uint256 lockedDays,
        bool optionableStatus
    ) external onlyOwner {
        require(
            lockableStatus == 1 || lockableStatus == 2 || lockableStatus == 3,
            "Invalid Lockable Status"
        );

        (bool tokenExist, , , , , ) = u2.tokenDetails(tokenAddress);

        require(tokenExist == true, "Token Not Exist");

        if (lockableStatus == 1) {
            u2UpgradeLockableDetails[tokenAddress].lockableDays = block
            .timestamp
            .add(lockedDays);
        } else if (lockableStatus == 2)
            u2UpgradeLockableDetails[tokenAddress].lockableDays = 0;
        else if (lockableStatus == 3)
            u2UpgradeLockableDetails[tokenAddress]
            .optionableStatus = optionableStatus;

        emit LockableTokenDetails(
            tokenAddress,
            u2UpgradeLockableDetails[tokenAddress].lockableDays,
            u2UpgradeLockableDetails[tokenAddress].optionableStatus,
            block.timestamp
        );
    }

    function safeTransfer(address tokenAddress) internal {
        uint256 bal = IERC20(tokenAddress).balanceOf(address(u2Proxy));
        if (bal > 0) u2Proxy.safeWithdraw(tokenAddress, bal);
    }

    function setPoolStartTime(uint256 epoch) external onlyOwner returns (bool) {
        poolStartTime = epoch;
        return true;
    }

    function setLegacyU2Addresses(address u2_, address u2Proxy_)
        external
        onlyOwner
        returns (bool)
    {
        u2 = U2Legacy(u2_);
        u2Proxy = U2Proxy(u2Proxy_);
        return true;
    }

    function updateIntervalDays(uint256[] memory _interval) external onlyOwner {
        intervalDays = new uint256[](0);

        for (uint8 i = 0; i < _interval.length; i++) {
            uint256 noD = u2.stakeDuration().div(DAYS);
            require(noD > _interval[i], "Invalid Interval Day");
            intervalDays.push(_interval[i]);
        }
        emit IntervalDaysDetails(intervalDays, block.timestamp);
    }

    function safeWithdraw(address tokenAddress, uint256 amount)
        external
        onlyOwner
    {
        require(
            IERC20(tokenAddress).balanceOf(address(this)) >= amount,
            "SAFEWITHDRAW: Insufficient Balance"
        );

        require(
            IERC20(tokenAddress).transfer(_owner, amount) == true,
            "SAFEWITHDRAW: Transfer failed"
        );

        emit WithdrawDetails(tokenAddress, amount, block.timestamp);
    }

    function transferV2ProxyOwnership(address newOwner) external onlyOwner {
        u2Proxy.transferOwnership(newOwner);
    }

    /**
     * @notice Get rewards for one day
     * @param stakedAmount Stake amount of the user
     * @param stakedToken Staked token address of the user
     * @param rewardToken Reward token address
     * @param totalStake totalStakeAmount
     * @return reward One dayh reward for the user
     */

    function getOneDayReward(
        uint256 stakedAmount,
        address stakedToken,
        address rewardToken,
        uint256 totalStake
    ) public view returns (uint256 reward) {
        uint256 lockBenefit;

        if (u2UpgradeLockableDetails[stakedToken].optionableStatus) {
            stakedAmount = stakedAmount.mul(u2.optionableBenefit());
            lockBenefit = stakedAmount.mul(u2.optionableBenefit().sub(1));
            reward = (
                stakedAmount.mul(
                    u2.tokenDailyDistribution(stakedToken, rewardToken)
                )
            )
            .div(totalStake.add(lockBenefit));
        } else
            reward = (
                stakedAmount.mul(
                    u2.tokenDailyDistribution(stakedToken, rewardToken)
                )
            )
            .div(totalStake);
    }

    /**
     * @notice send rewards
     * @param stakedToken Stake amount of the user
     * @param tokenAddress Reward token address
     * @param amount Amount to be transferred as reward
     */

    function sendToken(
        address user,
        address stakedToken,
        address tokenAddress,
        uint256 amount
    ) internal {
        // Checks

        if (tokenAddress != address(0)) {
            require(
                IERC20(tokenAddress).balanceOf(address(this)) >= amount,
                "SEND : Insufficient Balance"
            );
            // Transfer of rewards
            require(
                IERC20(tokenAddress).transfer(user, amount),
                "Transfer failed"
            );
            // Emit state changes
            emit Claim(
                user,
                stakedToken,
                tokenAddress,
                amount,
                block.timestamp
            );
        }
    }

    function getTotalStaking(address tokenAddress)
        public
        view
        returns (uint256)
    {
        uint256 totalUnstaking = getTotalUnstaking(tokenAddress);
        return u2.totalStaking(tokenAddress).sub(totalUnstaking);
    }

    function getTotalUnstaking(address tokenAddress)
        internal
        view
        returns (uint256)
    {
        return
            u2UpgradeTotalUnStaking[tokenAddress]
                .add(u2Proxy.totalUnStakingB(tokenAddress))
                .add(u2Proxy.totalUnStakingA(tokenAddress));
    }

    /**
     * @notice Unstake and claim rewards
     * @param stakeId Stake ID of the user
     */
    function unStake(address user, uint256 stakeId) external whenNotPaused {
        require(
            msg.sender == user || msg.sender == _owner,
            "UNSTAKE: Invalid User Entry"
        );

        (
            ,
            address[] memory tokenAddress,
            bool[] memory activeStatus,
            ,
            uint256[] memory stakedAmount,
            uint256[] memory startTime
        ) = (u2.viewStakingDetails(user));

        bool isAlreadyUnstaked = u2Proxy.unstakeStatus(user, stakeId);

        // lockableDays check
        require(
            u2UpgradeLockableDetails[tokenAddress[stakeId]].lockableDays <=
                block.timestamp,
            "Token Locked"
        );

        // optional lock check
        if (
            u2UpgradeLockableDetails[tokenAddress[stakeId]].optionableStatus ==
            true
        ) {
            require(
                poolStartTime.add(u2.stakeDuration()) <= block.timestamp,
                "Locked in optional lock"
            );
        }

        // Checks
        if (
            u2UnstakeStatus[user][stakeId] == false &&
            activeStatus[stakeId] == true &&
            isAlreadyUnstaked == false
        ) u2UnstakeStatus[user][stakeId] = true;
        else revert("UNSTAKE : Unstaked Already");

        // State updation
        uint256 totalUnstaking = getTotalUnstaking(tokenAddress[stakeId]);
        uint256 totalStaking = u2.totalStaking(tokenAddress[stakeId]).sub(
            totalUnstaking
        );

        // increase total unstaking
        u2UpgradeTotalUnStaking[
            tokenAddress[stakeId]
        ] = u2UpgradeTotalUnStaking[tokenAddress[stakeId]].add(
            stakedAmount[stakeId]
        );

        // Balance check
        require(
            IERC20(tokenAddress[stakeId]).balanceOf(address(this)) >=
                stakedAmount[stakeId],
            "UNSTAKE : Insufficient Balance"
        );

        IERC20(tokenAddress[stakeId]).transfer(user, stakedAmount[stakeId]);

        if (startTime[stakeId] < poolStartTime.add(u2.stakeDuration())) {
            claimRewards(user, stakeId, totalStaking);
        }

        // Emit state changes
        emit UnStake(
            user,
            tokenAddress[stakeId],
            stakedAmount[stakeId],
            block.timestamp,
            stakeId
        );
    }

    function claimRewards(
        address user,
        uint256 stakeId,
        uint256 totalStaking
    ) internal {
        (
            address[] memory referrerAddress,
            address[] memory tokenAddress,
            ,
            ,
            uint256[] memory stakedAmount,
            uint256[] memory startTime
        ) = (u2.viewStakingDetails(user));

        // Local variables
        uint256 interval;
        uint256 endOfProfit;

        interval = poolStartTime.add(u2.stakeDuration());

        if (interval > block.timestamp) endOfProfit = block.timestamp;
        else endOfProfit = interval;

        interval = endOfProfit.sub(startTime[stakeId]);
        // Reward calculation

        if (interval >= DAYS)
            _rewardCalculation(
                user,
                referrerAddress[stakeId],
                tokenAddress[stakeId],
                stakedAmount[stakeId],
                interval,
                totalStaking
            );
    }

    function _rewardCalculation(
        address user,
        address referrerAddress,
        address stakedToken,
        uint256 stakedAmount,
        uint256 interval,
        uint256 totalStake
    ) internal {
        uint256 rewardsEarned;
        uint256 noOfDays;

        noOfDays = interval.div(DAYS);

        rewardsEarned = noOfDays.mul(
            getOneDayReward(stakedAmount, stakedToken, stakedToken, totalStake)
        );

        // Referrer Earning
        if (referrerAddress != address(0)) {
            uint256 refEarned = (rewardsEarned.mul(u2.refPercentage())).div(
                100 ether
            );
            rewardsEarned = rewardsEarned.sub(refEarned);

            require(
                IERC20(stakedToken).transfer(referrerAddress, refEarned),
                "Transfer Failed"
            );

            emit ReferralEarn(
                referrerAddress,
                user,
                stakedToken,
                refEarned,
                block.timestamp
            );
        }

        //  Rewards Send
        sendToken(user, stakedToken, stakedToken, rewardsEarned);

        uint8 i = 1;

        while (i < intervalDays.length) {
            if (noOfDays >= intervalDays[i]) {
                uint256 balDays = noOfDays.sub((intervalDays[i].sub(1)));
                address rewardToken = u2.tokensSequenceList(stakedToken, i);

                if (
                    rewardToken != stakedToken &&
                    u2.tokenBlockedStatus(stakedToken, rewardToken) == false
                ) {
                    rewardsEarned = balDays.mul(
                        getOneDayReward(
                            stakedAmount,
                            stakedToken,
                            rewardToken,
                            totalStake
                        )
                    );
                    // Referrer Earning
                    if (referrerAddress != address(0)) {
                        uint256 refEarned = (
                            rewardsEarned.mul(u2.refPercentage())
                        )
                        .div(100 ether);
                        rewardsEarned = rewardsEarned.sub(refEarned);

                        require(
                            IERC20(rewardToken).transfer(
                                referrerAddress,
                                refEarned
                            ),
                            "Transfer Failed"
                        );

                        emit ReferralEarn(
                            referrerAddress,
                            user,
                            rewardToken,
                            refEarned,
                            block.timestamp
                        );
                    }
                    //  Rewards Send
                    sendToken(user, stakedToken, rewardToken, rewardsEarned);
                }
                i = i + 1;
            } else {
                break;
            }
        }
    }

    function emergencyUnstake(
        uint256 stakeId,
        address userAddress,
        address[] memory rewardtokens,
        uint256[] memory amount
    ) external onlyOwner {
        (
            address[] memory referrerAddress,
            address[] memory tokenAddress,
            bool[] memory activeStatus,
            ,
            uint256[] memory stakedAmount,

        ) = (u2.viewStakingDetails(userAddress));

        // Checks
        bool isAlreadyUnstaked = u2Proxy.unstakeStatus(userAddress, stakeId);

        if (
            u2UnstakeStatus[userAddress][stakeId] == false &&
            activeStatus[stakeId] == true &&
            isAlreadyUnstaked == false
        ) u2UnstakeStatus[userAddress][stakeId] = true;
        else revert("EMERGENCY: Unstaked Already");

        safeTransfer(tokenAddress[stakeId]);

        // Balance check
        require(
            IERC20(tokenAddress[stakeId]).balanceOf(address(this)) >=
                stakedAmount[stakeId],
            "EMERGENCY : Insufficient Balance"
        );

        u2UpgradeTotalUnStaking[
            tokenAddress[stakeId]
        ] = u2UpgradeTotalUnStaking[tokenAddress[stakeId]].add(
            stakedAmount[stakeId]
        );

        IERC20(tokenAddress[stakeId]).transfer(
            userAddress,
            stakedAmount[stakeId]
        );

        for (uint256 i = 0; i < rewardtokens.length; i++) {
            require(
                IERC20(rewardtokens[i]).balanceOf(address(this)) >= amount[i],
                "EMERGENCY : Insufficient Reward Balance"
            );

            uint256 rewardsEarned = amount[i];

            safeTransfer(tokenAddress[stakeId]);

            if (referrerAddress[stakeId] != address(0)) {
                uint256 refEarned = (rewardsEarned.mul(u2.refPercentage())).div(
                    100 ether
                );
                rewardsEarned = rewardsEarned.sub(refEarned);

                require(
                    IERC20(rewardtokens[i]).transfer(
                        referrerAddress[stakeId],
                        refEarned
                    ),
                    "EMERGENCY : Transfer Failed"
                );

                emit ReferralEarn(
                    referrerAddress[stakeId],
                    userAddress,
                    rewardtokens[i],
                    refEarned,
                    block.timestamp
                );
            }

            sendToken(
                userAddress,
                tokenAddress[stakeId],
                rewardtokens[i],
                rewardsEarned
            );
        }

        // Emit state changes
        emit UnStake(
            userAddress,
            tokenAddress[stakeId],
            stakedAmount[stakeId],
            block.timestamp,
            stakeId
        );
    }

    function lockContract(bool pauseStatus) external onlyOwner {
        if (pauseStatus == true) _pause();
        else if (pauseStatus == false) _unpause();
    }
}
function unStake(address user, uint256 stakeId) external whenNotPaused {
    require(
        msg.sender == user || msg.sender == _owner,
        "UNSTAKE: Invalid User Entry"
    );

    (
        ,
        address[] memory tokenAddress,
        bool[] memory activeStatus,
        ,
        uint256[] memory stakedAmount,
        uint256[] memory startTime
    ) = (u2.viewStakingDetails(user));

    bool isAlreadyUnstaked = u2Proxy.unstakeStatus(user, stakeId);

    // lockableDays check
    require(
        u2UpgradeLockableDetails[tokenAddress[stakeId]].lockableDays <=
            block.timestamp,
        "Token Locked"
    );

    // optional lock check
    if (
        u2UpgradeLockableDetails[tokenAddress[stakeId]].optionableStatus ==
        true
    ) {
        require(
            poolStartTime.add(u2.stakeDuration()) <= block.timestamp,
            "Locked in optional lock"
        );
    }

    // Checks
    if (
        u2UnstakeStatus[user][stakeId] == false &&
        activeStatus[stakeId] == true &&
        isAlreadyUnstaked == false
    ) u2UnstakeStatus[user][stakeId] = true;
    else revert("UNSTAKE : Unstaked Already");

    // State updation
    uint256 totalUnstaking = getTotalUnstaking(tokenAddress[stakeId]);
    uint256 totalStaking = u2.totalStaking(tokenAddress[stakeId]).sub(
        totalUnstaking
    );

    // increase total unstaking
    u2UpgradeTotalUnStaking[
        tokenAddress[stakeId]
    ] = u2UpgradeTotalUnStaking[tokenAddress[stakeId]].add(
        stakedAmount[stakeId]
    );

    // Balance check
    require(
        IERC20(tokenAddress[stakeId]).balanceOf(address(this)) >=
            stakedAmount[stakeId],
        "UNSTAKE : Insufficient Balance"
    );

    IERC20(tokenAddress[stakeId]).transfer(user, stakedAmount[stakeId]);

    if (startTime[stakeId] < poolStartTime.add(u2.stakeDuration())) {
        claimRewards(user, stakeId, totalStaking);
    }

    // Emit state changes
    emit UnStake(
        user,
        tokenAddress[stakeId],
        stakedAmount[stakeId],
        block.timestamp,
        stakeId
    );
}

/**
 * @notice Unstake and claim rewards
 * @param stakeId Stake ID of the user
 */