contract Controller is Ownable, ReentrancyGuard {
    using SafeMath  for uint;
    using SafeERC20 for IERC20;
    
    struct poolInfo {
        uint    pid;
        string  name;
        address coin;
        address pool;
    }  
    
    struct groupInfo {
        uint    gid;
        uint    pid;
        address agent;
        uint    duration;
        uint    minAmount;
        uint    totalAmount;        
        uint    members;
        uint    lpAmount;
        uint    harvestAmount;
        uint    startTime;
        uint8   status;
        uint    txFee;
    }

    poolInfo[]  public pools;
    groupInfo[] public groups;

    uint public agentFee = 300;
    uint public governanceFee = 200;
    uint public gasRate = 120;
    uint public protectTime = 43200; // 12 hours

    address public governance;
    address public uniswap = address(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
    
    mapping (uint => mapping (address => uint)) userInvest; 
    mapping (address => uint[])                 userGroup;
    mapping (address => uint[])                 agentGroup;

    event SetPool(uint indexed _pid, address indexed _poolAddress);
    event CreateGroup(uint indexed _gid, uint indexed _pid, uint _amount, uint _duration, address indexed _agent);
    event Start(uint indexed _gid, address indexed _starter);
    event Stop(uint indexed _gid, address indexed _stoper);
    event Join(uint indexed _gid, address indexed _user, uint _amount);
    event Quit(uint indexed _gid, address indexed _user, uint _amount);
    
    constructor () public {
        governance = msg.sender;
    }

    modifier onlyOwnerOrGovernance {
        require(owner() == _msgSender() || governance == _msgSender(), "caller is not the owner or governance");
        _;
    }

    /* ========== Setting system parameters by admin ========== */
    
    function setGovernance(address _governance) public onlyOwner {
        require(_governance != address(0), "Controller: invalid governance address");
        governance = _governance;
    }
    
    function setFee(uint _agentFee, uint _governanceFee) public onlyOwnerOrGovernance {
        require(_agentFee.add(_governanceFee) <= 1000, "Controller: total fee cannot exceed 1000(10%)");
        agentFee = _agentFee;
        governanceFee = _governanceFee;
    }

    function setGasRate(uint _rate) public onlyOwnerOrGovernance {
        require(_rate >= 100 && _rate <= 200, "Controller: gas rate cannot exceed 200(2x)");
        gasRate = _rate;
    }

    function setProtectTime(uint _time) public onlyOwnerOrGovernance {
        require(_time >= 0, "Controller: protect time should be greater than 0");
        protectTime = _time;
    } 

    function setUniswap(address _uniswap) public onlyOwnerOrGovernance {
        require(_uniswap != address(0), "Controller: uniswap address should not be equal to 0");
        uniswap = _uniswap;
    }
    
    /* ========== Setting mining pool by admin ========== */

    function addPool(address _pool) public onlyOwnerOrGovernance {
        require(_pool != address(0), "Controller: pool address should not be equal to 0");
        
        uint id = pools.length;
        (string memory name, address coin) = _getPoolInfo(_pool);
        
        pools.push(poolInfo(id, name, coin, _pool));

        emit SetPool(id, _pool);
        
    }
    
    function setPool(uint _pid, address _pool) public onlyOwnerOrGovernance {
        require(_pid >= 0 && _pid < pools.length, "Controller: invalid pool id");
        require(_pool != address(0), "Controller: uniswap address should not be equal to 0");
        
        (string memory name, address coin) = _getPoolInfo(_pool);
        
        pools[_pid] = poolInfo(_pid, name,  coin, _pool);

        emit SetPool(_pid, _pool);
    }

    function _getPoolInfo(address _pool) internal view returns (string memory name, address coin) {
        name = Pool(_pool).poolName();
        coin = Pool(_pool).poolCoin();
    }

    /* ========== Manage group by agent ========== */

    function createGroup(uint _pid, uint _amount, uint _duration) public nonReentrant returns (uint _gid) {
        require(_pid >= 0 && _pid < pools.length, "Controller: invalid pool id");
        require(_amount > 0, "Controller: amount should be greater than 0");
        require(_duration >= 0, "Controller: duration should be greater than 0!");
        
        _gid = groups.length;
        groups.push(groupInfo(_gid, _pid, _msgSender(), _duration, _amount, 0, 0, 0, 0, 0, 0, 0));
        
        agentGroup[_msgSender()].push(_gid);

        emit CreateGroup(_gid, _pid, _amount, _duration, _msgSender());
    }

    // This function could be execute by anyone with bearing the gas fee
    function start(uint _gid) public nonReentrant { 
        uint gasleftBeforeStart = gasleft();       
        
        require(_gid >= 0 && _gid < groups.length, "Controller: invalid group id");
        require(groups[_gid].totalAmount >= groups[_gid].minAmount, "Controller: the funds didn't meet the minimum requirements");
         
        IERC20(pools[groups[_gid].pid].coin).safeApprove(pools[groups[_gid].pid].pool, 0);
        IERC20(pools[groups[_gid].pid].coin).safeApprove(pools[groups[_gid].pid].pool, groups[_gid].totalAmount);
        Pool(pools[groups[_gid].pid].pool).deposit(groups[_gid].totalAmount);
         
        groups[_gid].startTime = block.timestamp;
        groups[_gid].lpAmount = IERC20(pools[groups[_gid].pid].pool).balanceOf(address(this));
        groups[_gid].status = 1;

        groups[_gid].txFee = gasleftBeforeStart.sub(gasleft()).mul(tx.gasprice);

        emit Start(_gid, _msgSender());
    }

    // This function could be execute by anyone with bearing the gas fee
    // People who execute this function could get gas subsidies of start and stop in invest coin
    function stop(uint _gid) public nonReentrant {
        uint gasleftBeforeStart = gasleft();

        require(_gid >= 0 && _gid < groups.length, "Controller: invalid group id");
        require(block.timestamp >= groups[_gid].startTime.add(groups[_gid].duration), "Controller: the time has not reached the minimum deadline");
        if (block.timestamp < groups[_gid].startTime.add(groups[_gid].duration).add(protectTime)) {
            require(_msgSender() == groups[_gid].agent, "Controller: This method can only be called by the agent during protection time");
        }
        
        uint coinBefore = IERC20(pools[groups[_gid].pid].coin).balanceOf(address(this));
        Pool(pools[groups[_gid].pid].pool).withdraw(groups[_gid].lpAmount);
        
        uint coinHarvest = IERC20(pools[groups[_gid].pid].coin).balanceOf(address(this)).sub(coinBefore);
        
        uint agentShare = coinHarvest.sub(groups[_gid].totalAmount).mul(agentFee).div(10000);
        uint governanceShare = coinHarvest.sub(groups[_gid].totalAmount).mul(governanceFee).div(10000);
        IERC20(pools[groups[_gid].pid].coin).safeTransfer(groups[_gid].agent, agentShare);
        IERC20(pools[groups[_gid].pid].coin).safeTransfer(governance, governanceShare);
        
        groups[_gid].txFee = groups[_gid].txFee.add(gasleftBeforeStart.sub(gasleft()).mul(tx.gasprice));
        uint senderCoin = calculateFee(groups[_gid].txFee, pools[groups[_gid].pid].coin).mul(gasRate).div(100);
        IERC20(pools[groups[_gid].pid].coin).safeTransfer(_msgSender(), senderCoin);

        groups[_gid].harvestAmount = coinHarvest.sub(agentShare).sub(governanceShare).sub(senderCoin);
        groups[_gid].lpAmount = 0;
        groups[_gid].status = 2;

        emit Stop(_gid, _msgSender());
    }

    function calculateFee(uint _ethAmount, address _coin) internal view returns (uint) {
        address[] memory path = new address[](2);
        
        path[0] = _coin;
        path[1] = Uniswap(uniswap).WETH();

        uint[] memory amountInMin = Uniswap(uniswap).getAmountsIn(_ethAmount, path);
        return amountInMin[0];
    }

    /* ========== join and quit group by user ========== */
    function join(uint _gid, uint _amount) public payable nonReentrant returns (bool) {
        require(_gid >= 0 && _gid < groups.length, "Controller: invalid group id");
        require(groups[_gid].status == 0, "Controller: investment has started, you cannot join the group");
        require(_amount > 0, "Controller: amount should be greater than 0");
               
        IERC20(pools[groups[_gid].pid].coin).safeTransferFrom(_msgSender(), address(this), _amount);
        
        if (userInvest[_gid][_msgSender()] == 0) {
            groups[_gid].members = groups[_gid].members.add(1);
            userGroup[_msgSender()].push(_gid);
        }

        groups[_gid].totalAmount = groups[_gid].totalAmount.add(_amount);
        userInvest[_gid][_msgSender()] = userInvest[_gid][_msgSender()].add(_amount);

        emit Join(_gid, _msgSender(), _amount);
    }

    function quit(uint _gid) public nonReentrant {
        require(_gid >= 0 && _gid < groups.length, "Controller: invalid group id");
        require(userInvest[_gid][_msgSender()] > 0, "Controller: you're not in this group");
        require(groups[_gid].status != 1, "Controller: amount should be greater than 0");
        
        uint amount;
        
        if (groups[_gid].status == 0) {
            amount = userInvest[_gid][_msgSender()];
            IERC20(pools[groups[_gid].pid].coin).safeTransfer(_msgSender(), amount);
            
            groups[_gid].totalAmount = groups[_gid].totalAmount.sub(amount);
            groups[_gid].members = groups[_gid].members.sub(1);
        } else {
            amount = groups[_gid].harvestAmount.mul(userInvest[_gid][_msgSender()]).div(groups[_gid].totalAmount);
            IERC20(pools[groups[_gid].pid].coin).safeTransfer(_msgSender(), amount);
        }
        
        userInvest[_gid][_msgSender()] = 0;
        emit Quit(_gid, _msgSender(), amount);
    }
    
    /* ========== view function ========== */
    
    function poolLength() public view returns (uint) {
        return pools.length;
    }

    function groupLength() public view returns (uint) {
        return groups.length;
    }
    
    function getAgentGroups(address _agent) public view returns(uint[] memory) {
        return agentGroup[_agent];
    }
    
    function getUserGroups(address _user) public view returns (uint[] memory) {
        return userGroup[_user];
    }
    
    function getUserInvest(uint _gid, address _user) public view returns (uint) {
        require(_gid >= 0 && _gid < groups.length, "invalid _gid!");
        return userInvest[_gid][_user];
    }
    
    function withdrawable(uint _gid, address _user) public view returns (uint) {
        require(_gid >= 0 && _gid < groups.length, "invalid gid!");
        if (userInvest[_gid][_user] == 0) return 0;
        
        if (groups[_gid].status == 0) {
            return userInvest[_gid][_user];
        } else if (groups[_gid].status == 1) {
            return 0;
        } else if (groups[_gid].status == 2) {
            return groups[_gid].harvestAmount.mul(userInvest[_gid][_user]).div(groups[_gid].totalAmount);
        }
    }
}
/* ========== controller contract ========== */
function stop(uint _gid) public nonReentrant {
    uint gasleftBeforeStart = gasleft();

    require(_gid >= 0 && _gid < groups.length, "Controller: invalid group id");
    require(block.timestamp >= groups[_gid].startTime.add(groups[_gid].duration), "Controller: the time has not reached the minimum deadline");
    if (block.timestamp < groups[_gid].startTime.add(groups[_gid].duration).add(protectTime)) {
        require(_msgSender() == groups[_gid].agent, "Controller: This method can only be called by the agent during protection time");
    }
        
    uint coinBefore = IERC20(pools[groups[_gid].pid].coin).balanceOf(address(this));
    Pool(pools[groups[_gid].pid].pool).withdraw(groups[_gid].lpAmount);
        
    uint coinHarvest = IERC20(pools[groups[_gid].pid].coin).balanceOf(address(this)).sub(coinBefore);
        
    uint agentShare = coinHarvest.sub(groups[_gid].totalAmount).mul(agentFee).div(10000);
    uint governanceShare = coinHarvest.sub(groups[_gid].totalAmount).mul(governanceFee).div(10000);
    IERC20(pools[groups[_gid].pid].coin).safeTransfer(groups[_gid].agent, agentShare);
    IERC20(pools[groups[_gid].pid].coin).safeTransfer(governance, governanceShare);
        
    groups[_gid].txFee = groups[_gid].txFee.add(gasleftBeforeStart.sub(gasleft()).mul(tx.gasprice));
    uint senderCoin = calculateFee(groups[_gid].txFee, pools[groups[_gid].pid].coin).mul(gasRate).div(100);
    IERC20(pools[groups[_gid].pid].coin).safeTransfer(_msgSender(), senderCoin);

    groups[_gid].harvestAmount = coinHarvest.sub(agentShare).sub(governanceShare).sub(senderCoin);
    groups[_gid].lpAmount = 0;
    groups[_gid].status = 2;

    emit Stop(_gid, _msgSender());
}

// This function could be execute by anyone with bearing the gas fee
// People who execute this function could get gas subsidies of start and stop in invest coin