abstract contract PoolWrapper is ERC20, Ownable, ReentrancyGuard{

    using SafeERC20 for IERC20;
    using SafeMath for uint;

    string  public poolName;
    address public poolCoin;

    address public feeTo;
    address public controller;
    uint public feeRate = 500;

    event Deposit(uint indexed _investAmount, uint indexed _shareAmount, address sender);
    event Withdraw(uint indexed _shareAmount, uint indexed _withdrawAmount, address sender);

    constructor(string memory _poolName, address _poolCoin) internal {
        feeTo = _msgSender();
        poolName = _poolName;
        poolCoin = _poolCoin;
    }
    
    modifier safeApprove(address _coin, address _to, uint _amount) {
        IERC20(_coin).safeApprove(_to, 0);
        IERC20(_coin).safeApprove(_to, _amount);
        _;
    }

    function setController(address _controller) public onlyOwner {
        require(_controller != address(0), "invalid controller address");
        controller = _controller;
    }
    
    function setFeeTo(address _feeTo) public onlyOwner {
        require(_feeTo != address(0), "invalid feeTo address");
        feeTo = _feeTo;
    }

    function setFeeRate(uint _feeRate) public onlyOwner {
        require(_feeRate >= 0 && _feeRate <= 1000, "invalid feeRate");
        feeRate = _feeRate;
    }

    function deposit(uint _amount) public nonReentrant {
        require(_amount > 0, "invalid amount");
        IERC20(poolCoin).safeTransferFrom(_msgSender(), address(this), _amount); 

        // claim        
        uint UnderlyingCoinAmountBeforeClaim = IERC20(poolCoin).balanceOf(address(this));
        _claim();
        uint underlyingCoinAmountClaimed = IERC20(poolCoin).balanceOf(address(this)).sub(UnderlyingCoinAmountBeforeClaim);
        
        // send fee
        if ( underlyingCoinAmountClaimed > 0 && _msgSender() != controller) {
            uint claimFee = underlyingCoinAmountClaimed.mul(feeRate).div(10000);
            IERC20(poolCoin).safeTransfer(feeTo, claimFee);
            underlyingCoinAmountClaimed = underlyingCoinAmountClaimed.sub(claimFee);
        }

        // deposit and stake
        uint lpAmount = _deposit(_amount.add(underlyingCoinAmountClaimed));
        _stake(lpAmount);
        uint investLpAmount = lpAmount.mul(_amount).div(_amount.add(underlyingCoinAmountClaimed));
        
        // calculate share
        uint shares;
        if(totalSupply() == 0) {
            shares = investLpAmount;
        } else {
            uint totalLpBalance = _stakeBalance();
            shares = investLpAmount.mul(totalSupply()).div(totalLpBalance.sub(investLpAmount));
        }

        // mint share
        _mint(_msgSender(), shares);     
        emit Deposit(_amount, shares, _msgSender());
    }

    function withdraw(uint _amount) public nonReentrant {
        require(_amount > 0 && balanceOf(_msgSender()) >= _amount, "invalid _amount or insufficient balance");

        // claim
        uint UnderlyingCoinAmountBeforeClaim = IERC20(poolCoin).balanceOf(address(this));
        _claim();
        uint underlyingCoinAmountClaimed = IERC20(poolCoin).balanceOf(address(this)).sub(UnderlyingCoinAmountBeforeClaim);
        
        if (underlyingCoinAmountClaimed > 0) {
            // send fee
            if(_msgSender() != controller) {
                uint claimFee = underlyingCoinAmountClaimed.mul(feeRate).div(10000);
                IERC20(poolCoin).safeTransfer(feeTo, claimFee);
                underlyingCoinAmountClaimed = underlyingCoinAmountClaimed.sub(claimFee);
            }
            // deposit and stake
            uint lpAmount = _deposit(underlyingCoinAmountClaimed);
            _stake(lpAmount);
        }
        
        // calculate value of share
        uint totalLpBalance = _stakeBalance();
        uint unstakeLpAmountExpected = totalLpBalance.mul(_amount).div(totalSupply());
        
        // withdraw
        uint UnderlyingCoinAmountBeforeWithdraw = IERC20(poolCoin).balanceOf(address(this));
        uint unstakeLpAmountFactual = _unstake(unstakeLpAmountExpected);
        _withdraw(unstakeLpAmountFactual);
        uint withdrawUnderlyingCoinAmount = IERC20(poolCoin).balanceOf(address(this)).sub(UnderlyingCoinAmountBeforeWithdraw);

        // burn share
        _burn(_msgSender(), _amount);
        IERC20(poolCoin).safeTransfer(_msgSender(), withdrawUnderlyingCoinAmount);
        emit Withdraw(_amount, withdrawUnderlyingCoinAmount, _msgSender());
    }

    /* ========== internal function ========== */
    
    function _deposit(uint) internal virtual returns (uint) { 
        revert("need override");
    }

    function _stake(uint) internal virtual {
        revert("need override");
    }

    function _unstake(uint) internal virtual returns (uint)  {
        revert("need override");
    }
    
    function _withdraw(uint) internal virtual {    
        revert("need override");
    }

    function _claim() internal virtual returns (uint) {
        revert("need override");
    }

    function _stakeBalance() internal view virtual returns (uint) {
        revert("need override");
    } 
}
/* ========== Mining pool wrapper ========== */
function _deposit(uint) internal virtual returns (uint) { 
    revert("need override");
}

/* ========== internal function ========== */