contract UnifarmV22 is Admin {
    // Wrappers over Solidity's arithmetic operations
    using SafeMath for uint256;

    // Stores Stake Details
    struct stakeInfo {
        address user;
        bool[] isActive;
        address[] referrer;
        address[] tokenAddress;
        uint256[] stakeId;
        uint256[] stakedAmount;
        uint256[] startTime;
    }

    // Mapping
    mapping(address => stakeInfo) public stakingDetails;
    mapping(address => mapping(address => uint256)) public userTotalStaking;
    mapping(address => uint256) public totalStaking;
    uint256 public poolStartTime;

    // Events
    event Stake(
        address indexed userAddress,
        uint256 stakeId,
        address indexed referrerAddress,
        address indexed tokenAddress,
        uint256 stakedAmount,
        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
    );

    constructor(address _trustedForwarder) Admin(_msgSender()) {
        poolStartTime = block.timestamp;
        trustedForwarder = _trustedForwarder;
    }

    /**
     * @notice Stake tokens to earn rewards
     * @param tokenAddress Staking token address
     * @param amount Amount of tokens to be staked
     */

    function stake(
        address referrerAddress,
        address tokenAddress,
        uint256 amount
    ) external whenNotPaused {
        // checks
        require(
            _msgSender() != referrerAddress,
            "STAKE: invalid referrer address"
        );
        require(
            tokenDetails[tokenAddress].isExist,
            "STAKE : Token is not Exist"
        );
        require(
            userTotalStaking[_msgSender()][tokenAddress].add(amount) >=
                tokenDetails[tokenAddress].userMinStake,
            "STAKE : Min Amount should be within permit"
        );
        require(
            userTotalStaking[_msgSender()][tokenAddress].add(amount) <=
                tokenDetails[tokenAddress].userMaxStake,
            "STAKE : Max Amount should be within permit"
        );
        require(
            totalStaking[tokenAddress].add(amount) <=
                tokenDetails[tokenAddress].totalMaxStake,
            "STAKE : Maxlimit exceeds"
        );

        require(
            poolStartTime.add(stakeDuration) > block.timestamp,
            "STAKE: Staking Time Completed"
        );

        // Storing stake details
        stakingDetails[_msgSender()].stakeId.push(
            stakingDetails[_msgSender()].stakeId.length
        );
        stakingDetails[_msgSender()].isActive.push(true);
        stakingDetails[_msgSender()].user = _msgSender();
        stakingDetails[_msgSender()].referrer.push(referrerAddress);
        stakingDetails[_msgSender()].tokenAddress.push(tokenAddress);
        stakingDetails[_msgSender()].startTime.push(block.timestamp);

        // Update total staking amount
        stakingDetails[_msgSender()].stakedAmount.push(amount);
        totalStaking[tokenAddress] = totalStaking[tokenAddress].add(amount);
        userTotalStaking[_msgSender()][tokenAddress] = userTotalStaking[
            _msgSender()
        ][tokenAddress].add(amount);

        // Transfer tokens from user to contract
        require(
            IERC20(tokenAddress).transferFrom(
                _msgSender(),
                address(this),
                amount
            ),
            "Transfer Failed"
        );

        // Emit state changes
        emit Stake(
            _msgSender(),
            (stakingDetails[_msgSender()].stakeId.length.sub(1)),
            referrerAddress,
            tokenAddress,
            amount,
            block.timestamp
        );
    }

    /**
     * @notice Claim accumulated rewards
     * @param stakeId Stake ID of the user
     * @param stakedAmount Staked amount of the user
     */

    function claimRewards(
        address userAddress,
        uint256 stakeId,
        uint256 stakedAmount,
        uint256 totalStake
    ) internal {
        // Local variables
        uint256 interval;
        uint256 endOfProfit;

        interval = poolStartTime.add(stakeDuration);

        // Interval calculation
        if (interval > block.timestamp) endOfProfit = block.timestamp;
        else endOfProfit = poolStartTime.add(stakeDuration);

        interval = endOfProfit.sub(
            stakingDetails[userAddress].startTime[stakeId]
        );
        uint256[2] memory stakeData;
        stakeData[0] = (stakedAmount);
        stakeData[1] = (totalStake);

        // Reward calculation
        if (interval >= HOURS)
            _rewardCalculation(userAddress, stakeId, stakeData, interval);
    }

    function _rewardCalculation(
        address userAddress,
        uint256 stakeId,
        uint256[2] memory stakingData,
        uint256 interval
    ) internal {
        uint256 rewardsEarned;
        uint256 refEarned;
        uint256[2] memory noOfDays;

        noOfDays[1] = interval.div(HOURS);
        noOfDays[0] = interval.div(DAYS);

        rewardsEarned = noOfDays[1].mul(
            getOneDayReward(
                stakingData[0],
                stakingDetails[userAddress].tokenAddress[stakeId],
                stakingDetails[userAddress].tokenAddress[stakeId],
                stakingData[1]
            )
        );

        // Referrer Earning
        if (stakingDetails[userAddress].referrer[stakeId] != address(0)) {
            refEarned = (rewardsEarned.mul(refPercentage)).div(100 ether);
            rewardsEarned = rewardsEarned.sub(refEarned);

            require(
                IERC20(stakingDetails[userAddress].tokenAddress[stakeId])
                    .transfer(
                        stakingDetails[userAddress].referrer[stakeId],
                        refEarned
                    ) == true,
                "Transfer Failed"
            );

            emit ReferralEarn(
                stakingDetails[userAddress].referrer[stakeId],
                _msgSender(),
                stakingDetails[userAddress].tokenAddress[stakeId],
                refEarned,
                block.timestamp
            );
        }

        //  Rewards Send
        sendToken(
            userAddress,
            stakingDetails[userAddress].tokenAddress[stakeId],
            stakingDetails[userAddress].tokenAddress[stakeId],
            rewardsEarned
        );

        uint8 i = 1;

        while (i < intervalDays.length) {
            if (noOfDays[0] >= intervalDays[i]) {
                uint256 reductionHours = (intervalDays[i].sub(1)).mul(24);
                uint256 balHours = noOfDays[1].sub(reductionHours);

                address rewardToken = tokensSequenceList[
                    stakingDetails[userAddress].tokenAddress[stakeId]
                ][i];

                if (
                    rewardToken !=
                    stakingDetails[userAddress].tokenAddress[stakeId] &&
                    tokenBlockedStatus[
                        stakingDetails[userAddress].tokenAddress[stakeId]
                    ][rewardToken] ==
                    false
                ) {
                    rewardsEarned = balHours.mul(
                        getOneDayReward(
                            stakingData[0],
                            stakingDetails[userAddress].tokenAddress[stakeId],
                            rewardToken,
                            stakingData[1]
                        )
                    );

                    // Referrer Earning

                    if (
                        stakingDetails[userAddress].referrer[stakeId] !=
                        address(0)
                    ) {
                        refEarned = (rewardsEarned.mul(refPercentage)).div(
                            100 ether
                        );
                        rewardsEarned = rewardsEarned.sub(refEarned);

                        require(
                            IERC20(rewardToken).transfer(
                                stakingDetails[userAddress].referrer[stakeId],
                                refEarned
                            ) == true,
                            "Transfer Failed"
                        );

                        emit ReferralEarn(
                            stakingDetails[userAddress].referrer[stakeId],
                            _msgSender(),
                            stakingDetails[userAddress].tokenAddress[stakeId],
                            refEarned,
                            block.timestamp
                        );
                    }

                    //  Rewards Send
                    sendToken(
                        userAddress,
                        stakingDetails[userAddress].tokenAddress[stakeId],
                        rewardToken,
                        rewardsEarned
                    );
                }
                i = i + 1;
            } else {
                break;
            }
        }
    }

    /**
     * @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
     * @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 (tokenDetails[stakedToken].optionableStatus) {
            stakedAmount = stakedAmount.mul(optionableBenefit);
            lockBenefit = stakedAmount.mul(optionableBenefit.sub(1));
            reward = (
                stakedAmount.mul(
                    tokenDailyDistribution[stakedToken][rewardToken]
                )
            ).div(totalStake.add(lockBenefit));
        } else {
            reward = (
                stakedAmount.mul(
                    tokenDailyDistribution[stakedToken][rewardToken]
                )
            ).div(totalStake);
        }
    }

    /**
     * @notice Get rewards for one day
     * @param stakedToken Stake amount of the user
     * @param tokenAddress Reward token address
     * @param amount Amount to be transferred as reward
     */
    function sendToken(
        address userAddress,
        address stakedToken,
        address tokenAddress,
        uint256 amount
    ) internal {
        // Checks
        if (tokenAddress != address(0)) {
            require(
                rewardCap[tokenAddress] >= amount,
                "SEND : Insufficient Reward Balance"
            );
            // Transfer of rewards
            rewardCap[tokenAddress] = rewardCap[tokenAddress].sub(amount);

            require(
                IERC20(tokenAddress).transfer(userAddress, amount),
                "Transfer failed"
            );

            // Emit state changes
            emit Claim(
                userAddress,
                stakedToken,
                tokenAddress,
                amount,
                block.timestamp
            );
        }
    }

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

        address stakedToken = stakingDetails[userAddress].tokenAddress[stakeId];

        // lockableDays check
        require(
            tokenDetails[stakedToken].lockableDays <= block.timestamp,
            "UNSTAKE: Token Locked"
        );

        // optional lock check
        if (tokenDetails[stakedToken].optionableStatus)
            require(
                stakingDetails[userAddress].startTime[stakeId].add(
                    stakeDuration
                ) <= block.timestamp,
                "UNSTAKE: Locked in optional lock"
            );

        // Checks
        require(
            stakingDetails[userAddress].stakedAmount[stakeId] > 0 ||
                stakingDetails[userAddress].isActive[stakeId] == true,
            "UNSTAKE : Already Claimed (or) Insufficient Staked"
        );

        // State updation
        uint256 stakedAmount = stakingDetails[userAddress].stakedAmount[
            stakeId
        ];
        uint256 totalStaking1 = totalStaking[stakedToken];

        stakingDetails[userAddress].stakedAmount[stakeId] = 0;
        stakingDetails[userAddress].isActive[stakeId] = false;

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

        // Transfer staked token back to user
        IERC20(stakingDetails[userAddress].tokenAddress[stakeId]).transfer(
            userAddress,
            stakedAmount
        );

        claimRewards(userAddress, stakeId, stakedAmount, totalStaking1);

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

        return true;
    }

    function emergencyUnstake(
        uint256 stakeId,
        address userAddress,
        address[] memory rewardtokens,
        uint256[] memory amount
    ) external onlyOwner {
        // Checks
        require(
            stakingDetails[userAddress].stakedAmount[stakeId] > 0 &&
                stakingDetails[userAddress].isActive[stakeId] == true,
            "EMERGENCY : Already Claimed (or) Insufficient Staked"
        );

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

        uint256 stakeAmount = stakingDetails[userAddress].stakedAmount[stakeId];
        stakingDetails[userAddress].isActive[stakeId] = false;
        stakingDetails[userAddress].stakedAmount[stakeId] = 0;

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

        for (uint256 i; i < rewardtokens.length; i++) {
            uint256 rewardsEarned = amount[i];

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

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

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

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

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

    /**
     * @notice View staking details
     * @param _user User address
     */
    function viewStakingDetails(address _user)
        public
        view
        returns (
            address[] memory,
            address[] memory,
            bool[] memory,
            uint256[] memory,
            uint256[] memory,
            uint256[] memory
        )
    {
        return (
            stakingDetails[_user].referrer,
            stakingDetails[_user].tokenAddress,
            stakingDetails[_user].isActive,
            stakingDetails[_user].stakeId,
            stakingDetails[_user].stakedAmount,
            stakingDetails[_user].startTime
        );
    }

    /**
     * Override this function.
     * This version is to keep track of BaseRelayRecipient you are using
     * in your contract.
     */
    function versionRecipient() external pure override returns (string memory) {
        return "1";
    }

    function updateTrustForwarder(address _newTrustForwarder)
        external
        onlyOwner
    {
        trustedForwarder = _newTrustForwarder;
    }

    function pause() external onlyOwner returns (bool) {
        _pause();
        return true;
    }

    function unpause() external onlyOwner returns (bool) {
        _unpause();
        return true;
    }
}
/**
 * @title Unifarm Contract
 * @author OroPocket
 */
function viewStakingDetails(address _user)
    public
    view
    returns (
        address[] memory,
        address[] memory,
        bool[] memory,
        uint256[] memory,
        uint256[] memory,
        uint256[] memory
    )
{
    return (
        stakingDetails[_user].referrer,
        stakingDetails[_user].tokenAddress,
        stakingDetails[_user].isActive,
        stakingDetails[_user].stakeId,
        stakingDetails[_user].stakedAmount,
        stakingDetails[_user].startTime
    );
}

/**
 * @notice View staking details
 * @param _user User address
 */