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

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

    IRewardsToken public rewardsToken;
    IERC20 public stakingToken;
    IERC721 public NTFToken;
    address payable public treasury;
    IUniswap public uniswap;
    IFactory public factory;
    IUniswapV2Pair public pair;
    address public NDR;

    uint256 public periodFinish = 0;
    uint256 public rewardRate = 0;
    uint256 public rewardsDuration = 360 days;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public interest = 10;
    uint256 public mul1;
    uint256 public mul2;
    uint256 public minLp;
    uint256 public maxLp;
    uint256 public deadline;

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

    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;

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

    constructor(
        address payable _treasury,
        address _rewardsToken,
        address _stakingToken,
        address _NTFToken,
        address _uniswap,
        address _factory,
        address _NDR
    ) public {
        treasury = _treasury;
        rewardsToken = IRewardsToken(_rewardsToken);
        stakingToken = IERC20(_stakingToken);
        NTFToken = IERC721(_NTFToken);
        uniswap = IUniswap(_uniswap);
        factory = IFactory(_factory);
        pair = IUniswapV2Pair(_stakingToken);
        NDR = _NDR;
    }

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

    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    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)
            );
    }

    function earned(address account) public view returns (uint256) {
        return
            _balances[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;
    }

    /* ========== UNISWAP FUNCTIONS ========== */

    receive() payable external {
        assert(msg.sender == uniswap.WETH());
    }

    function getReserves() internal view returns (uint, uint) {
        (uint reserves1, uint reserves2,) = pair.getReserves();
        return (reserves1, reserves2);
    }

    function getPrice() public view returns (uint) {
        //ETH pool amount / balance LP * 2
        (uint amount,,) = pair.getReserves();
        uint price = amount * 1e18 / pair.totalSupply() * 2;
        return price;
    }

    function getAmountsOut(uint amount, address token) internal view returns (uint) {
        uint[] memory amounts = uniswap.getAmountsOut(amount, getPathForTokenToEth(token));
        uint256 outputTokenCount = uint256(amounts[amounts.length - 1]);
        return outputTokenCount;
    }

    function getAmountsIn(uint amount, address token) internal view returns (uint) {
        uint[] memory amounts = uniswap.getAmountsIn(amount, getPathForTokenToEth(token));
        uint256 outputTokenCount = uint256(amounts[0]);
        return outputTokenCount;
    }

    function getPathForTokenToEth(address token) internal view returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = uniswap.WETH();
        path[1] = token;
        return path;
    }

    function swapExactETHForTokens(uint amountEth) internal {
        uint[] memory amounts = uniswap.swapExactETHForTokens.value(amountEth)(0, getPathForTokenToEth(address(NDR)), address(this), now + deadline);
    }

    function addLiquidityETH(uint amountTokenDesired, uint amountEth) internal {
        IERC20(NDR).approve(address(uniswap), amountTokenDesired);
        uniswap.addLiquidityETH.value(amountEth)(address(NDR), amountTokenDesired, amountTokenDesired * 9 / 10, amountEth * 9 / 10, address(0), now + deadline);
    }

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

    function changeAddresses(
        address payable _treasury,
        address _rewardsToken,
        address _stakingToken,
        address _NTFToken,
        address _uniswap,
        address _factory,
        address _NDR
    ) public onlyOwner {
        treasury = _treasury;
        rewardsToken = IRewardsToken(_rewardsToken);
        stakingToken = IERC20(_stakingToken);
        NTFToken = IERC721(_NTFToken);
        uniswap = IUniswap(_uniswap);
        factory = IFactory(_factory);
        pair = IUniswapV2Pair(_stakingToken);
        NDR = _NDR;
    }

    function changeMultiplicator(uint256 _mul1, uint256 _mul2) public onlyOwner returns(uint256, uint256) {
        mul1 = _mul1;
        mul2 = _mul2;
        return (mul1, mul2);
    }

    function changeDeadline(uint256 _deadline) public onlyOwner returns(uint256) {
        deadline = _deadline;
        return deadline;
    }

    function burnRewards(uint256 amount) public onlyOwner {
        rewardsToken.burn(amount);
    }

    function withdrawDust(uint256 amount) public onlyOwner {
        treasury.transfer(amount);
    }

    function changeInterest(uint256 newInterest) public onlyOwner returns(uint256) {
        interest = newInterest;
        return interest;
    }

    function changeLpAmount(uint256 _minLp, uint256 _maxLp) public onlyOwner returns(uint256, uint256) {
        minLp = _minLp;
        maxLp = _maxLp;
        return (minLp, maxLp);
    }

    function stake(uint256 amount)
        external
        nonReentrant
        whenNotPaused
        updateReward(msg.sender)
    {
        require(amount > 0, "Cannot stake 0");
        require(amount >= minLp && amount <= maxLp, "wrong amount");
        _totalSupply = _totalSupply.add(amount);
        _balances[msg.sender] = _balances[msg.sender].add(amount);
        stakingToken.safeTransferFrom(msg.sender, address(this), amount);
        emit Staked(msg.sender, amount);
    }

    function withdraw(uint256 amount)
        public
        nonReentrant
        updateReward(msg.sender)
    {
        require(amount > 0, "Cannot withdraw 0");
        _totalSupply = _totalSupply.sub(amount);
        _balances[msg.sender] = _balances[msg.sender].sub(amount);
        stakingToken.safeTransfer(msg.sender, amount);
        emit Withdrawn(msg.sender, amount);
    }

    function exit() external {
        withdraw(_balances[msg.sender]);
    }

    function redeem(uint256 tokenId)
        public
        nonReentrant
        whenNotPaused
        updateReward(msg.sender)
    {
        uint256 reward = rewards[msg.sender];
        uint256 amount = NFTFactoryInterface(address(NTFToken)).getHashPrice(tokenId); //nft price in HASH
        require(amount > 0, "can't buy in HASH");
        require(reward >= amount, "not enough HASH");
        rewards[msg.sender] = rewards[msg.sender].sub(amount);
        rewardsToken.burn(amount);
        NTFToken.safeTransferFrom(address(this), msg.sender, tokenId);
        emit RewardPaid(msg.sender, reward);
    }

    function buy(uint256 tokenId) public payable {
        uint256 amount = NFTFactoryInterface(address(NTFToken)).getRarity(tokenId);
        uint256 price = getPrice() * amount * mul1 / mul2;
        require(msg.value >= price, "wrong value");

        uint fee = msg.value / interest;
        uint amountEth = (msg.value - fee) / 2;
        uint amountToken = getAmountsOut(amountEth, address(NDR));
        treasury.transfer(fee);
        swapExactETHForTokens(amountEth);
        addLiquidityETH(amountToken, amountEth * 9 / 10);
        NTFToken.safeTransferFrom(address(this), msg.sender, tokenId);
    }

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

    function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) public returns (bytes4) {
        emit NFTReceived(operator, from, tokenId, data);
        return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
    }

    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 = rewardsToken.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(stakingToken) &&
                tokenAddress != address(rewardsToken),
            "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 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);
    event NFTReceived(address operator, address from, uint256 tokenId, bytes data);
}
function totalSupply() external view returns (uint256) {
    return _totalSupply;
}

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