contract YieldFarming is OwnableUpgradeSafe {
    using SafeMath for uint256;
    

    // List of usable variables & structs
    IERC20 public sqrl;
    address public sqrlAddress;

    
    struct Plan {
        address sourceToken;
        uint256 tokenMultiplier; // actual multipler = tokenMultiplier / tokenMultiplierDivisor
        uint256 tokenMultiplierDivisor; // actual multipler = tokenMultiplier / tokenMultiplierDivisor
        uint256 multiplierCyclePeriod; // in seconds
        uint256 multiplierMaxCycle; // maximum number of times
        uint256 minimumTokensRequired;
        address planAuthor;
        bool planEnabled;
    }

    Plan[] public plans;

    struct UserInfo {
        uint256 _amount;
        address _tokenType;
        uint256 _timestamp;
    }

    uint256 lastRewardBalance;

    mapping (address => mapping(uint256 => UserInfo)) public userInfo;
    

    //Emitters to log the events & actions
    event ChangeByAddPlan(address planAuthor, uint256 planId);

    event ChangeByUpdatePlan(address planAuthor, uint256 planId);

    event StakeEvent(address indexed user, uint256 planId, uint256 amount);
    
    event UnstakeEvent(address indexed user, uint256 planId, uint256 amount);

    event UnstakeRewardEvent(address indexed user, uint256 planId, uint256 rewardAmount);

    event EmergencyWithdrawal(address user, uint256 amount);

    event OverallRewardBalanceEvent(uint256 rewardBalance);

    event RewardBalanceNow(uint256 rewardBalance);
    

    // Initializer function (replaces constructor)
    function initialize(address _sqrlAddress, uint256 _lastRewardBalance) public initializer {
        OwnableUpgradeSafe.__Ownable_init();
        sqrl = IERC20(_sqrlAddress);
        sqrlAddress = _sqrlAddress;
        lastRewardBalance = _lastRewardBalance;
    }

    //
    // 1. Plan creation: this section covers the creation of yield farming plans
    // only the owner is allow to create and modify plans
    //

    //Add yield farming plans
    function addPlan(address _sourceToken, uint256 _tokenMultiplier, uint256 _tokenMultiplierDivisor, uint256 _multiplierCyclePeriod, uint256 _multiplierMaxCycle, uint256 _minimumTokensRequired, bool _planEnabled) public onlyOwner returns(Plan[] memory) {
        Plan storage _newPlan = plans.push();
        _newPlan.sourceToken = _sourceToken;
        _newPlan.tokenMultiplier = _tokenMultiplier;
        _newPlan.tokenMultiplierDivisor = _tokenMultiplierDivisor;
        _newPlan.multiplierCyclePeriod = _multiplierCyclePeriod;
        _newPlan.multiplierMaxCycle = _multiplierMaxCycle;
        _newPlan.minimumTokensRequired = _minimumTokensRequired;
        _newPlan.planAuthor = msg.sender;
        _newPlan.planEnabled = _planEnabled;

        emit ChangeByAddPlan(msg.sender, plans.length - 1);
        return plans;
    }

    //Yield farm add plan storage
    function storageDisablePlan(uint256 _planId, Plan[] storage plansArray, bool _planEnableDisable) internal {
        plansArray[_planId].planEnabled = _planEnableDisable;
    }

    //Disable yield farming plans
    function switchPlan(uint256 _planId) public onlyOwner returns(Plan[] memory) {
        
        bool _planEnableDisable;

        if (plans[_planId].planEnabled == true){
            _planEnableDisable = false;
        }else{
            _planEnableDisable = true;
        }

        storageDisablePlan(_planId, plans, _planEnableDisable);

        emit ChangeByUpdatePlan(msg.sender, _planId);

        return plans;
    }

    //List yield farming plans
    function getPlans() public view returns(Plan[] memory) {
        return plans;
    }

    //
    // 2. Staking mechanism: this section covers the staking mechanism that is open to the public.
    // Contains stake() and unstake()
    //
    function stake(uint256 _planId, uint256 _stakeAmount) public {

        address _tokenType = plans[_planId].sourceToken;

        require(IERC20(_tokenType).balanceOf(msg.sender) > 0, "Insufficient token balance.");
        require(_stakeAmount <= IERC20(_tokenType).balanceOf(msg.sender), "You cannot stake more than what you own.");
        require(plans[_planId].planEnabled, "This reward plan is disabled.");
        require(userInfo[msg.sender][_planId]._amount == 0, "You have already staked in this reward plan.");
        require(plans[_planId].minimumTokensRequired <= IERC20(_tokenType).balanceOf(msg.sender), "You do not have sufficient balance to stake.");
        require(plans[_planId].minimumTokensRequired <= _stakeAmount, "You are not allowed to stake below the minimum token amount required to participate in this reward plan.");

        IERC20(_tokenType).transferFrom(msg.sender, address(this), _stakeAmount);
        UserInfo storage user = userInfo[msg.sender][_planId];
        user._amount = _stakeAmount;
        user._tokenType = _tokenType;
        user._timestamp = now;
        

        emit StakeEvent(msg.sender, _planId, _stakeAmount);
    }

    function unstake(uint256 _planId) public {

        require(userInfo[msg.sender][_planId]._amount > 0, "You have not yet staked for this plan.");

        uint256 _rewardAmount = calculateReward(_planId, userInfo[msg.sender][_planId]._amount, userInfo[msg.sender][_planId]._timestamp);
        uint _stakedAmount = userInfo[msg.sender][_planId]._amount;

        //Scenario 1: transfer reward & stake in one transaction if both are the same token
        if(plans[_planId].sourceToken == sqrlAddress){
            //Transfer SQRL reward
            sqrl.transfer(msg.sender, _rewardAmount.add(_stakedAmount));
            //Update overall reward balance
            lastRewardBalance = lastRewardBalance.sub(_rewardAmount);
        }
        //Scenario 2: transfer reward & stake in 2 transactions if both are different tokens
        else{
            //Transfer SQRL reward
            sqrl.transfer(msg.sender, _rewardAmount);
            //Update overall reward balance
            lastRewardBalance = lastRewardBalance.sub(_rewardAmount);

            //Transfer original stake
            IERC20(plans[_planId].sourceToken).transfer(msg.sender, _stakedAmount); 
        }

        //Reset user's stake
        userInfo[msg.sender][_planId]._amount = 0;
        userInfo[msg.sender][_planId]._tokenType = 0x0000000000000000000000000000000000000000;        
        userInfo[msg.sender][_planId]._timestamp = 0;

        emit UnstakeEvent(msg.sender, _planId, _stakedAmount);
        emit UnstakeRewardEvent(msg.sender, _planId, _rewardAmount);

        emit OverallRewardBalanceEvent(lastRewardBalance);
    }

    function safeUnstakingTransfer(uint256 _intendedRewardAmount) internal view returns (uint256) {

        uint256 _sqrlBalance = lastRewardBalance;

        if(_intendedRewardAmount > _sqrlBalance){
            return _sqrlBalance;
        }else{
            return _intendedRewardAmount;
        }

    }

    function calculateReward(uint256 _planId, uint256 _stakeAmount, uint256 _stakedTime) internal view returns (uint256 ){

        // IF( (time.now - stakedTime) / theMultiplierCyclePeriod < theMultiplierMaxCycle, (time.now - stakedTime) / theMultiplierCyclePeriod, theMultiplierMaxCycle) * theTokenMultiplier / 100 * stakeAmount
        uint256 _TimeDifference = now.sub(_stakedTime);
        uint256 _FinalCycle;
        
        if(_TimeDifference.div(plans[_planId].multiplierCyclePeriod) < plans[_planId].multiplierMaxCycle){
            _FinalCycle = _TimeDifference.div(plans[_planId].multiplierCyclePeriod);
        }else{
            _FinalCycle = plans[_planId].multiplierMaxCycle;
        }

        // Compounding interest
        uint256 _intendedRewardAmount = _stakeAmount;

        for(uint i=0; i<_FinalCycle; i++){
            _intendedRewardAmount = _intendedRewardAmount.mul(plans[_planId].tokenMultiplier).div(plans[_planId].tokenMultiplierDivisor);
        }
        //Return only reward component without the stake
        _intendedRewardAmount = _intendedRewardAmount.sub(_stakeAmount);

        return safeUnstakingTransfer(_intendedRewardAmount);
    }

    function getUserReward(uint256 _planId) public view returns (uint256) {
        uint256 _rewardAmount = calculateReward(_planId, userInfo[msg.sender][_planId]._amount, userInfo[msg.sender][_planId]._timestamp);

        return _rewardAmount;
    }

    function getUserStake(uint256 _planId) public view returns (uint256) {

        return userInfo[msg.sender][_planId]._amount;
    }

    function getLastRewardBalance() public view returns (uint256) {

        return lastRewardBalance;
    }

    //
    // 3. Emergency withdrawal of SQRL should there be any issues with the contract code
    //
    //
    function emergencyWithdrawal() public onlyOwner {
        
        uint256 sqrlBalance = lastRewardBalance;

        sqrl.transfer(msg.sender, sqrlBalance);

        lastRewardBalance = 0;

        emit EmergencyWithdrawal(msg.sender, sqrlBalance);
        emit RewardBalanceNow(lastRewardBalance);
    }

    //
    // 4. Add to the reward balance
    //
    //
    function addToRewardBalance(uint256 _additionalRewardAmount) public onlyOwner {

        lastRewardBalance = lastRewardBalance + _additionalRewardAmount;

        emit RewardBalanceNow(lastRewardBalance);
    }
}
//Import access control
// Import base Initializable contract
// Import the IERC20 interface and and SafeMath library
function addPlan(address _sourceToken, uint256 _tokenMultiplier, uint256 _tokenMultiplierDivisor, uint256 _multiplierCyclePeriod, uint256 _multiplierMaxCycle, uint256 _minimumTokensRequired, bool _planEnabled) public onlyOwner returns(Plan[] memory) {
    Plan storage _newPlan = plans.push();
    _newPlan.sourceToken = _sourceToken;
    _newPlan.tokenMultiplier = _tokenMultiplier;
    _newPlan.tokenMultiplierDivisor = _tokenMultiplierDivisor;
    _newPlan.multiplierCyclePeriod = _multiplierCyclePeriod;
    _newPlan.multiplierMaxCycle = _multiplierMaxCycle;
    _newPlan.minimumTokensRequired = _minimumTokensRequired;
    _newPlan.planAuthor = msg.sender;
    _newPlan.planEnabled = _planEnabled;

    emit ChangeByAddPlan(msg.sender, plans.length - 1);
    return plans;
}

//
// 1. Plan creation: this section covers the creation of yield farming plans
// only the owner is allow to create and modify plans
//
//Add yield farming plans