// Contract: StakingPool
// Target function: function withdrawUnlock(uint256 _pid) public
 the upper range, calculate a new s-value
        // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
        // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
        // these malleable signatures as well.
        if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
            return (address(0), RecoverError.InvalidSignatureS);
        }
        if (v != 27 && v != 28) {
            return (address(0), RecoverError.InvalidSignatureV);
        }

        // If the signature is valid (and not malleable), return the signer address
        address signer = ecrecover(hash, v, r, s);
        if (signer == address(0)) {
            return (address(0), RecoverError.InvalidSignature);
        }

        return (signer, RecoverError.NoError);
    }

    /**
     * @dev Overload of {ECDSA-recover} that receives the `v`,
     * `r` and `s` signature fields separately.
     */
    function recover(
        bytes32 hash,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal pure returns (address) {
        (address recovered, RecoverError error) = tryRecover(hash, v, r, s);
        _throwError(error);
        return recovered;
    }

    /**
     * @dev Returns an Ethereum Signed Message, created from a `hash`. This
     * produces hash corresponding to the one signed with the
     * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * JSON-RPC method as part of EIP-191.
     *
     * See {recover}.
     */
    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
        // 32 is the length in bytes of hash,
        // enforced by the type signature above
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }

    /**
     * @dev Returns an Ethereum Signed Typed Data, created from a
     * `domainSeparator` and a `structHash`. This produces hash corresponding
     * to the one signed with the
     * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
     * JSON-RPC method as part of EIP-712.
     *
     * See {recover}.
     */
    function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
    }
}

/**
 * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
 *
 * The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
 * thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
 * they need in their contracts using a combination of `abi.encode` and `keccak256`.
 *
 * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
 * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
 * ({_hashTypedDataV4}).
 *
 * The implementation of the domain separator was designed to be as efficient as possible while still properly updating
 * the chain id to protect against replay attacks on an eventual fork of the chain.
 *
 * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
 * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
 *
 * _Available since v3.4._
 */
abstract contract EIP712 {
    /* solhint-disable var-name-mixedcase */
    // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
    // invalidate the cached domain separator if the chain id changes.
    bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
    uint256 private immutable _CACHED_CHAIN_ID;

    bytes32 private immutable _HASHED_NAME;
    bytes32 private immutable _HASHED_VERSION;
    bytes32 private immutable _TYPE_HASH;

    /* solhint-enable var-name-mixedcase */

    /**
     * @dev Initializes the domain separator and parameter caches.
     *
     * The meaning of `name` and `version` is specified in
     * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
     *
     * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
     * - `version`: the current major version of the signing domain.
     *
     * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
     * contract upgrade].
     */
    constructor(string memory name, string memory version) {
        bytes32 hashedName = keccak256(bytes(name));
        bytes32 hashedVersion = keccak256(bytes(version));
        bytes32 typeHash = keccak256(
            "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
        );
        _HASHED_NAME = hashedName;
        _HASHED_VERSION = hashedVersion;
        _CACHED_CHAIN_ID = block.chainid;
        _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
        _TYPE_HASH = typeHash;
    }

    /**
     * @dev Returns the domain separator for the current chain.
     */
    function _domainSeparatorV4() internal view returns (bytes32) {
        if (block.chainid == _CACHED_CHAIN_ID) {
            return _CACHED_DOMAIN_SEPARATOR;
        } else {
            return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
        }
    }

    function _buildDomainSeparator(
        bytes32 typeHash,
        bytes32 nameHash,
        bytes32 versionHash
    ) private view returns (bytes32) {
        return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this)));
    }

    /**
     * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
     * function returns the hash of the fully encoded EIP712 message for this domain.
     *
     * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
     *
     * ```solidity
     * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
     *     keccak256("Mail(address to,string contents)"),
     *     mailTo,
     *     keccak256(bytes(mailContents))
     * )));
     * address signer = ECDSA.recover(digest, signature);
     * ```
     */
    function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
        return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
    }
}

