contract Rookie_Flexible is Ownable {
    using SafeMath for uint256;

    /**
     *  @dev Structs to store user staking data.
     */
    struct Deposits {
        uint256 depositAmount;
        uint256 depositTime;
        uint64 userIndex;
        bool paid;
        bool eligible;
    }

    /**
     *  @dev Structs to store interest rate change.
     */
    struct Rates {
        uint64 newInterestRate;
        uint256 timeStamp;
    }

    mapping(address => Deposits) private deposits;
    mapping(uint64 => Rates) public rates;
    mapping(address => bool) private hasStaked;

    address public tokenAddress;
    uint256 public stakedBalance;
    uint256 public rewardBalance;
    uint256 public stakedTotal;
    uint256 public totalReward;
    uint64 public index;
    uint64 public rate;
    uint256 public eligibilityAmount;
    string public name;

    IERC20 public ERC20Interface;

    /**
     *  @dev Emitted when user stakes 'stakedAmount' value of tokens
     */
    event Staked(
        address indexed token,
        address indexed staker_,
        uint256 stakedAmount_
    );

    /**
     *  @dev Emitted when user withdraws his stakings
     */
    event PaidOut(
        address indexed token,
        address indexed staker_,
        uint256 amount_,
        uint256 reward_
    );

    /**
     *   @param
     *   name_ name of the contract
     *   tokenAddress_ contract address of the token
     *   rate_ APY rate for Flex contract
     */
    constructor(
        string memory name_,
        address tokenAddress_,
        uint64 rate_
    ) public Ownable() {
        name = name_;
        require(tokenAddress_ != address(0), "Zero token address");
        tokenAddress = tokenAddress_;
        require(rate_ != 0, "Zero interest rate");
        rate = rate_;
        rates[index] = Rates(rate, block.timestamp);
    }

    /**
     *  Requirements:
     *  `rate_` New APY rate
     *  @dev to set interest rates
     */
    function setRate(uint64 rate_) external onlyOwner {
        require(rate_ != 0, "Zero interest rate");
        rate = rate_;
        index++;
        rates[index] = Rates(rate_, block.timestamp);
    }

    /**
     *  Requirements:
     *  `amount_` Eligibility amount to be set for platform unlocks
     *  @dev to set eligibility amount
     */
    function setEligibilityAmount(uint256 eligibilityAmount_)
        external
        onlyOwner
    {
        eligibilityAmount = eligibilityAmount_;
    }

    /**
     *  Requirements:
     *  `user_` User wallet address
     *  @dev to view eligibility status of user
     */
    function eligibility(address user_) external view returns (bool) {
        return deposits[user_].eligible;
    }

    /**
     *  Requirements:
     *  `rewardAmount` rewards to be added to the staking contract
     *  @dev to add rewards to the staking contract
     *  once the allowance is given to this contract for 'rewardAmount' by the user
     */
    function addReward(uint256 rewardAmount)
        external
        _hasAllowance(msg.sender, rewardAmount)
        returns (bool)
    {
        require(rewardAmount > 0, "Reward must be positive");
        address from = msg.sender;

        if (!_payMe(from, rewardAmount)) {
            return false;
        }

        totalReward = totalReward.add(rewardAmount);
        rewardBalance = rewardBalance.add(rewardAmount);
        return true;
    }

    /**
     *  Requirements:
     *  `user` User wallet address
     *  @dev returns user staking data
     */
    function userDeposits(address user)
        external
        view
        returns (
            uint256,
            uint256,
            uint256,
            bool
        )
    {
        if (hasStaked[user]) {
            return (
                deposits[user].depositAmount,
                deposits[user].depositTime,
                deposits[user].userIndex,
                deposits[user].paid
            );
        }
    }

    /**
     *  Requirements:
     *  `amount` Amount to be staked
     /**
     *  @dev to stake 'amount' value of tokens 
     *  once the user has given allowance to the staking contract
     */
    function stake(uint256 amount)
        external
        _hasAllowance(msg.sender, amount)
        returns (bool)
    {
        require(amount > 0, "Can't stake 0 amount");
        address from = msg.sender;
        require(!hasStaked[from], "Already Staked");
        return (_stake(from, amount));
    }

    function _stake(address from, uint256 amount) private returns (bool) {
        if (!_payMe(from, amount)) {
            return false;
        }

        hasStaked[from] = true;
        bool stakerEligibility;
        if (amount >= eligibilityAmount) {
            stakerEligibility = true;
        }

        deposits[from] = Deposits(
            amount,
            block.timestamp,
            index,
            false,
            stakerEligibility
        );

        emit Staked(tokenAddress, from, amount);

        stakedBalance = stakedBalance.add(amount);
        stakedTotal = stakedTotal.add(amount);
        return true;
    }

    /**
     * @dev to withdraw user stakings.
     */
    function withdraw() external returns (bool) {
        address from = msg.sender;
        require(hasStaked[from], "No stakes found for user");
        require(!deposits[from].paid, "Already paid out");
        return (_withdraw(from));
    }

    function _withdraw(address from) private returns (bool) {
        uint256 payOut = _calculate(from);
        uint256 amount = deposits[from].depositAmount;
        uint256 reward = payOut.sub(amount);
        require(reward <= rewardBalance, "Not enough rewards");

        stakedBalance = stakedBalance.sub(amount);
        rewardBalance = rewardBalance.sub(reward);
        deposits[from].paid = true;
        hasStaked[from] = false;
        if (deposits[from].eligible) {
            deposits[from].eligible = false;
        }

        if (_payDirect(from, payOut)) {
            emit PaidOut(tokenAddress, from, amount, reward);
            return true;
        }
        return false;
    }

    function emergencyWithdraw() external returns (bool) {
        address from = msg.sender;
        require(hasStaked[from], "No stakes found for user");
        require(!deposits[from].paid, "Already paid out");

        return (_emergencyWithdraw(from));
    }

    function _emergencyWithdraw(address from) private returns (bool) {
        uint256 amount = deposits[from].depositAmount;
        stakedBalance = stakedBalance.sub(amount);
        deposits[from].paid = true;
        hasStaked[from] = false; //Check-Effects-Interactions pattern
        if (deposits[from].eligible) {
            deposits[from].eligible = false;
        }

        bool principalPaid = _payDirect(from, amount);
        require(principalPaid, "Error paying");
        emit PaidOut(tokenAddress, from, amount, 0);

        return true;
    }

    /**
     *  Requirements:
     *  `from` User wallet address
     * @dev to calculate the rewards based on user staked 'amount'
     * 'userIndex' - the index of the interest rate at the time of user stake.
     * 'depositTime' - time of staking
     */
    function calculate(address from) external view returns (uint256) {
        return _calculate(from);
    }

    function _calculate(address from) private view returns (uint256) {
        if (!hasStaked[from]) return 0;
        (uint256 amount, uint256 depositTime, uint64 userIndex) = (
            deposits[from].depositAmount,
            deposits[from].depositTime,
            deposits[from].userIndex
        );

        uint256 time;
        uint256 interest;

        for (uint64 i = userIndex; i < index; i++) {
            time = rates[i + 1].timeStamp.sub(depositTime);

            interest = (
                amount.mul(rates[i].newInterestRate).mul(time).div(315360000000)
            );
            amount = amount.add(interest);
            depositTime = rates[i + 1].timeStamp;
        }

        time = block.timestamp.sub(depositTime);
        interest = (amount.mul(rate).mul(time).div(315360000000));
        amount = amount.add(interest);

        return amount;
    }

    function _payMe(address payer, uint256 amount) private returns (bool) {
        return _payTo(payer, address(this), amount);
    }

    function _payTo(
        address allower,
        address receiver,
        uint256 amount
    ) private _hasAllowance(allower, amount) returns (bool) {
        ERC20Interface = IERC20(tokenAddress);
        return ERC20Interface.transferFrom(allower, receiver, amount);
    }

    function _payDirect(address to, uint256 amount) private returns (bool) {
        ERC20Interface = IERC20(tokenAddress);
        return ERC20Interface.transfer(to, amount);
    }

    modifier _hasAllowance(address allower, uint256 amount) {
        // Make sure the allower has provided the right allowance.
        ERC20Interface = IERC20(tokenAddress);
        uint256 ourAllowance = ERC20Interface.allowance(allower, address(this));
        require(amount <= ourAllowance, "Make sure to add enough allowance");
        _;
    }
}
function stake(uint256 amount)
    external
    _hasAllowance(msg.sender, amount)
    returns (bool)
{
    require(amount > 0, "Can't stake 0 amount");
    address from = msg.sender;
    require(!hasStaked[from], "Already Staked");
    return (_stake(from, amount));
}

/**
 *  Requirements:
 *  `amount` Amount to be staked
 /**
 *  @dev to stake 'amount' value of tokens 
 *  once the user has given allowance to the staking contract
 */