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

    struct NftToken {
      bool hasValue;
      mapping(address => uint256) balances;
    }

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

    IERC20 public NDR;
    IERC1155 public NFT;

    uint256 public periodFinish = 0;
    uint256 public rewardRate = 0;
    uint256 public rewardsDuration = 182 days;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public _totalStrength;
    uint256 private _totalSupply;

    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public strengthWeight;
    mapping(address => uint256) public rewards;
    mapping(address => uint256) private _balances;

    uint256 public tokenMaxAmount;
    mapping(uint256 => bool) private supportedSeries;
    uint256[] public nftTokens;
    mapping(uint256 => NftToken) public nftTokenMap;

    /* ========== CONSTRUCTOR ========== */

    constructor(address _NFT, address _NDR) public {
        NDR = IERC20(_NDR);
        NFT = IERC1155(_NFT);
    }

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

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

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

    /**
     * @dev Total staked card by user
     */
    function balanceByTokenIdOf(uint256 tokenId, address account) external view returns (uint256) {
        return nftTokenMap[tokenId].balances[account];
    }

    /**
     * @dev Returns all NFT tokenids
     */
    function getNftTokens() external view returns (uint256[] memory) {
        return nftTokens;
    }


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

    function rewardPerToken() public view returns (uint256) {
        if (_totalStrength == 0) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored.add(
                lastTimeRewardApplicable()
                    .sub(lastUpdateTime)
                    .mul(rewardRate)
                    .mul(1e18)
                    .div(_totalStrength)
            );
    }

    /**
     * @dev Get claimable NDR amount
     */
    function earned(address account) public view returns (uint256) {
        return
            strengthWeight[account]
                .mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
                .div(1e18)
                .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 Sets supported series
     */
    function setSupportedSeries(uint256 _series, bool supported) public onlyOwner {
       supportedSeries[_series] = supported;
    }

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

    /**
     * @dev Stake ERC-1155
     */
    function stake(uint256[] calldata tokenIds, uint256[] calldata amounts) external nonReentrant whenNotPaused updateReward(msg.sender) {
        require(tokenIds.length == amounts.length, "TokenIds and amounts length should be the same");
        for(uint256 i = 0; i < tokenIds.length; i++) {
            stakeInternal(tokenIds[i], amounts[i]);
        }
    }

    /**
     * @dev Stake particular ERC-1155 tokenId
     */
    function stakeInternal(uint256 tokenId, uint256 amount) internal {
        (,,,,,uint256 series) = INodeRunnersNFT(address(NFT)).getFighter(tokenId);
        if(!nftTokenMap[tokenId].hasValue) {
            nftTokens.push(tokenId);
            nftTokenMap[tokenId] = NftToken({ hasValue: true });
        }
        require(supportedSeries[series] == true, "wrong id");
        require(nftTokenMap[tokenId].balances[msg.sender].add(amount) <= tokenMaxAmount, "NFT max reached");

        (uint256 strength,,,,,) = INodeRunnersNFT(address(NFT)).getFighter(tokenId);
        strength = strength.mul(amount);

        strengthWeight[msg.sender] = strengthWeight[msg.sender].add(strength);
        _totalStrength = _totalStrength.add(strength);
        _totalSupply = _totalSupply.add(amount);
        _balances[msg.sender] = _balances[msg.sender].add(amount);        
        nftTokenMap[tokenId].balances[msg.sender] = nftTokenMap[tokenId].balances[msg.sender].add(amount);

        NFT.safeTransferFrom(msg.sender, address(this), tokenId, amount, "0x0");
        emit Staked(msg.sender, tokenId, amount);
    }

    /**
     * @dev Withdraw ERC-1155
     */
    function withdrawNFT(uint256[] memory tokenIds, uint256[] memory amounts) public updateReward(msg.sender) {
        require(tokenIds.length == amounts.length, "TokenIds and amounts length should be the same");
        for(uint256 i = 0; i < tokenIds.length; i++) {
            withdrawNFTInternal(tokenIds[i], amounts[i]);
        }
    }

    /**
     * @dev Withdraw particular ERC-1155 tokenId
     */
    function withdrawNFTInternal(uint256 tokenId, uint256 amount) internal {
        (uint256 strength,,,,,) = INodeRunnersNFT(address(NFT)).getFighter(tokenId);
        strength = strength.mul(amount);

        strengthWeight[msg.sender] = strengthWeight[msg.sender].sub(strength);
        _totalStrength = _totalStrength.sub(strength);
        _totalSupply = _totalSupply.sub(amount);
        _balances[msg.sender] = _balances[msg.sender].sub(amount);
        nftTokenMap[tokenId].balances[msg.sender] = nftTokenMap[tokenId].balances[msg.sender].sub(amount);

        NFT.safeTransferFrom(address(this), msg.sender, tokenId, amount, "0x0");
        emit Withdrawn(msg.sender, tokenId, amount);
    }

    /**
     * @dev Withdraw all cards
     */
    function withdraw() public updateReward(msg.sender) {
        uint256[] memory tokenIds = new uint256[](nftTokens.length);
        uint256[] memory amounts = new uint256[](nftTokens.length);
        for (uint8 i = 0; i < nftTokens.length; i++) {
            uint256 tokenId = nftTokens[i];
            tokenIds[i] = tokenId;
            uint256 balance = nftTokenMap[tokenId].balances[msg.sender];
            amounts[i] = balance;
        }
        withdrawNFT(tokenIds, amounts);  
    }

    /**
     * @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();
        getReward();
    }

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

    function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure virtual returns (bytes4) {
        return this.onERC1155Received.selector;
    }

    function notifyRewardAmount(uint256 reward)
        external
        onlyOwner
        updateReward(address(0))
    {
        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(NFT) &&
                tokenAddress != address(NDR),
            "Cannot withdraw the staking or rewards tokens"
        );
        IERC20(tokenAddress).safeTransfer(this.owner(), tokenAmount);
        emit Recovered(tokenAddress, tokenAmount);
    }

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

    /* ========== 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 tokenId, uint256 amount);
    event Withdrawn(address indexed user, uint256 tokenId, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardsDurationUpdated(uint256 newDuration);
    event Recovered(address token, uint256 amount);
}
function changeAddresses(address _NDR, address _NFT) public onlyOwner {
    NDR = IERC20(_NDR);
    NFT = IERC1155(_NFT);
}

/**
 * @dev Change contract addresses of ERC-20, ERC-1155 tokens
 */