contract LaunchPadPool is Ownable, EIP712{
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    using EnumerableSet for EnumerableSet.AddressSet;
    EnumerableSet.AddressSet private _governors;

    // Info of each LaunchPad Project.
    struct LaunchPadInfo {
        uint8 launchStatus;     // launchPad Project status, 0 pending, 1 paying, 2 claiming
        IERC20 launchToken;  //launchpad token
        IERC20 payToken; // pay token

        uint256 timeStart; // launchpad start.
        uint256 timeEnd ;  // launchpad end.

        uint256 tokenAmount; //token amount.
        uint256 priceBasedPayToken;  //pay, decimals is 8
        
        uint256 maxPerAddress; //cap limit 
        uint256 launchMaxPay; //pay cap limit

        uint256 releaseStart;   // claim time start
        uint256 lockPeriod; //lock time

        uint256 totalAmount;    // Total amount of current pool deposit.

    }

    LaunchPadInfo[] public launchPadInfo; //all launchPadInfo

    // Info of each user.
    struct UserInfo {
        uint256 paidAmount;     // How many pay tokens the user has provided.
        uint256 lockedAmount;// user locked token Amount
        uint256 pickedAmount; //user picked amount.
    }

    // Info of each user that stakes LP tokens.
    mapping(uint256 => mapping(address => UserInfo)) public userInfo;


    event Claimed(address indexed owner, uint8 lauchID, address rewardToken, uint256 rewardAmount, uint256 refundAmount);
    event Deposited(address indexed sender, uint8 lauchID, address paidToken, uint256 paidAmount );
    event GovernorAdded( address _user);
    event GovernorDeleted( address _user);

    bytes32 constant public LAUNCHPAD_CALL_HASH_TYPE = keccak256("getLaunchPad(address receiver,uint8 lanchID,uint256 rewardAmount,uint256 refundAmount)");
    address public immutable cSigner;
    
    /**
     * @dev Constructor.
     */
    constructor(address _signer) EIP712("LaunchPadPool", "1"){
        cSigner = _signer;
    }

    // Add a new launchpad project to the pool. Can only be called by the owner.
    function add( IERC20 _launchToken,IERC20 _payToken, uint256 _timeStart, uint256 _timeEnd , uint256 _tokenAmount,uint256 _priceBasedPayToken,uint256 _maxPerAddress,
        uint256 _launchMaxPay, uint256 _releaseStart, uint256 _lockPeriod) public onlyGovernor {
        
        require( _timeStart > block.timestamp && _timeEnd > _timeStart, "invalid time limit");
        
        launchPadInfo.push(LaunchPadInfo({
            launchStatus : 0,
            launchToken : _launchToken,
            payToken : _payToken,
            timeStart : _timeStart,
            timeEnd : _timeEnd,
            tokenAmount : _tokenAmount,
            priceBasedPayToken : _priceBasedPayToken,
            maxPerAddress : _maxPerAddress,
            launchMaxPay : _launchMaxPay,
            releaseStart : _releaseStart,
            lockPeriod : _lockPeriod,
            totalAmount : 0
        }));
        
    }


    function setLaunchPadInfo(uint8 _pid, IERC20 _launchToken,IERC20 _payToken, uint256 _timeStart, uint256 _timeEnd , uint256 _tokenAmount,uint256 _priceBasedPayToken,uint256 _maxPerAddress,
        uint256 _launchMaxPay, uint256 _releaseStart, uint256 _lockPeriod) public onlyGovernor {
        
        require( _timeStart > block.timestamp && _timeEnd > _timeStart, "invalid time limit");

        require( _pid < launchPadInfo.length, "invalid pid");

        LaunchPadInfo storage pool = launchPadInfo[_pid];
        if( pool.launchStatus == 0 ){
            pool.launchToken = _launchToken;
            pool.payToken = _payToken;
            pool.timeStart = _timeStart;
            pool.timeEnd = _timeEnd;
            pool.tokenAmount = _tokenAmount;
            pool.priceBasedPayToken = _priceBasedPayToken;

            pool.maxPerAddress = _maxPerAddress;
            pool.launchMaxPay = _launchMaxPay;
            pool.releaseStart = _releaseStart;
            pool.lockPeriod = _lockPeriod;
        }
        
    }

    function setLaunchPadInfo(uint8 _pid, uint8 _launchStatus ) public onlyGovernor {
        LaunchPadInfo storage pool = launchPadInfo[_pid];

        require( _launchStatus > pool.launchStatus, "invalid status");

        pool.launchStatus = _launchStatus;
        
    }

    //user deposit to enter launchpad
    function deposit(uint8 _pid, uint256 _amount) public {
        LaunchPadInfo storage pool = launchPadInfo[_pid];
        UserInfo storage user = userInfo[_pid][ msg.sender ];

        require( pool.launchStatus == 1 , "invalid status");

        require( pool.timeEnd > block.timestamp, "invalid time");

        //deposit
        TransferHelper.safeTransferFrom( address(pool.payToken), msg.sender, address(this), _amount);
        user.paidAmount = user.paidAmount.add(_amount);

        pool.totalAmount = pool.totalAmount.add(_amount);
        emit Deposited( msg.sender, _pid, address(pool.payToken), _amount);
    }


    //claim reward token quota
    function claim(uint8 _pid, uint256 _rewardAmount, uint256 _refundAmount, uint8 v, bytes32 r, bytes32 s) external {
        LaunchPadInfo storage pool = launchPadInfo[_pid];
        UserInfo storage user = userInfo[_pid][ msg.sender ];

        require( pool.timeEnd <= block.timestamp, "waiting for end");

        require( user.paidAmount > 0 ,"invalid user");

        require( pool.maxPerAddress >= _rewardAmount ,"limit max quota");

        //check rewardamount &refundamount
        uint256 tokenNeedPaid = _rewardAmount.mul( pool.priceBasedPayToken).div(1e8);
        require( user.paidAmount.sub(_refundAmount) >= tokenNeedPaid, "check refund quota");

        //only once time claim
        if( user.lockedAmount > 0 ){
            return;
        }

        user.lockedAmount = _rewardAmount;
        
        bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", 
            ECDSA.toTypedDataHash(_domainSeparatorV4(),
                keccak256(abi.encode(LAUNCHPAD_CALL_HASH_TYPE, msg.sender, _pid, _rewardAmount, _refundAmount))
        )));
        require(ecrecover(digest, v, r, s) == cSigner, "LaunchPadPool: Invalid signer");
       
        uint256 _pickedAmount = _rewardAmount;
        if( pool.lockPeriod > 0 ){
            _pickedAmount = 0;
            if( block.timestamp > pool.releaseStart ){
                _pickedAmount = _rewardAmount.mul(block.timestamp.sub(pool.releaseStart)).div(pool.lockPeriod);
                if( _pickedAmount > _rewardAmount ){
                    _pickedAmount = _rewardAmount;
                }
            }
        }
        user.pickedAmount = _pickedAmount;

        if( _pickedAmount > 0 ){
            TransferHelper.safeTransfer( address(pool.launchToken), msg.sender , _pickedAmount );
        }

        if( _refundAmount > 0 ){
            TransferHelper.safeTransfer( address(pool.payToken), msg.sender , _refundAmount );
        }

        emit Claimed( msg.sender , _pid, address(pool.launchToken), _rewardAmount, _refundAmount );
    }

    //claim release reward token
    function claimReward(uint8 _pid) public {
        LaunchPadInfo storage pool = launchPadInfo[_pid];
        UserInfo storage user = userInfo[_pid][ msg.sender ];

        if( user.lockedAmount > user.pickedAmount ){
            uint256 _pickedAmount = 0;

            if( block.timestamp > pool.releaseStart ){
                _pickedAmount = user.lockedAmount.mul(block.timestamp.sub(pool.releaseStart)).div(pool.lockPeriod);
                if( _pickedAmount > user.lockedAmount ){
                    _pickedAmount = user.lockedAmount;
                }
                _pickedAmount = _pickedAmount.sub( user.pickedAmount );
                user.pickedAmount = user.pickedAmount.add(_pickedAmount);
                TransferHelper.safeTransfer( address(pool.launchToken), msg.sender , _pickedAmount );
            }
        }
    }

    //emergency token
    function withdrawEmergency(address tokenaddress,address to) public onlyOwner{	
        TransferHelper.safeTransfer( tokenaddress, to , IERC20(tokenaddress).balanceOf(address(this)));
    }

    function withdrawEmergencyNative(address to , uint256 amount) public onlyOwner{	
        TransferHelper.safeTransferNative(to, amount);
    }

    //governor
    function addGovernor(address _governor) public onlyOwner returns (bool) {
        require(_governor != address(0), "_governor is the zero address");
        emit GovernorAdded( _governor );
        return EnumerableSet.add(_governors, _governor);
    }

    function delGovernor(address _governor) public onlyOwner returns (bool) {
        require(_governor != address(0), "_governor is the zero address");
        emit GovernorDeleted( _governor );
        return EnumerableSet.remove(_governors, _governor);
    }

    function getGovernorLength() public view returns (uint256) {
        return EnumerableSet.length(_governors);
    }

    function isGovernor(address account) public view returns (bool) {
        return EnumerableSet.contains(_governors, account);
    }

    function getGovernor(uint256 _index) public view  returns (address){
        require(_index <= getGovernorLength() - 1, "index out of bounds");
        return EnumerableSet.at(_governors, _index);
    }

    // modifier for governor function
    modifier onlyGovernor() {
        require(isGovernor(msg.sender) || owner() == msg.sender , "caller is not the governor");
        _;
    }

}
/**
 * @dev LaunchPad Pool.
 */
function claimReward(uint8 _pid) public {
    LaunchPadInfo storage pool = launchPadInfo[_pid];
    UserInfo storage user = userInfo[_pid][ msg.sender ];

    if( user.lockedAmount > user.pickedAmount ){
        uint256 _pickedAmount = 0;

        if( block.timestamp > pool.releaseStart ){
            _pickedAmount = user.lockedAmount.mul(block.timestamp.sub(pool.releaseStart)).div(pool.lockPeriod);
            if( _pickedAmount > user.lockedAmount ){
                _pickedAmount = user.lockedAmount;
            }
            _pickedAmount = _pickedAmount.sub( user.pickedAmount );
            user.pickedAmount = user.pickedAmount.add(_pickedAmount);
            TransferHelper.safeTransfer( address(pool.launchToken), msg.sender , _pickedAmount );
        }
    }
}

//claim release reward token