contract StakingPool is Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    using EnumerableSet for EnumerableSet.AddressSet;
    EnumerableSet.AddressSet private _governors;

    struct UserInfo {
        uint256 amount;
        uint256 rewardDebt;
        uint256 lockAmount;
        uint256 releaseAmount;
    }

    struct PoolInfo {
        IERC20 lpToken;
        IERC20 rewardToken;

        uint256 timeStart;
        uint256 timeEnd;

        // bonus tokens for per second.
        uint256 bonusPerSecond;
        
        uint256 lockedSeconds;  //after exit stake ,need lock time

        uint256 lpAmount;

        uint256 lastRewardTime;
        uint256 accBonusPerShare;

        uint256 lpTokenType;  //0 indicate normal token, 1 is xtoken
        IERC20  lpTokenFrom; 

        bool open; //pool status
    }

    // Info of each pool.
    PoolInfo[] public poolInfo;

    // Info of each user that stakes LP tokens.
    mapping(uint256 => mapping(address => UserInfo)) public userInfo;

    struct WithdrawOrder {
        uint256 orderTime;
        uint256 amount;
    }

    mapping(uint256 => mapping(address => WithdrawOrder[])) public userWithdrawInfo;

    event Deposit(address indexed user, uint256 indexed pid, address token, uint256 amount);
    event Withdraw(address indexed user, uint256 indexed pid,  address token, uint256 amount);
    event WithdrawUnlock(address indexed user, uint256 indexed pid, address token, uint256 amount);
    event EmergencyWithdraw(address indexed user, uint256 indexed pid, uint256 amount);

    event GovernorAdded( address _user);
    event GovernorDeleted( address _user);

    bytes32 constant public STAKINGPOOL_CALL_HASH_TYPE = keccak256("withdrawExit(address receiver,uint256 pid,uint256 exitAmount)");

    /**
     * @dev Constructor.
     */
    constructor(){
    }

    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    function getLockLength(uint256 _pid, address _user) public view returns (uint256) {
        return userWithdrawInfo[_pid][_user].length;
    }

    // Add a new staking pool to the pool. Can only be called by the owner.
    function add( IERC20 _lpToken,IERC20 _rewardToken, uint256 _timeStart, uint256 _timeEnd , uint256 _bonusPerSecond,uint256 _lockedSeconds,
        uint256 _lpTokenType, IERC20 _lpTokenFrom , bool _open) public onlyGovernor {
        
        require( _timeStart > block.timestamp && _timeEnd > _timeStart, "invalid time limit");
        
        poolInfo.push(PoolInfo({
            lpToken : _lpToken,
            rewardToken : _rewardToken,
            timeStart : _timeStart,
            timeEnd : _timeEnd,

            bonusPerSecond : _bonusPerSecond,
            lockedSeconds : _lockedSeconds,

            lpAmount : 0,
            lastRewardTime : _timeStart,
            accBonusPerShare :0,

            lpTokenType :_lpTokenType,
            lpTokenFrom : _lpTokenFrom,
            open : _open
        }));

        //need transfer staking bonus token
        uint256 _amount = _timeEnd.sub(_timeStart).mul(_bonusPerSecond);

        TransferHelper.safeTransferFrom( address(_rewardToken), msg.sender, address(this), _amount);
        
    }

    // set staking pool info, only can adjust time where pool is not start harvest.
    function setPoolInfo( uint256 _pid, uint256 _timeStart, uint256 _timeEnd , uint256 _bonusPerSecond,uint256 _lockedSeconds,
        uint256 _lpTokenType, IERC20 _lpTokenFrom , bool _open) public onlyGovernor {
        
        require( _timeStart > block.timestamp && _timeEnd > _timeStart, "invalid time limit");
        
        PoolInfo storage pool = poolInfo[_pid];

        require( pool.timeStart > block.timestamp, "staking is enabled");

        require( block.timestamp < pool.timeStart, "pool is mining");

        //reward token 
        uint256 _newAmount = _timeEnd.sub(_timeStart).mul(_bonusPerSecond);
        uint256 _amount = pool.timeEnd.sub(pool.timeStart).mul(pool.bonusPerSecond);

        if( _newAmount > _amount){
            TransferHelper.safeTransferFrom( address(pool.rewardToken), msg.sender, address(this), _newAmount.sub(_amount));
        }
        else if( _newAmount < _amount){
            TransferHelper.safeTransfer( address(pool.rewardToken), msg.sender , _amount.sub(_newAmount) );
        }

        pool.timeStart = _timeStart;
        pool.lastRewardTime = _timeStart;
        pool.timeEnd = _timeEnd;
        pool.bonusPerSecond = _bonusPerSecond;
        pool.lockedSeconds = _lockedSeconds;

        pool.lpTokenType = _lpTokenType;
        pool.lpTokenFrom = _lpTokenFrom;
        pool.open = _open;
    }

    //update pool status
    function setPoolStatus( uint256 _pid,  bool _open) public onlyGovernor {
        PoolInfo storage pool = poolInfo[_pid];
        pool.open = _open;
    }

    // Update reward variables of the given pool to be up-to-date.
    function updatePool(uint256 _pid) public {
        PoolInfo storage pool = poolInfo[_pid];
        uint256 number = block.timestamp ;
        if (number <= pool.lastRewardTime) {
            return;
        }

        if( pool.lastRewardTime >= pool.timeEnd){
            return;
        }

        //set the end time
        if( number >= pool.timeEnd ){
            number = pool.timeEnd;
        }

        uint256 lpSupply = pool.lpAmount;
        if (lpSupply == 0) {
            pool.lastRewardTime = number;
            return;
        }

        uint256 multiplier = number.sub(pool.lastRewardTime);
        uint256 bonusReward = multiplier.mul(pool.bonusPerSecond);
        pool.accBonusPerShare = pool.accBonusPerShare.add(bonusReward.mul(1e12).div(lpSupply));
        pool.lastRewardTime = number;
    }


    function pending(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][_user];
        uint256 accBonusPerShare = pool.accBonusPerShare;
        uint256 lpSupply = pool.lpAmount;
        uint256 number = block.timestamp;

        if( number <= pool.timeStart ){
            return 0;
        }

        if( number > pool.timeEnd ){
            number = pool.timeEnd;
        }

        if (number > pool.lastRewardTime && lpSupply != 0) {
            uint256 multiplier = number.sub(pool.lastRewardTime);
            uint256 bonusReward = multiplier.mul(pool.bonusPerSecond) ;
            accBonusPerShare = accBonusPerShare.add(bonusReward.mul(1e12).div(lpSupply));
        }
        return user.amount.mul(accBonusPerShare).div(1e12).sub(user.rewardDebt);
    }


    // Deposit LP tokens dividends bonus;
    function deposit(uint256 _pid, uint256 _amount) public {
        PoolInfo storage pool = poolInfo[_pid];

        require( pool.open == true , "pool is closed!");

        UserInfo storage user = userInfo[_pid][msg.sender];
        updatePool(_pid);
        if (user.amount > 0) {
            uint256 pendingAmount = user.amount.mul(pool.accBonusPerShare).div(1e12).sub(user.rewardDebt);
            if (pendingAmount > 0) {
                TransferHelper.safeTransfer( address(pool.rewardToken), msg.sender , pendingAmount );
            }
        }
        if (_amount > 0) {
            pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount);
            user.amount = user.amount.add(_amount);

            pool.lpAmount = pool.lpAmount.add(_amount); 
        }
        user.rewardDebt = user.amount.mul(pool.accBonusPerShare).div(1e12);
        emit Deposit(msg.sender, _pid, address(pool.lpToken), _amount);
    }

    // Withdraw LP tokens.
    function withdraw(uint256 _pid, uint256 _amount) public {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][msg.sender];
        require(user.amount >= _amount, "withdraw: not good");
        updatePool(_pid);

        uint256 pendingAmount = user.amount.mul(pool.accBonusPerShare).div(1e12).sub(user.rewardDebt);
        if (pendingAmount > 0) {
            TransferHelper.safeTransfer( address(pool.rewardToken), msg.sender , pendingAmount );
        }
        if (_amount > 0  ) {
            if( pool.lockedSeconds == 0 ){
                user.amount = user.amount.sub(_amount);
                pool.lpAmount = pool.lpAmount.sub(_amount); 
                pool.lpToken.safeTransfer(address(msg.sender), _amount);
            }
            else{
                user.lockAmount = user.lockAmount.add(_amount);

                userWithdrawInfo[_pid][msg.sender].push(WithdrawOrder({orderTime: block.timestamp, amount: _amount}));
            }
            
        }
        user.rewardDebt = user.amount.mul(pool.accBonusPerShare).div(1e12);
        
        emit Withdraw(msg.sender, _pid, address(pool.lpToken), _amount);
    }

    //unlock lp token
    function pendingUnlock(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][_user];
        
        uint256 unlockAmount = 0;
        uint256 checkTime = block.timestamp;

        if( user.lockAmount > 0 && pool.lockedSeconds > 0){
            WithdrawOrder[] memory orders = userWithdrawInfo[_pid][_user];
            uint256 len = orders.length;
            for (uint256 i = 0; i < len; i++) {
                if( orders[i].orderTime.add(pool.lockedSeconds) <= checkTime ){
                    unlockAmount = unlockAmount.add( orders[i].amount);
                }
                else{
                    break;//withdraw orders sorted by ordertime asc
                }
            }
        }

        return unlockAmount;
    }