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

    struct NftToken {
      bool hasValue;
      bool active;
      uint256 maxAmount;
      uint256 strengthWeight;
      uint256 strengthDecimals;
      mapping(address => uint256) balances;
    }

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

    IERC20 public NDR;
    IERC1155 public NFT;
    INFTStaking public NFTStaking;

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

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

    uint256[] public nftTokens;
    mapping(uint256 => NftToken) public nftTokenMap;

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

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

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

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

    /**
     * @dev Returns custom NFT settings
     */
    function getNftTokenMap(uint256 tokenId) external view returns (bool, bool, uint256, uint256, uint256) {
        return (
            nftTokenMap[tokenId].hasValue,
            nftTokenMap[tokenId].active,
            nftTokenMap[tokenId].maxAmount,
            nftTokenMap[tokenId].strengthWeight,
            nftTokenMap[tokenId].strengthDecimals
        );
    }

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

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

    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) {
        uint8 rewardApplicable = 1;
        if(nftRequired) {
            rewardApplicable = NFTStaking.balanceOf(account) > 0 ? 1 : 0;
        }
        return
            strengthWeight[account].mul(rewardApplicable)
                .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 Pause contract
     */
    function pause() external onlyOwner {
        super._pause();
    }

    /**
     * @dev Unpause contract
     */
    function unpause() external onlyOwner {
        super._unpause();
    }

    /**
     * @dev Init supported Nft tokenId
     */
    function initNftToken(uint256 _tokenId, uint256 _maxAmount, uint256 _strengthWeight, uint256 _strengthDecimals) public onlyOwner {
        require(!nftTokenMap[_tokenId].hasValue, 'NFT token is already defined');
        nftTokens.push(_tokenId);
        nftTokenMap[_tokenId] = NftToken({ hasValue: true, active: true, maxAmount: _maxAmount, strengthWeight: _strengthWeight, strengthDecimals: _strengthDecimals });
    }
    
    /**
     * @dev Init supported Nft tokenId with batch
     */
    function initNftTokenBatch(uint256[] calldata _tokenIds, uint256[] calldata _maxAmounts, uint256[] calldata _strengthWeights, uint256[] calldata _strengthDecimals) public onlyOwner {
        require(_tokenIds.length == _maxAmounts.length && _tokenIds.length == _strengthWeights.length && _tokenIds.length == _strengthDecimals.length, 'Invalid params length');
        
        for(uint256 i = 0; i < _tokenIds.length; i++) {
            initNftToken(_tokenIds[i], _maxAmounts[i], _strengthWeights[i], _strengthDecimals[i]);
        }
    }

    /**
     * @dev Update supported Nft tokenId
     */
    function updateNftToken(uint256 _tokenId, uint256 _maxAmount, uint256 _strengthWeight, uint256 _strengthDecimals) public onlyOwner {
        require(nftTokenMap[_tokenId].hasValue, 'NFT token is not defined');
        nftTokenMap[_tokenId].maxAmount = _maxAmount;
        nftTokenMap[_tokenId].strengthWeight = _strengthWeight;
        nftTokenMap[_tokenId].strengthDecimals = _strengthDecimals;
    }

    /**
     * @dev Activates supported Nft tokenId
     */
    function activateNftToken(uint256 _tokenId) public onlyOwner {
        nftTokenMap[_tokenId].active = true;
    }

    /**
     * @dev Deactivates supported Nft tokenId
     */
    function deactivateNftToken(uint256 _tokenId) public onlyOwner {
        nftTokenMap[_tokenId].active = false;
    }

    /**
     * @dev Sets main nft stake requirement
     */
    function setNftRequired(bool _nftRequired) public onlyOwner {
       nftRequired = _nftRequired;
    }

    /**
     * @dev Sets withdraw reward fee
     */
    function setWithdrawRewardFee(uint256 _withdrawRewardFee) external onlyOwner {
       fee = _withdrawRewardFee;
    }

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

    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 ERC-1155
     */
    function stakeInternal(uint256 tokenId, uint256 amount) internal {
        require(nftTokenMap[tokenId].active, "wrong id");
        require(nftTokenMap[tokenId].balances[msg.sender].add(amount) <= nftTokenMap[tokenId].maxAmount, "NFT max");
        
        uint256 strength = nftTokenMap[tokenId].strengthWeight.div(nftTokenMap[tokenId].strengthDecimals).mul(amount);
        strengthWeight[msg.sender] = strengthWeight[msg.sender].add(strength);
        _totalStrength = _totalStrength.add(strength);

        _totalSupply = _totalSupply.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);
    }

    /**
     * @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 = nftTokenMap[tokenId].strengthWeight.div(nftTokenMap[tokenId].strengthDecimals).mul(amount);
        strengthWeight[msg.sender] = strengthWeight[msg.sender].sub(strength);
        _totalStrength = _totalStrength.sub(strength);
        
        _totalSupply = _totalSupply.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);
    }

    /**
     * @dev Withdraw all ERC-1155 tokens
     */
    function withdraw() public updateReward(msg.sender) {
        uint8 length = 0;
        for (uint8 i = 0; i < nftTokens.length; i++) {
            uint256 balance = nftTokenMap[nftTokens[i]].balances[msg.sender];
            if(balance > 0) {
                length++;
            }
        }

        uint256[] memory tokenIds = new uint256[](length);
        uint256[] memory amounts = new uint256[](length);
        uint8 j = 0;
        for (uint8 i = 0; i < nftTokens.length; i++) {
            uint256 tokenId = nftTokens[i];
            uint256 balance = nftTokenMap[tokenId].balances[msg.sender];
            if(balance > 0) {
                tokenIds[j] = tokenId;
                amounts[j] = balance;
                j++;
            }
        }

        withdrawNFT(tokenIds, amounts);  
    }

    /**
     * @dev Get all rewards
     */
    function getReward() public payable nonReentrant updateReward(msg.sender) {
        require (msg.value == fee, "Get reward fee required");
        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 payable {
        require (msg.value == fee, "Get reward fee required");
        withdraw();
        getReward();
    }

    /**
     * @dev Withdraw reward fee
     */
    function withdrawRewardFee() external onlyOwner {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer failed");
    }

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

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

    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(NFT) &&
                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 tokenId);
    event Withdrawn(address indexed user, uint256 tokenId);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardsDurationUpdated(uint256 newDuration);
    event Recovered(address token, uint256 amount);

    receive() external payable {
    }
}
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 ERC-1155
 */