contract BigBang is Pausable, Ownable {
    using SafeMath for uint256;
  
    // Info of each pool.
    struct PoolInfo {
        address stakingToken;                   // Address of staking token contract.
        address rewardsToken;                   // Address of rewards token contract.
        uint256 tier1Fee;                       // TIER1Fee: uint representing the rewards rate (e.g. 5 stands for 0.005 -> 0.5%)
        uint256 tier2Fee;                       // TIER2Fee: uint representing the rewards rate (e.g. 5  stands for 0.005 -> 0.5%)
        uint256 tier3Fee;                       // TIER3Fee: uint representing the rewards rate (e.g. 5  stands for 0.005 -> 0.5%)
        uint256 tier4Fee;                       // TIER4Fee: uint representing the rewards rate (e.g. 5  stands for 0.005 -> 0.5%)
        uint256 tier5Fee;                       // TIER5Fee: uint representing the rewards rate (e.g. 5  stands for 0.005 -> 0.5%)
        uint256 multiplier;                     // 1 or 10 (1 month)
        uint256  uom;                           // unit of measure: 
                                                   // 0: month
                                                   // 1: week
                                                   // 2: day
                                                   // 3: hour
                                                   // 4: minute
        uint256 totalDeposited;                 // Total deposits of the stake contract
        uint256 totalRewards;                   // Total deposits of the rewards contract
        uint256 totalRewardsDebt;               // Total debt towards the stakers
    }
    
  

   /** @dev Info of each user. */
    struct UserInfo {
        uint256 amount;     // How many Staking tokens the user has provided.
        uint256 rewardDebt; // Reward debt. See explanation below.
        uint256 endOfStake; // Date when the rewards can be claimed
        uint256 apy;        // APY
        /*
        We do some fancy math here. Basically, any point in time, the amount of rewards
        entitled to a user but is pending to be distributed is:
        
        user.rewardDebt = (user.amount * pool.accCakePerShare)  
        
        */
    }


    /** @dev Info of each user that stakes LP tokens.*/
    mapping (uint256 => mapping (address => UserInfo)) public userInfo;
    /** @dev Info of each pool. */
    PoolInfo[] public poolInfo;


    /** @dev notify that StakeContract address has been changed
     * @param oldSC old address of the staking contract
     * @param newSC new address of the staking contract
     */
    event NotifyNewStakingContract(address oldSC, address newSC);

      /** @dev notify that StakeContract address has been changed
     * @param oldRC old address of the rewards contract
     * @param newRC new address of the rewards contract
     */
    event NotifyNewRewardsContract(address oldRC, address newRC);

    /** @dev trigger notification of deposits
     * @param pid           pid of the pool
     * @param sender        sender of transaction
     * @param amount        amount of staked tokens
     */
    event NotifyStake(uint256 pid, address sender, uint256 amount);

    /** @dev trigger notification of staked amount
     * @param pid           pid of the pool
     * @param sender        sender of transaction
     * @param amount        amount of withdrawal tokens
     */
    
    event NotifyUnstake(
        uint256 pid,
        address sender,
        uint256 amount
    );

    /** @dev trigger notification of withdrawal
     * @param pid               pid of the pool
     * @param sender            address of msg.sender
     * @param rewardAmount      users final rewards withdrawn
     */
    event NotifyRewardClaimed(
        uint256 pid, 
        address sender, 
        uint256 rewardAmount);

    /** @dev trigger notification of withdrawal
     * @param pid               pid of the pool
     * @param rewardAmount      users final rewards withdrawn
     */
    event NotifyRewardAdded(
        uint256 pid, 
        uint256 rewardAmount);


    /** @dev contract constructor
     */
    constructor () {}
        
    /************************ PUBLIC Queries **********************************/
    
    /** @dev check total rewards in the pool
     * @param _pid of the pool
     * @param _poolAddress address of the pool
     */

    function totalRewardsInThePool(uint256 _pid, address _poolAddress) public view returns (uint256){
        // Retrieve Pool information
        PoolInfo storage pool = poolInfo[_pid];
        return ERC20(pool.rewardsToken).balanceOf(_poolAddress);
        }

    /** @dev check if user has already staked something
     * @param _pid of the pool
     * @param _address of the user
     */
    function isAlreadyUser(uint256 _pid, address _address ) public view returns (uint256) {
        UserInfo storage user = userInfo[_pid][_address];
        uint256 isUser = 0;
        if (user.amount > 0)
        {
            isUser = 1;
        }
        return isUser;
        
    }

    /** @dev check if the pool is open for staking: the total rewardsDebt of the users does not exceed the total rewards of the pool.
     * @param _pid of the pool
     */
    function isPoolOpen(uint256 _pid) public view returns (bool) {
        // Retrieve Pool information
        PoolInfo storage pool = poolInfo[_pid];
        // Check if the total rewards in the pool are minor than the debt that the pool has towards the stakers
        if (pool.totalRewards < pool.totalRewardsDebt)
        {
            // The pool is closed for staking
            return false;
        }
        // The pool is open for staking
        return true;
    }
    
    /**
     * @dev check how many pools have been created.
     */
     function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    
    /**@dev retrieve amount deposited by the msg.sender
     * @param _address of the user
     * @param _pid of the pool
     * @return uint
     */
    function getBalanceOfDeposit(uint256 _pid, address _address)
        external
        view
        returns (uint256)
    {
        UserInfo storage user = userInfo[_pid][_address];
        return user.amount;
    }
    

    /** @dev retreive current state of users funds
     * @param _address of the user
     * @param _pid of the pool
     * @return array of values describing the current state of user
     */
    function getState(uint256 _pid,  address _address) external view returns (uint256[] memory) {
        UserInfo storage user = userInfo[_pid][_address];
        uint256[] memory state = new uint256[](5);
        state[0] = user.amount;                                     // total staked amount for getting rewards
        state[1] = user.endOfStake;                                 // remaining days for getting rewards
        state[2] = claimableRewards(_pid, _address);                // claimable Rewards
        state[3] = user.apy;                                        // apy
        state[4] = user.rewardDebt;                                 // total reward debt

        return state;
    }


    /************************ USER MANAGEMENT **********************************/

    /************************ POOL MANAGEMENT **********************************/
    
     /** @dev adding a new stiaking pool
     * @param _stakingToken: token to be used for staking
     * @param _rewardsToken token to be used for rewards
     * @param _tier1Fee Fee tier 1
     * @param _tier2Fee Fee tier 2
     * @param _tier3Fee Fee tier 3
     * @param _tier4Fee Fee tier 4
     * @param _tier5Fee Fee tier 5
     */
    
    
    function addPool  (
        address _stakingToken,
        address _rewardsToken,
        uint256 _tier1Fee,
        uint256 _tier2Fee,
        uint256 _tier3Fee,
        uint256 _tier4Fee,
        uint256 _tier5Fee, 
        uint256 _uom,
        uint256 _multiplier
        ) external 
    {
            poolInfo.push(PoolInfo({
            stakingToken:       _stakingToken,
            rewardsToken:       _rewardsToken,
            tier1Fee:           _tier1Fee,
            tier2Fee:           _tier2Fee,
            tier3Fee:           _tier3Fee,
            tier4Fee:           _tier4Fee,
            tier5Fee:           _tier5Fee,
            uom:                _uom,
            multiplier:         _multiplier,
            totalDeposited:0,
            totalRewards:0,
            totalRewardsDebt:0
        })
        );
    }
    

    /** @dev set staking contract address
     * @param _pid: pid of the pool
     * @param _stakeContract new address to change staking contract / mechanism
     */
    function setStakeContract(uint256 _pid, address _stakeContract) external onlyOwner {
        require(_stakeContract != address(0));
        PoolInfo storage pool = poolInfo[_pid];
        address oldSC = pool.stakingToken;
        pool.stakingToken = _stakeContract;
        emit NotifyNewStakingContract(oldSC, _stakeContract);
    }

    /** @dev set rewards contract address
     ** @pid pid of the pool
     * @param _rewardContract new address to change  contract / mechanism
     */
    function setRewardsContract(uint256 _pid, address _rewardContract) external onlyOwner {
        require(_rewardContract != address(0));
        PoolInfo storage pool = poolInfo[_pid];
        address oldRC = pool.rewardsToken;
        pool.rewardsToken = _rewardContract;
        emit NotifyNewRewardsContract(oldRC, _rewardContract);
    }

    /** @dev add rewards amount to the pool
     ** @pid pid of the pool
     * @param _pid pid of the pool 
     * @param _amount of rewards token
     */
    function addRewardsToThePool(uint256 _pid, address _poolAddress, uint256 _amount) external {
        PoolInfo storage pool = poolInfo[_pid];
        require(totalRewardsInThePool(_pid, _poolAddress) >= _amount, 
                "Insufficient Rewards token for the pool"
        );
        pool.totalRewards += _amount;
        emit NotifyRewardAdded(_pid, _amount);
    }

  
    /************************ POOL MANAGEMENT **********************************/

    /************************ POOL MANAGEMENT INTERNAL **********************************/

    /** @dev adding a new user to the staking pool
    * @param _pid: pid of the pool
    * @param _user: address of the user
    * @param _amount: deposited amount
    * @param _endOfStake: date of the end of stake
    * @param _rewardDebt: rewardDebt
    */
    
    function addUserToPool(
        uint256 _pid,
        address _user,
        uint256 _amount,
        uint256 _endOfStake,
        uint256 _apy,
        uint256 _rewardDebt
    ) internal {
        UserInfo memory user = UserInfo({
            amount: _amount,
            rewardDebt: _rewardDebt,
            endOfStake: _endOfStake,
            apy: _apy
        });
        userInfo[_pid][_user] = user;
    }
    
     /** @dev remove user from the staking pool*/

    function removeUserFromPool(
        uint256 _pid,
        address _user
    ) internal {
        delete userInfo[_pid][_user];
    }

    /** @dev calculate rewards for the user
     * @param _pid pid of the pool
     * @param _apy of the user
     * @param _amount staked by the user
     * @return uint that is the value of the rewards 
    */

    function calculateRewards(uint256 _pid, uint256 _apy, uint256 _amount) public view returns(uint256){
        // retrieve pool information
        PoolInfo memory pool = poolInfo[_pid];
        uint256 rewards= _amount.div(pool.totalRewards).mul(_apy).div(1000);
        return rewards;
    }

    /************************ POOL MANAGEMENT INTERNAL **********************************/


    /************************ PUBLIC POOL ACTIONS **********************************/

    /** @dev deposit funds to the contract
     *@param _dValue value of tokens that the msg.sender wants to deposit
     *@param _tier days msg.sender wants to deposit the token for
     *@param _pid pool
     
     */

    function stake(uint256 _pid, address _address, address _poolAddress, uint256 _dValue, uint256 _tier) external whenNotPaused {
        // require the user to be a new user
        PoolInfo storage pool = poolInfo[_pid];
        uint256 endOfStake;
        uint256 apy;
        uint256 totalSupplyRewardsToken = ERC20(pool.rewardsToken).totalSupply();
        uint256 totalSupplyStakingToken = ERC20(pool.stakingToken).totalSupply();

        // user can't have more than 1 position in the pool
        require(
            isAlreadyUser(_pid, _address) == 0,
            "User has already a position in the pool"
        );

        // pool has to be opened
        require (
            isPoolOpen(_pid) == true,
            "The pool is closed: no enough rewards"
        );

        // require the user to have enough tokens to be deposited
        require(
            ERC20(pool.stakingToken).balanceOf(_address) >= _dValue,
            "The user does not have enough tokens to deposit"
        );

        // increase the total deposited variable of the value of tokens transferred as parameter
        pool.totalDeposited = pool.totalDeposited.add(_dValue);

        //calculate tier % for the user
        if (_tier == 1) {
            apy = pool.tier1Fee;
        }
        if (_tier == 2) {
            apy = pool.tier2Fee;
        }
        if (_tier == 3) {
            apy = pool.tier3Fee;
        }
        if (_tier == 4) {
            apy = pool.tier4Fee;
        }
        if (_tier == 5) {
            apy = pool.tier5Fee;
        }

        //calculate end of stake
        
        if(pool.uom == 0) {
           endOfStake = block.timestamp + (_tier * pool.multiplier *  30 days);    
        }
        if(pool.uom == 1) {
            endOfStake = block.timestamp + (_tier * pool.multiplier * 1 weeks);    
        }
        if(pool.uom == 2) {
            endOfStake = block.timestamp + (_tier * pool.multiplier * 1 days);        
        }
        if(pool.uom == 3) {
            endOfStake = block.timestamp + (_tier * pool.multiplier * 1 hours);    
        }
        if(pool.uom == 4) {
            endOfStake = block.timestamp + (_tier * pool.multiplier * 1 minutes);    
        }
        
        
        // calculate rewards
        uint256 weigthedRatio = totalSupplyRewardsToken / totalSupplyStakingToken;
        uint256 rewards= _dValue.mul(apy).div(1000).mul(weigthedRatio);

        // update the total reward debt of the pool
        pool.totalRewardsDebt = pool.totalRewardsDebt.add(rewards);

        // add  user in the mapping array
        addUserToPool(
            _pid
            ,_address
            ,_dValue
            ,endOfStake
            ,apy
            ,rewards
        );

        // transfer the tokens from the ERC20 smart contract to this pool address
        ERC20(pool.stakingToken).transferFrom(_address , _poolAddress, _dValue);
        // emit notify stake event
        emit NotifyStake(_pid, _address, _dValue);
    }

    /** @dev withdrawal funds out of pool
     * @param _pid pid of the pool
     * @param _address of the user
     * @param wdValue amount to unstake
     */
    function unstake(uint256 _pid, address _address, uint256 wdValue) external whenNotPaused {
        
        // get pool information
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo memory user = userInfo[_pid][_address];
        // require the amount to withdraw to be > 0
        require(wdValue > 0);
        // require the sender to have a deposited balance >= at the withdraw requested amount
        require(user.amount >= wdValue);
        
        // transfer the withdraw requested amount to the spender
        ERC20(pool.stakingToken).transferFrom(msg.sender, _address, wdValue);

        // emit new event
        emit NotifyUnstake(
            _pid,
            _address,
            wdValue
        );
        
        // calculate new deposited amount
        user.amount.sub(wdValue);

        // remove user from the mapping array
        removeUserFromPool(_pid, _address);
        
        // redute the deposited balance by the withdraw requested amount
        pool.totalDeposited = pool.totalDeposited.sub(wdValue);
        
    }

    /** @dev calculate rewards for the user
     * @param _address of the user
     * @return uint that is the value of the rewards
     */

    function claimableRewards(uint256 _pid, address _address)
        public
        view
        returns (uint256)
    {
        
        UserInfo storage user = userInfo[_pid][_address];
        
        uint256 rewards = 0;
        
        
        // check if the staking period is over
        if (block.timestamp > user.endOfStake) {
           rewards = user.rewardDebt;
            
        }
        // return the rewards balance
        return rewards;
    }

    /** @dev claim rewards out of pool
     * @param _address of the user
     * @param _pid of the pool
     */
    
    function claimRewards(uint256 _pid, address _address) external whenNotPaused {
        // add a require
        // get user information
        UserInfo storage user = userInfo[_pid][_address];
        uint256 prevRewardDebt = user.rewardDebt;
        uint256 newRewardDebt;
        
        // get pool information
        PoolInfo storage pool = poolInfo[_pid]; 
        
        // transfer the rewards
        ERC20(pool.rewardsToken).transferFrom(msg.sender, _address,claimableRewards(_pid, _address));
        emit NotifyRewardClaimed(_pid, _address, claimableRewards(_pid, _address));
        
        // update the user information about the rewardDebt
        newRewardDebt = prevRewardDebt - claimableRewards(_pid, _address);
        user.rewardDebt = newRewardDebt;
        
        // update pool information
        pool.totalRewardsDebt -= prevRewardDebt;
    }
    
    /************************ PUBLIC POOL ACTIONS **********************************/
    
}
/* @title Big Bang Contract
 * Open Zeppelin Pausable is Ownable.  contains address owner */
function calculateRewards(uint256 _pid, uint256 _apy, uint256 _amount) public view returns(uint256){
    // retrieve pool information
    PoolInfo memory pool = poolInfo[_pid];
    uint256 rewards= _amount.div(pool.totalRewards).mul(_apy).div(1000);
    return rewards;
}

/** @dev calculate rewards for the user
 * @param _pid pid of the pool
 * @param _apy of the user
 * @param _amount staked by the user
 * @return uint that is the value of the rewards 
 */