contract UniverseFarm is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using SafeToken for address;

    // Info of each user.
    struct UserInfo {
        uint256 amount;     // How many tokens the user has provided.
        mapping(uint256 => uint256) accReward;  // Accumulated reward since position opened
        mapping(uint256 => uint256) startIndex; // Start ETH index
    }

    address public merkleDistributor;
    IERC20 public jTesta;
    IERC20 public testa;
    IPlanetFarmConfig public config;
    uint112 public startLiquidity;
    uint112 public currentLiquidity;
    uint256 public startBlock;
    uint256 public initStartBlock;
    int public progressive = 0;
    uint256[] public accETHReward;
    uint256 public activationCount;
    uint256 public totalStake;
    uint256 public prevRoundETHReward;
    uint256 public round;
    uint256 public profit;
    
    // Info of each user that stakes LP tokens.
    mapping (address => UserInfo) public userInfo; 
   
    event Deposit(address indexed user, uint256 amount);
    event Withdraw(address indexed user, uint256 amount);
    event Harvest(address indexed user, uint256 amount);
    event HarvestAndWithdraw(address indexed user, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 amount);

    constructor(
        uint256 _profit,
        uint256 _startBlock,
        IERC20 _testa,
        IERC20 _jTesta,
        IPlanetFarmConfig _config,
        address _merkleDistributor
    ) public {
        profit = _profit;
        startBlock = _startBlock;
        initStartBlock = _startBlock;
        testa = _testa;
        jTesta = _jTesta;
        config = _config;
        startLiquidity = _config.getLiquidity();
        accETHReward.push(0);
        merkleDistributor = _merkleDistributor;
    }

    /// @dev Require that the caller must be an EOA account to avoid flash loans.
    modifier onlyEOA() {
        require(msg.sender == tx.origin, "Not EOA");
        _;
    }
    
    receive() external payable {}

    function setConfig(IPlanetFarmConfig _config) public onlyOwner {
        config = _config;
    }

    function setMerkleDistributor(address _merkleDistributor) public onlyOwner {
        merkleDistributor = _merkleDistributor;
    }

    function setProfit(uint256 _profit) public onlyOwner {
        profit = _profit;
    }
    
    function setToken(IERC20 _testa, IERC20 _jTesta) public onlyOwner {
        testa = _testa;
        jTesta = _jTesta;
    }

    function harvestAndWithdraw(uint256 _amount) public nonReentrant {
        UserInfo storage user = userInfo[msg.sender];
        uint256 jTestaSupply = jTesta.balanceOf(address(this));
        ( , int maxProgressive) = config.getProgressive();
        require(getBlockPass() <= config.getActivateAtBlock());
        require((progressive == maxProgressive) && (jTestaSupply != 0), "Must have jTestaSupply and reach maxProgressive to harvest");
        require(user.amount >= _amount, "No JTesta cannot withdraw");
        
        uint256 rewardAmount = getUserReward(msg.sender);
        uint256 userProfit = rewardAmount.mul(profit).div(10000);
        uint256 _harvestFee = config.getTestaFee(userProfit);

        require(testa.balanceOf(msg.sender) > _harvestFee, "Must have enough testa before harvest");
        testa.safeTransferFrom(msg.sender, config.getCompany(), _harvestFee);
        removeReward(msg.sender, rewardAmount);
        SafeToken.safeTransferETH(merkleDistributor, rewardAmount.sub(userProfit));
        SafeToken.safeTransferETH(msg.sender, userProfit);
        if(_amount > 0) {
            user.amount = user.amount.sub(_amount);
            jTesta.safeTransfer(msg.sender, _amount);
            totalStake = totalStake.sub(_amount);
        }
        emit HarvestAndWithdraw(msg.sender, _amount);
    }

    function harvest() public nonReentrant {
        UserInfo storage user = userInfo[msg.sender];
        uint256 jTestaSupply = jTesta.balanceOf(address(this));
        ( , int maxProgressive) = config.getProgressive();
        require(getBlockPass() <= config.getActivateAtBlock());
        require((progressive == maxProgressive) && (jTestaSupply != 0), "Must have jTestaSupply and reach maxProgressive to harvest");
        require(user.amount > 0, "No JTesta cannot withdraw");
        
        uint256 rewardAmount = getUserReward(msg.sender);
        uint256 userProfit = rewardAmount.mul(profit).div(10000);
        uint256 _harvestFee = config.getTestaFee(userProfit);
        
        require(testa.balanceOf(msg.sender) > _harvestFee, "Must have enough testa before harvest");
        testa.safeTransferFrom(msg.sender, config.getCompany(), _harvestFee);
        removeReward(msg.sender, rewardAmount);
        SafeToken.safeTransferETH(merkleDistributor, rewardAmount.sub(userProfit));
        SafeToken.safeTransferETH(msg.sender, userProfit);
        emit Harvest(msg.sender, rewardAmount);
    }
    
    function firstActivate() public onlyEOA nonReentrant {
        require(jTesta.balanceOf(msg.sender) >= config.getJTestaAmount(), "Insufficient jTesta amount");
        require(initStartBlock == startBlock);
        require(block.number >= initStartBlock, "Cannot activate until the specific block time arrive");

        currentLiquidity = config.getLiquidity();
        startBlock = block.number;
        startLiquidity = currentLiquidity;   
        // send Testa to user who press activate button
        safeTestaTransfer(msg.sender, config.getTestaReward());
    }

    function activate() public onlyEOA nonReentrant {
        require(jTesta.balanceOf(msg.sender) >= config.getJTestaAmount(), "Insufficient jTesta amount");
        require(startBlock != initStartBlock && startBlock > initStartBlock);
        require(getBlockPass() >= config.getActivateAtBlock(), "Cannot activate until specific amount of blocks pass");
        (int minProgressive, int maxProgressive) = config.getProgressive();
        currentLiquidity = config.getLiquidity();
        accETHReward.push(accETHReward[activationCount] + (address(this).balance).sub(prevRoundETHReward).mul(1e12).div(totalStake));
        prevRoundETHReward = address(this).balance;
        activationCount++;
        uint256 requiredLiquidity = config.getRequiredLiquidity(startLiquidity);
        
        if(currentLiquidity > requiredLiquidity){
            progressive++;
        }else{
            progressive--;
        }

        if(progressive <= minProgressive){
            progressive = minProgressive;
            uint256 totalReward = address(this).balance;
            resetGlobalReward();
            SafeToken.safeTransferETH(config.getCompany(), totalReward);
        }else if(progressive >= maxProgressive){
            progressive = maxProgressive;
        }
        startBlock = block.number;  
        startLiquidity = currentLiquidity;

        // send Testa to user who press activate button
        safeTestaTransfer(msg.sender, config.getTestaReward());
    }

	function updateUserReward(address userAddress) internal {
		UserInfo storage user = userInfo[userAddress];
		if (user.startIndex[round] != activationCount) {
			uint256 userReward = getUserReward(userAddress);
			user.accReward[round] = userReward;
			user.startIndex[round] = activationCount;
		}
	}
	
	function removeReward(address userAddress,uint256 rewardAmount) internal {
	    UserInfo storage user = userInfo[userAddress];
	    user.accReward[round] = 0;
	    user.startIndex[round] = activationCount;
        prevRoundETHReward = prevRoundETHReward.sub(rewardAmount);
	}
	
	function resetGlobalReward() internal {
	    delete accETHReward;
        accETHReward.push(0);
        activationCount = 0;
        prevRoundETHReward = 0;
        round++;
	}

	function getUserReward(address userAddress) public view returns(uint256) {
        UserInfo storage user = userInfo[userAddress];
        uint256 prevReward = accETHReward[activationCount] - accETHReward[user.startIndex[round]];
        uint256 total = user.accReward[round].add(user.amount.mul(prevReward).div(1e12));
		return total;
    }

    function getUserAccReward(address userAddress) public view  returns(uint256) {
        UserInfo storage user = userInfo[userAddress];
        return user.accReward[round];
    }

    function getUserStartIndex(address userAddress) public view  returns(uint256) {
        UserInfo storage user = userInfo[userAddress];
        return user.startIndex[round];
    }

    function getTestaPoolBalance() public view returns (uint256){
        return testa.balanceOf(address(this));
    }
    
    function getProgressive() public view returns (int){
        return progressive;
    }

    function getLatestBlock() public view returns (uint256) {
        return block.number;
    }
    
    function getBlockPass() public view returns (uint256){
        return block.number.sub(startBlock);
    }

    function getStartedBlock() public view returns (uint256) {
        return startBlock;
    }
    
    function totalETH() public view returns (uint256) {
        return address(this).balance;
    }
    
    // Deposit LP tokens to TestaFarm for Testa allocation.
    function deposit(uint256 amount) public {
        require(amount > 0 , "invalid amount");
        UserInfo storage user = userInfo[msg.sender];
		updateUserReward(msg.sender);
        jTesta.safeTransferFrom(msg.sender, address(this), amount);
        user.amount = user.amount.add(amount);
		
		totalStake = totalStake.add(amount);
        emit Deposit(msg.sender, amount);
    }

    // Withdraw LP tokens from TestaFarm.
    function withdraw(uint256 _amount) public {
        UserInfo storage user = userInfo[msg.sender];
        require(user.amount >= _amount, "Not enought token to withdraw");
        if(_amount > 0) {
			uint256 rewardAmount = getUserReward(msg.sender);
			removeReward(msg.sender, rewardAmount);
            SafeToken.safeTransferETH(config.getCompany(), rewardAmount);
            user.amount = user.amount.sub(_amount);
            jTesta.safeTransfer(msg.sender, _amount);
            totalStake = totalStake.sub(_amount);
        }
        emit Withdraw(msg.sender, _amount);
    }
    
    function emergencyWithdraw(uint256 amount) public onlyOwner{
        uint256 totalETHBal = address(this).balance;
        if(amount > totalETHBal){
            SafeToken.safeTransferETH(msg.sender, totalETHBal);
        }else{
            SafeToken.safeTransferETH(msg.sender, amount);
        }
    }

    // Safe testa transfer function, just in case if rounding error causes pool to not have enough Testa.
    function safeTestaTransfer(address _to, uint256 _amount) internal {
        uint256 testaBal = testa.balanceOf(address(this));
        if (_amount > testaBal) {
            testa.safeTransfer(_to, testaBal);
        } else {
            testa.safeTransfer(_to, _amount);
        }
    } 
}
function safeTestaTransfer(address _to, uint256 _amount) internal {
    uint256 testaBal = testa.balanceOf(address(this));
    if (_amount > testaBal) {
        testa.safeTransfer(_to, testaBal);
    } else {
        testa.safeTransfer(_to, _amount);
    }
} 

// Safe testa transfer function, just in case if rounding error causes pool to not have enough Testa.