contract ERC20CustomStaking is ReentrancyGuard, Pausable, Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    /* ========== STATE VARIABLES ========== */

    IERC20 public NDR;
    IERC20 public token;
    INFTStaking public NFTStaking;

    uint256 public periodFinish = 0;
    uint256 public rewardRate = 0;
    uint256 public rewardsDuration;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 private _totalSupply;
    bool public nftRequired;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;
    mapping(address => uint256) public balances;

    uint256 public tokenMaxAmount;
    
    /* ========== CONSTRUCTOR ========== */

    constructor(address _token, address _NDR, address _NFTSTAKING) public {
        token = IERC20(_token);
        NDR = IERC20(_NDR);
        NFTStaking = INFTStaking(_NFTSTAKING);
    }

    /* ========== VIEWS ========== */

    /**
     * @dev Total staked cards in total
     */
    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev Total stake by user
     */
    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }

    function lastTimeRewardApplicable() public view returns (uint256) {
        return min(block.timestamp, periodFinish);
    }

    function rewardPerToken() public view returns (uint256) {
        if (_totalSupply == 0) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored.add(
                lastTimeRewardApplicable()
                    .sub(lastUpdateTime)
                    .mul(rewardRate)
                    .mul(10 ** 18)
                    .div(_totalSupply)
            );
    }

    /**
     * @dev Get claimable NDR amount
     */
    function earned(address account) public view returns (uint256) {
        uint8 rewardApplicable = 1;
        if(nftRequired) {
            rewardApplicable = NFTStaking.balanceOf(account) > 0 ? 1 : 0;
        }
        return balances[account].mul(rewardApplicable)
            .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
            .div(10 ** 18)
            .add(rewards[account]);
    }

    function getRewardForDuration() external view returns (uint256) {
        return rewardRate.mul(rewardsDuration);
    }

    function min(uint256 a, uint256 b) public pure returns (uint256) {
        return a < b ? a : b;
    }

    /* ========== MUTATIVE FUNCTIONS ========== */

    /**
     * @dev Sets max amount
     */
    function setTokenMaxAmount(uint256 _tokenMaxAmount) public onlyOwner {
       tokenMaxAmount = _tokenMaxAmount;
    }

    /**
     * @dev init supported ERC-20 
     */
    function setNftRequired(bool _nftRequired) public onlyOwner {
       nftRequired = _nftRequired;
    }

    /**
     * @dev Change contract addresses of ERC-20, ERC-1155 tokens
     */
    function changeAddresses(address _NDR, address _token) public onlyOwner {
        NDR = IERC20(_NDR);
        token = IERC20(_token);
    }

    /**
     * @dev Stake ERC-1155
     */
    function stake(uint256 amount) external nonReentrant whenNotPaused updateReward(msg.sender) {
        require(balances[msg.sender].add(amount) <= tokenMaxAmount, "ERC20 max");
        
        token.safeTransferFrom(msg.sender, address(this), amount);
        _totalSupply = _totalSupply.add(amount);
        balances[msg.sender] = balances[msg.sender].add(amount);

        emit Staked(msg.sender, amount);
    }

    /**
     * @dev Withdraw ERC-20
     */
    function withdraw(uint256 amount) public updateReward(msg.sender) {
        _totalSupply = _totalSupply.sub(amount);
        balances[msg.sender] = balances[msg.sender].sub(amount);
        token.safeTransfer(msg.sender, amount);

        emit Withdrawn(msg.sender, amount);
    }

    /**
     * @dev Get all rewards
     */
    function getReward() public nonReentrant updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            NDR.safeTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
    }

    /**
     * @dev Unstake all cards and get all rewards
     */
    function exit() external {
        withdraw(balances[msg.sender]);
        getReward();
    }

    /* ========== RESTRICTED FUNCTIONS ========== */

    function notifyRewardAmount(uint256 reward, uint256 duration)
        external
        onlyOwner
        updateReward(address(0))
    {
        rewardsDuration = duration;
        if (block.timestamp >= periodFinish) {
            rewardRate = reward.div(rewardsDuration);
        } else {
            uint256 remaining = periodFinish.sub(block.timestamp);
            uint256 leftover = remaining.mul(rewardRate);
            rewardRate = reward.add(leftover).div(rewardsDuration);
        }

        // Ensure the provided reward amount is not more than the balance in the contract.
        // This keeps the reward rate in the right range, preventing overflows due to
        // very high values of rewardRate in the earned and rewardsPerToken functions;
        // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow.
        uint256 balance = NDR.balanceOf(address(this));
        require(
            rewardRate <= balance.div(rewardsDuration),
            "Provided reward too high"
        );
        lastUpdateTime = block.timestamp;
        periodFinish = block.timestamp.add(rewardsDuration);
        emit RewardAdded(reward);
    }

    // Added to support recovering LP Rewards from other systems such as BAL to be distributed to holders
    function recoverERC20(address tokenAddress, uint256 tokenAmount)
        external
        onlyOwner
    {
        // Cannot recover the staking token or the rewards token
        require(tokenAddress != address(token) && tokenAddress != address(NDR), "Cannot withdraw the staking or rewards tokens");
        IERC20(tokenAddress).safeTransfer(this.owner(), tokenAmount);
        emit Recovered(tokenAddress, tokenAmount);
    }

    function setRewardsDuration(uint256 duration) external onlyOwner {
        require(
            block.timestamp > periodFinish,
            "Previous rewards period must be complete before changing the duration for the new period"
        );
        rewardsDuration = duration;
        emit RewardsDurationUpdated(duration);
    }

    /* ========== MODIFIERS ========== */

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    /* ========== EVENTS ========== */

    event RewardAdded(uint256 reward);
    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardsDurationUpdated(uint256 newDuration);
    event Recovered(address token, uint256 amount);
}
function getReward() public nonReentrant updateReward(msg.sender) {
    uint256 reward = rewards[msg.sender];
    if (reward > 0) {
        rewards[msg.sender] = 0;
        NDR.safeTransfer(msg.sender, reward);
        emit RewardPaid(msg.sender, reward);
    }
}

/**
 * @dev Get all rewards
 */