contract MunchSAStaking is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    struct Pool {
        uint256 remainingBalance;   // Funds available for new stakers
        uint256 attributed;         // Funds taken from balance for stakers
        uint256 paidOut;            // Rewards claimed, therefore paidOut <= attributed
        uint256 minStake;
        uint256 maxStake;
        uint stakingDuration;
        uint minPercentToCharity;   // minimum % of rewards being given to charity - 50% is stored as 50
        uint apy;                   // integer percentage - 50% is stored as 50
    }

    struct UserInfo {
        uint256 amountDeposited;
        uint256 remainingRewards;   // Amount not claimed yet
        uint256 rewardsDonated;     // Out of claimed rewards, amount sent to charity
        uint stakingStartTime;      // Timestamp when user started staking
        uint lastRewardTime;        // Last time rewards were claimed
        uint256 rewardDebt;         // Rewards to ignore in redis computation based on when user joined pool
        uint percentToCharity;      // as an int: 50% is stored as 50
    }

    // Address of the ERC20 Token contract.
    IERC20 _munchToken;
    // Address where the sell of the tokens for charity will happen
    address _charityAddress;

    // Counting the accumulated rewards received by the contract so we can
    // redistribute them to users.
    uint256 _accRedisTokensPerShare;
    uint256 _lastRedisTotal;
    uint256 _stakedAndFunds; // Sum of all MUNCH tokens staked or funded for pools
    uint256 _staked; // Sum of all MUNCH tokens staked - used to get redis share of a user

    // All pools added to the contract.
    Pool[] public pools;

    // Info of each user that stakes tokens.
    mapping(uint => mapping(address => UserInfo)) public userInfo;

    event Deposit(uint poolIdx, address indexed user, uint256 amount);
    event Withdraw(uint poolIdx, address indexed user, uint256 amount);

    constructor(address munchToken) {
        _munchToken = IERC20(munchToken);
        _charityAddress = address(_munchToken);
    }

    function addPool(uint stakingDurationInDays, uint256 minStake, uint maxStake, uint apy) public onlyOwner {
        pools.push(Pool({
            remainingBalance: 0,
            attributed: 0,
            paidOut: 0,
            minStake: minStake,
            maxStake: maxStake,
            stakingDuration: stakingDurationInDays * 1 days,
            minPercentToCharity: 50,
            apy: apy
        }));
    }

    // Fund a pool to allow users to participate
    function fund(uint poolIdx, uint256 amount) public onlyOwner {
        Pool storage pool = pools[poolIdx];
        _munchToken.safeTransferFrom(address(msg.sender), address(this), amount);
        pool.remainingBalance = pool.remainingBalance.add(amount);
        _stakedAndFunds = _stakedAndFunds.add(amount);
    }

    // Allows to unlock a pool for any emergency reason. Only accumulated rewards can be redeemed.
    function unlockPool(uint poolIdx) public onlyOwner {
        pools[poolIdx].stakingDuration = 0;
    }

    function setMinPercentToCharity(uint poolIdx, uint minPercentToCharity) public onlyOwner {
        Pool storage pool = pools[poolIdx];
        pool.minPercentToCharity = minPercentToCharity;
    }

    function setCharityAddress(address addy) public onlyOwner {
        _charityAddress = addy;
    }

    // View function to see the expected total rewards for a given address.
    function totalRewards(uint poolIdx, address wallet) external view returns (uint256) {
        UserInfo storage user = userInfo[poolIdx][wallet];
        Pool storage pool = pools[poolIdx];
        return user.amountDeposited.mul(pool.apy).mul(pool.stakingDuration).div(365 days).div(100);
    }

    // View function to see how much can currently be redeemed.
    function pending(uint poolIdx, address wallet) public view returns (uint256) {
        UserInfo storage user = userInfo[poolIdx][wallet];
        Pool storage pool = pools[poolIdx];

        uint timeSinceLastReward = block.timestamp - user.lastRewardTime;
        uint timeFromLastRewardToEnd = user.stakingStartTime + pool.stakingDuration - user.lastRewardTime;
        uint256 pendingReward = user.remainingRewards.mul(timeSinceLastReward).div(timeFromLastRewardToEnd);
        return pendingReward > user.remainingRewards ? user.remainingRewards : pendingReward;
    }

    // View function to see how much redistribution token can currently be redeemed.
    function redisCount(uint poolIdx, address wallet) public view returns (uint256) {
        if (_accRedisTokensPerShare == 0 || _staked == 0) {
            return 0;
        }
        UserInfo storage user = userInfo[poolIdx][wallet];
        return user.amountDeposited.mul(_accRedisTokensPerShare).div(1e36).sub(user.rewardDebt);
    }

    // Update the count of redistribution tokens (coming from MUNCH tx tax)
    // To call BEFORE every change made to the contract balance.
    function updateRedisCount() internal {
        uint256 munchBal = _munchToken.balanceOf(address(this));
        if (munchBal == 0 || _staked == 0) {
            return;
        }

        // Whatever is not part of staked and funded tokens is redistribution.
        // We have not had changes to the total amount between the last call and now.
        uint256 totalRedis = munchBal.sub(_stakedAndFunds);
        uint256 toAccountFor = totalRedis.sub(_lastRedisTotal);
        _lastRedisTotal = totalRedis;

        _accRedisTokensPerShare = _accRedisTokensPerShare.add(toAccountFor.mul(1e36).div(_staked));
    }

    // Deposit tokens to start a staking period.
    // If some tokens are already there, rewards are returned and staking lock starts over with sum of deposits
    //
    // To change the percentage given out to charity, you need to call this function with amount = 0
    // This has the effect of reseting your lock time and withdrawing your rewards.
    function deposit(uint poolIdx, uint256 amount, uint percentToCharity) public {
        UserInfo storage user = userInfo[poolIdx][msg.sender];
        Pool storage pool = pools[poolIdx];
        require(percentToCharity >= pool.minPercentToCharity && percentToCharity <= 100, "Invalid percentage to give to charity");
        uint256 totalDeposit = user.amountDeposited.add(amount);
        require(pool.minStake <= totalDeposit && pool.maxStake >= totalDeposit, "Unauthorized amount");

        if (user.amountDeposited > 0) {
            transferMunchRewards(poolIdx); // this calls updateRedisCount()
        } else {
            updateRedisCount();
        }

        if (amount > 0) {
            uint256 newRewards = amount.mul(pool.apy).mul(pool.stakingDuration).div(365 days).div(100);
            require(pool.remainingBalance >= newRewards, "Pool is full");

            userInfo[poolIdx][msg.sender] = UserInfo({
                amountDeposited: totalDeposit,
                remainingRewards: user.remainingRewards.add(newRewards),
                rewardsDonated: user.rewardsDonated,
                lastRewardTime: block.timestamp,
                stakingStartTime: block.timestamp,
                percentToCharity: percentToCharity,
                rewardDebt: totalDeposit.mul(_accRedisTokensPerShare).div(1e36)
            });
            pool.remainingBalance = pool.remainingBalance.sub(newRewards);
            pool.attributed = pool.attributed.add(newRewards);

            _stakedAndFunds = _stakedAndFunds.add(amount);
            _staked = _staked.add(amount);

            _munchToken.safeTransferFrom(address(msg.sender), address(this), amount);

            emit Deposit(poolIdx, msg.sender, amount);
        } else {
            user.percentToCharity = percentToCharity;
        }
    }

    // Withdraw all staked tokens from a given pool.
    function withdraw(uint poolIdx) public {
        UserInfo storage user = userInfo[poolIdx][msg.sender];
        Pool storage pool = pools[poolIdx];

        require(block.timestamp - user.stakingStartTime > pool.stakingDuration, "Lock period not over");

        // Rewards
        transferMunchRewards(poolIdx); // this calls updateRedisCount()

        // Clean up
        _stakedAndFunds = _stakedAndFunds.sub(user.amountDeposited);
        _staked = _staked.sub(user.amountDeposited);
        user.remainingRewards = 0;
        uint256 amountToWithdraw = user.amountDeposited;
        user.amountDeposited = 0;

        // Withdraw
        _munchToken.safeTransfer(address(msg.sender), amountToWithdraw);
        emit Withdraw(poolIdx, msg.sender, amountToWithdraw);
    }

    // Called both internally and directly to claim rewards for a given wallet.
    // HAS TO call updateRedisCount()
    function transferMunchRewards(uint poolIdx) public {
        UserInfo storage user = userInfo[poolIdx][msg.sender];
        Pool storage pool = pools[poolIdx];
        uint256 pendingRewards = pending(poolIdx, msg.sender);

        updateRedisCount();

        if(pendingRewards > 0) {
            // account for redistribution tokens coming from tx tax on MUNCH token.
            uint256 redis = redisCount(poolIdx, msg.sender);
            uint256 pendingAmount = pendingRewards.add(redis);

            uint256 toCharity = pendingAmount.mul(user.percentToCharity).div(100);
            uint256 toHolder = pendingAmount.sub(toCharity);

            if (toCharity > 0) {
                // send share to charity
                _munchToken.transfer(_charityAddress, toCharity);
            }

            if (toHolder > 0) {
                // send share to holder
                _munchToken.transfer(msg.sender, toHolder);
            }

            // mark as paid out, redeemed, and write down how much was donated to charity
            _stakedAndFunds = _stakedAndFunds.sub(pendingRewards);
            _lastRedisTotal = _lastRedisTotal.sub(redis);
            pool.paidOut = pool.paidOut.add(pendingRewards);
            user.remainingRewards = user.remainingRewards.sub(pendingRewards);
            user.rewardsDonated = user.rewardsDonated.add(toCharity); // includes redis
            user.lastRewardTime = block.timestamp;
        }
    }

    // Withdraw Munch tokens from the funds deposited in a given pool
    // This functions does not allow owner to withdraw funds deposited by or attributed to holders,
    // only remaining funds.
    function fundWithdraw(uint poolIdx, uint256 amount) onlyOwner public {
        Pool storage pool = pools[poolIdx];
        require(pool.remainingBalance >= amount, "Cannot withdraw more than remaining pool balance");
        updateRedisCount();
        _munchToken.transfer(msg.sender, amount);
        _stakedAndFunds = _stakedAndFunds.sub(amount);
    }

    // Withdraw any ETH sent to the contract
    function ethWithdraw() onlyOwner public {
        uint256 balance = address(this).balance;
        require(balance > 0, "Balance is zero.");
        payable(msg.sender).transfer(balance);
    }
}
// Stake Munch tokens to earn more as rewards.
//
// 
//
function redisCount(uint poolIdx, address wallet) public view returns (uint256) {
    if (_accRedisTokensPerShare == 0 || _staked == 0) {
        return 0;
    }
    UserInfo storage user = userInfo[poolIdx][wallet];
    return user.amountDeposited.mul(_accRedisTokensPerShare).div(1e36).sub(user.rewardDebt);
}

// View function to see how much redistribution token can currently be redeemed.