contract Farmhalla is ReentrancyGuard {

    using SafeMath for uint256;
    using Address for address;
    using SafeERC20 for IERC20;

    event Staked(address indexed from, uint256 amountETH, uint256 amountLP);
    event Withdrawn(address indexed to, uint256 amountETH, uint256 amountLP);
    event Claimed(address indexed to, uint256 amount);
    event Halving(uint256 amount);
    event Received(address indexed from, uint256 amount);

    Valhalla public token;
    IUniswapV2Factory public factory;
    IUniswapV2Router02 public router;
    address public weth;
    address payable public treasury;
    address public pairAddress;

    struct AccountInfo {
        // Staked LP token balance
        uint256 balance;
        uint256 withdrawTimestamp;
        uint256 reward;
        uint256 rewardPerTokenPaid;
    }
    mapping(address => AccountInfo) public accountInfos;

    // Staked LP token total supply
    uint256 private _totalSupply = 0;

    uint256 public constant HALVING_DURATION = 8 days;
    uint256 public rewardAllocation = 4444 * 1e18;
    uint256 public halvingTimestamp = 0;
    uint256 public lastUpdateTimestamp = 0;

    uint256 public rewardRate = 0;
    uint256 public rewardPerTokenStored = 0;

    // Farming will be started manually by dev
    uint256 public farmingStartTimestamp = 0;
    bool public farmingStarted = false;

    constructor(address _routerAddress) public {
        token = new Valhalla(address(this));

        router = IUniswapV2Router02(_routerAddress);
        factory = IUniswapV2Factory(router.factory());
        weth = router.WETH();
        treasury = msg.sender;
        pairAddress = factory.createPair(address(token), weth);

        // Calc reward rate
        rewardRate = rewardAllocation.div(HALVING_DURATION);
    }

    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    function stake() external payable nonReentrant {
        _checkFarming();
        _updateReward(msg.sender);
        _halving();

        require(msg.value > 0, 'Cannot stake 0');
        require(!address(msg.sender).isContract(), 'Please use your individual account');

        // 10% allocated to treasury for marketing and development expenses
        uint256 fee = msg.value.div(10);
        uint256 amount = msg.value.sub(fee);
        treasury.transfer(fee);

        uint256 ethAmount = IERC20(weth).balanceOf(pairAddress);
        uint256 tokenAmount = IERC20(token).balanceOf(pairAddress);

        // If eth amount = 0 then set initial price to 1 eth = 8 val
        uint256 amountTokenDesired = ethAmount == 0 ? (amount * 8) : amount.mul(tokenAmount).div(ethAmount);
        // Mint borrowed val
        token.mint(address(this), amountTokenDesired);

        // Add liquidity in uniswap
        uint256 amountETHDesired = amount;
        IERC20(token).approve(address(router), amountTokenDesired);
        (,, uint256 liquidity) = router.addLiquidityETH{value : amountETHDesired}(address(token), amountTokenDesired, 1, 1, address(this), block.timestamp + 1 days);

        // Add LP token to total supply
        _totalSupply = _totalSupply.add(liquidity);

        // Add to balance
        accountInfos[msg.sender].balance = accountInfos[msg.sender].balance.add(liquidity);
        
        // Set stake timestamp as withdraw timestamp
        // to prevent withdraw immediately after first staking
        if (accountInfos[msg.sender].withdrawTimestamp == 0) {
            accountInfos[msg.sender].withdrawTimestamp = block.timestamp;
        }

        emit Staked(msg.sender, msg.value, liquidity);
    }

    function withdraw() external nonReentrant {
        _checkFarming();
        _updateReward(msg.sender);
        _halving();

        require(accountInfos[msg.sender].withdrawTimestamp + 8 days <= block.timestamp, 'You must wait 8 days since your last withdraw or stake');
        require(accountInfos[msg.sender].balance > 0, 'Cannot withdraw 0');

        // Limit withdraw LP token
        uint256 amount = accountInfos[msg.sender].balance;
        
        // Reduce total supply
        _totalSupply = _totalSupply.sub(amount);
        // Reduce balance
        accountInfos[msg.sender].balance = accountInfos[msg.sender].balance.sub(amount);
        // Set timestamp
        accountInfos[msg.sender].withdrawTimestamp = block.timestamp;

        // Remove liquidity in uniswap
        IERC20(pairAddress).approve(address(router), amount);
        (uint256 tokenAmount, uint256 ethAmount) = router.removeLiquidity(address(token), weth, amount, 0, 0, address(this), block.timestamp + 1 days);

        // Burn borrowed val
        token.burn(address(this), tokenAmount);
        // Withdraw ETH and send to sender
        IWETH(weth).withdraw(ethAmount);
        msg.sender.transfer(ethAmount);

        emit Withdrawn(msg.sender, ethAmount, amount);
    }

    function claim() external nonReentrant {
        _checkFarming();
        _updateReward(msg.sender);
        _halving();

        uint256 reward = accountInfos[msg.sender].reward;
        require(reward > 0, 'There is no reward to claim');

        if (reward > 0) {
            // Reduce first
            accountInfos[msg.sender].reward = 0;
            
            // Send reward
            token.mint(msg.sender, reward);
            
            emit Claimed(msg.sender, reward);
        }
    }

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

    function balanceOf(address account) external view returns (uint256) {
        return accountInfos[account].balance;
    }

    function rewardPerToken() public view returns (uint256) {
        if (_totalSupply == 0) {
            return rewardPerTokenStored;
        }

        return rewardPerTokenStored
        .add(
            lastRewardTimestamp()
            .sub(lastUpdateTimestamp)
            .mul(rewardRate)
            .mul(1e18)
            .div(_totalSupply)
        );
    }

    function lastRewardTimestamp() public view returns (uint256) {
        return Math.min(block.timestamp, halvingTimestamp);
    }

    function rewardEarned(address account) public view returns (uint256) {
        return accountInfos[account].balance.mul(
            rewardPerToken().sub(accountInfos[account].rewardPerTokenPaid)
        )
        .div(1e18)
        .add(accountInfos[account].reward);
    }

    // Token price in eth
    function tokenPrice() public view returns (uint256) {
        uint256 ethAmount = IERC20(weth).balanceOf(pairAddress);
        uint256 tokenAmount = IERC20(token).balanceOf(pairAddress);
        return tokenAmount > 0 ?
        // Current price
        ethAmount.mul(1e18).div(tokenAmount) :
        // Initial price
        (uint256(1e18).div(2));
    }

    function _updateReward(address account) internal {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTimestamp = lastRewardTimestamp();
        if (account != address(0)) {
            accountInfos[account].reward = rewardEarned(account);
            accountInfos[account].rewardPerTokenPaid = rewardPerTokenStored;
        }
    }

    // Do halving when timestamp reached
    function _halving() internal {
        if (block.timestamp >= halvingTimestamp) {
            rewardAllocation = rewardAllocation.div(2);

            rewardRate = rewardAllocation.div(HALVING_DURATION);
            halvingTimestamp = halvingTimestamp.add(HALVING_DURATION);

            _updateReward(msg.sender);
            emit Halving(rewardAllocation);
        }
    }

    // Check if farming is started
    function _checkFarming() internal {
        require(farmingStartTimestamp != 0, 'Please wait until farming started');
        if (!farmingStarted) {
            farmingStarted = true;
            halvingTimestamp = block.timestamp.add(HALVING_DURATION);
            lastUpdateTimestamp = block.timestamp;
        }
    }

    // Start farming
    // Only dev can call this function
    function start() public {
        require(msg.sender == treasury, 'Only dev can start farming');
        require(farmingStartTimestamp == 0, 'Farming has already started');

        farmingStartTimestamp = block.timestamp;
    }
}
function _halving() internal {
    if (block.timestamp >= halvingTimestamp) {
        rewardAllocation = rewardAllocation.div(2);

        rewardRate = rewardAllocation.div(HALVING_DURATION);
        halvingTimestamp = halvingTimestamp.add(HALVING_DURATION);

        _updateReward(msg.sender);
        emit Halving(rewardAllocation);
    }
}

// Do halving when timestamp reached