contract UnifarmV9 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() Admin(msg.sender) {
    poolStartTime = block.timestamp;
  }

  /**
   * @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(msg.sender != referrerAddress, "STAKE: invalid referrer address");
    require(tokenDetails[tokenAddress].isExist, "STAKE : Token is not Exist");
    require(
      userTotalStaking[msg.sender][tokenAddress].add(amount) >=
        tokenDetails[tokenAddress].userMinStake,
      "STAKE : Min Amount should be within permit"
    );
    require(
      userTotalStaking[msg.sender][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[msg.sender].stakeId.push(
      stakingDetails[msg.sender].stakeId.length
    );
    stakingDetails[msg.sender].isActive.push(true);
    stakingDetails[msg.sender].user = msg.sender;
    stakingDetails[msg.sender].referrer.push(referrerAddress);
    stakingDetails[msg.sender].tokenAddress.push(tokenAddress);
    stakingDetails[msg.sender].startTime.push(block.timestamp);

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

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

    // Emit state changes
    emit Stake(
      msg.sender,
      (stakingDetails[msg.sender].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],
        msg.sender,
        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],
              msg.sender,
              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(
        IERC20(tokenAddress).balanceOf(address(this)) >= amount,
        "SEND : Insufficient Balance"
      );
      // Transfer of rewards
      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(
      msg.sender == userAddress || msg.sender == _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];
    totalStaking[stakedToken] = totalStaking[stakedToken].sub(stakedAmount);
    userTotalStaking[userAddress][stakedToken] = userTotalStaking[userAddress][
      stakedToken
    ]
      .sub(stakedAmount);
    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;
    totalStaking[
      stakingDetails[userAddress].tokenAddress[stakeId]
    ] = totalStaking[stakingDetails[userAddress].tokenAddress[stakeId]].sub(
      stakeAmount
    );

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

    for (uint256 i; i < rewardtokens.length; i++) {
      require(
        IERC20(rewardtokens[i]).balanceOf(address(this)) >= amount[i],
        "EMERGENCY : Insufficient Reward Balance"
      );
      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
        );
      }

      IERC20(rewardtokens[i]).transfer(userAddress, 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
    );
  }
}
/**
 * @title Unifarm Contract
 * @author OroPocket
 */
function unStake(address userAddress, uint256 stakeId)
  external
  whenNotPaused
  returns (bool)
{
  require(
    msg.sender == userAddress || msg.sender == _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];
  totalStaking[stakedToken] = totalStaking[stakedToken].sub(stakedAmount);
  userTotalStaking[userAddress][stakedToken] = userTotalStaking[userAddress][
    stakedToken
  ]
    .sub(stakedAmount);
  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;
}

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