contract PopsicleJointStaking is LPTokenWrapper, Ownable {
    using SafeERC20 for IERC20;
    // Immutable Address of Sushi token
    IERC20 public immutable sushi;
    // Immutable masterchef contract address
    IMasterChef public immutable masterChef;
    uint256 public immutable pid; // sushi pool id

// Reward rate - This is done to set ICE reward rate proportion. 
    uint256 public rewardRate = 2000000;
// Custom divisioner that is implemented in order to give the ability to alter rate reward according to the project needs
    uint256 public constant DIVISIONER = 10 ** 6;

// Set of variables that is storing user Sushi rewards
    uint256 public sushiPerTokenStored;
    // Info of each user.
    struct UserInfo {
        uint256 remainingIceTokenReward; // Remaining Token amount that is owned to the user.
        uint256 sushiPerTokenPaid;
        uint256 sushiRewards;
    }
    
    // Info of each user that stakes ICE tokens.
    mapping(address => UserInfo) public userInfo;
    //mapping(address => uint256) public sushiPerTokenPaid;
    //mapping(address => uint256) public sushiRewards;

    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event SushiPaid(address indexed user, uint256 reward);

    constructor(
        address _ice,
        address _sushi,
        address _lpToken,
        address _masterChef,
        uint256 _pid
    )
        LPTokenWrapper(_ice, _lpToken)
    {
        require(
           _sushi != address(0) && _masterChef != address(0),
           "NULL_ADDRESSES"
        );
        sushi = IERC20(_sushi);
        masterChef = IMasterChef(_masterChef);
        pid = _pid;
    }
// Function which tracks rewards of a user and harvests all sushi rewards from Masterchef
    function _updateReward(address account) override internal {
        UserInfo storage user = userInfo[msg.sender];
        uint _then = sushi.balanceOf(address(this));
        masterChef.withdraw(pid, 0); // harvests sushi
        sushiPerTokenStored = _sushiPerToken(sushi.balanceOf(address(this)) - _then);

        if (account != address(0)) {
            user.sushiRewards = _sushiEarned(account, sushiPerTokenStored);
            user.sushiPerTokenPaid = sushiPerTokenStored;
        }
    }

// View function which shows sushi rewards amount of our Pool 
    function sushiPerToken() public view returns (uint256) {
        return _sushiPerToken(masterChef.pendingSushi(pid, address(this)));
    }
// Calculates how much sushi is provied per LP token 
    function _sushiPerToken(uint earned_) internal view returns (uint256) {
        uint _totalSupply = totalSupply();
        if (_totalSupply > 0) {
            return sushiPerTokenStored + earned_ * 1e18 / _totalSupply;
        }
        return sushiPerTokenStored;
    }
// View function which shows user ICE reward for displayment on frontend
    function earned(address account) public view returns (uint256) {
        UserInfo memory user = userInfo[account];
        return _sushiEarned(account, sushiPerToken()) * rewardRate / DIVISIONER + user.remainingIceTokenReward;
    }
// View function which shows user Sushi reward for displayment on frontend
    function sushiEarned(address account) public view returns (uint256) {
        return _sushiEarned(account, sushiPerToken());
    }
// Calculates how much sushi is entitled for a particular user
    function _sushiEarned(address account, uint256 sushiPerToken_) internal view returns (uint256) {
        UserInfo memory user = userInfo[account];
        return
            balanceOf(account) * (sushiPerToken_ - user.sushiPerTokenPaid) / 1e18 + user.sushiRewards;
    }

    // stake visibility is public as overriding LPTokenWrapper's stake() function
    //Recieves users LP tokens and deposits them to Masterchef contract
    function stake(uint256 amount) override public updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        super.stake(amount);
        lpToken.approve(address(masterChef), amount);
        masterChef.deposit(pid, amount);
        emit Staked(msg.sender, amount);
    }
// Recieves Lp tokens from Masterchef and give it out to the user
    function withdraw(uint256 amount) override public updateReward(msg.sender) {
        require(amount > 0, "Cannot withdraw 0");
        masterChef.withdraw(pid, amount); // harvests sushi
        super.withdraw(amount);
        emit Withdrawn(msg.sender, amount);
    }

    // "Go home" function which withdraws all Funds and distributes all rewards to the user
    function exit() external {
        require(msg.sender != address(0));
        
        UserInfo storage user = userInfo[msg.sender];
        uint _then = sushi.balanceOf(address(this));
        uint256 amount = balanceOf(msg.sender);
        require(amount > 0, "Cannot withdraw 0");
        
        masterChef.withdraw(pid, amount); // harvests sushi
        sushiPerTokenStored = _sushiPerToken(sushi.balanceOf(address(this)) - _then);
        
        user.sushiRewards = _sushiEarned(msg.sender, sushiPerTokenStored);
        user.sushiPerTokenPaid = sushiPerTokenStored;
        
        super.withdraw(amount);
        emit Withdrawn(msg.sender, amount);
        
        uint256 reward = user.sushiRewards;
        if (reward > 0) {
            user.sushiRewards = 0;
            sushi.safeTransfer(msg.sender, reward);
            emit SushiPaid(msg.sender, reward);
        }
        reward = reward * rewardRate / DIVISIONER + user.remainingIceTokenReward;
        if (reward > 0)
        {
            user.remainingIceTokenReward = safeRewardTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
        
    }
    // Changes rewards rate of ICE token
    function setRewardRate(uint256 _rewardRate) external onlyOwner {
        rewardRate = _rewardRate;
    }
// Harvests rewards to the user but leaves the Lp tokens deposited
    function getReward() public updateReward(msg.sender) {
        UserInfo storage user = userInfo[msg.sender];
        uint256 reward = user.sushiRewards;
        if (reward > 0) {
            user.sushiRewards = 0;
            sushi.safeTransfer(msg.sender, reward);
            emit SushiPaid(msg.sender, reward);
        }
        reward = reward * rewardRate / DIVISIONER + user.remainingIceTokenReward;
        if (reward > 0)
        {
            user.remainingIceTokenReward = safeRewardTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
    }
    
    // Safe token distribution
    function safeRewardTransfer(address _to, uint256 _amount) internal returns(uint256) {
        uint256 rewardTokenBalance = ice.balanceOf(address(this));
        if (rewardTokenBalance == 0) { //save some gas fee
            return _amount;
        }
        if (_amount > rewardTokenBalance) { //save some gas fee
            ice.transfer(_to, rewardTokenBalance);
            return _amount - rewardTokenBalance;
        }
        ice.transfer(_to, _amount);
        return 0;
    }
}
/**
 * This contract is responsible fpr forwarding LP tokens to Masterchef contract.
 * It calculates ICE rewards and distrubutes both ICE and Sushi
 */
function getReward() public updateReward(msg.sender) {
        UserInfo storage user = userInfo[msg.sender];
        uint256 reward = user.sushiRewards;
        if (reward > 0) {
            user.sushiRewards = 0;
            sushi.safeTransfer(msg.sender, reward);
            emit SushiPaid(msg.sender, reward);
        }
        reward = reward * rewardRate / DIVISIONER + user.remainingIceTokenReward;
        if (reward > 0)
        {
            user.remainingIceTokenReward = safeRewardTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
    }
// Harvests rewards to the user but leaves the Lp tokens deposited