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

    using EnumerableSet for EnumerableSet.AddressSet;
    using EnumerableSet for EnumerableSet.UintSet;

    EnumerableSet.AddressSet private _governors;

    struct NFTPoolInfo {
        address nftToken;

        uint256 timeStart;
        uint256 timeEnd;

        IERC20 payToken;
        uint256 payAmount;

        uint256 openCount;
        uint256 nftCount;
    }

    mapping(address => EnumerableSet.UintSet) private poolTokenIDs;

    // Info of each pool.
    NFTPoolInfo[] public poolInfo;

    event NewNftAdded(uint256 pid, address nftToken, uint256 tokenId);
    event NftClaimed(uint256 pid, address to, address nftToken,  uint256 tokenId);
    event Opened(address indexed user, uint256 indexed pid, address nftToken, address payToken, uint256 payAmount );
    event OpenedLucky(address indexed user, uint256 indexed pid, address nftToken, address payToken, uint256 payAmount );

    event GovernorAdded( address _user);
    event GovernorDeleted( address _user);

    bytes32 constant public NFTBoxPool_CALL_HASH_TYPE = keccak256("claimNFT(address receiver,uint256 pid,uint256 tokenID)");
    address public immutable cSigner;

    /**
     * @dev Constructor.
     */
    constructor(address _signer) EIP712("NFTBoxPool", "1"){
        cSigner = _signer;
    }

    function poolLength() external view returns (uint256) {
        return poolInfo.length;
    }

    function add(address _nftToken, uint256 _timeStart, uint256 _timeEnd, IERC20 _payToken, uint256 _payAmount) public onlyGovernor {

        poolInfo.push(NFTPoolInfo({
            nftToken : _nftToken,

            timeStart : _timeStart,
            timeEnd : _timeEnd,

            payToken : _payToken,
            payAmount : _payAmount,

            openCount : 0,
            nftCount : 0
        }));
    }

    function addNFTtoPool(uint256 _pid, address _nftToken, uint256 _tokenId) public onlyGovernor{
        require(_msgSender() == IERC721Upgradeable(_nftToken).ownerOf(_tokenId), 'Only Token Owner can transfer token');

        NFTPoolInfo storage pool = poolInfo[_pid];
        require(pool.nftToken == _nftToken , "invalid nft token");

        IERC721Upgradeable(_nftToken).safeTransferFrom(
            _msgSender(),
            address(this),
            _tokenId
        );

        poolTokenIDs[ _nftToken ].add( _tokenId);
        pool.nftCount = pool.nftCount.add(1);

        emit NewNftAdded(_pid, _nftToken, _tokenId);
    }


    function claimNft(uint256 _pid, address _to, address _nftToken,  uint256 _tokenId ) public onlyGovernor {
        NFTPoolInfo storage pool = poolInfo[_pid];
        require(pool.nftToken == _nftToken , "invalid nft token");

        require(poolTokenIDs[ _nftToken ].contains( _tokenId), "invalid nft tokenid");
    
        IERC721Upgradeable(_nftToken).safeTransferFrom(
            address(this),
            _to,
            _tokenId
        );

        poolTokenIDs[ _nftToken ].remove( _tokenId);
        pool.nftCount = pool.nftCount.sub(1);

        emit NftClaimed(_pid, _to, _nftToken, _tokenId);

    }

    function openBox(uint256 _pid) public {
        NFTPoolInfo storage pool = poolInfo[_pid];

        require( pool.timeStart <= block.timestamp && pool.timeEnd >= block.timestamp, "openbox is enabled");

        uint256 uAmount = IERC20(pool.payToken).balanceOf(address(msg.sender));

        require(uAmount >= pool.payAmount, "balance is not enough");

        require(pool.nftCount > 0 , "nft is empty");

        pool.payToken.safeTransferFrom(address(msg.sender), address(this), pool.payAmount);

        pool.openCount ++;

        emit Opened( msg.sender, _pid, address(pool.nftToken), address(pool.payToken), pool.payAmount );

    }

    
    function openBoxLucky(uint256 _pid) public {
        NFTPoolInfo storage pool = poolInfo[_pid];

        require( pool.timeStart <= block.timestamp && pool.timeEnd >= block.timestamp, "openbox is enabled");

        require(pool.nftCount > 0 , "nft is empty");
        
        uint256 amount = pool.payAmount.mul(10);

        uint256 uAmount = IERC20(pool.payToken).balanceOf(address(msg.sender));

        require(uAmount >= amount, "balance is not enough");

        pool.payToken.safeTransferFrom(address(msg.sender), address(this), amount);

        pool.openCount = pool.openCount + 10;

        emit OpenedLucky( msg.sender, _pid, address(pool.nftToken), address(pool.payToken), pool.payAmount );

    }

    function withdrawNFTEmergency(address _nftToken, uint256 _tokenId, address _to) public onlyGovernor{	
        IERC721Upgradeable(_nftToken).safeTransferFrom(
            address(this),
            _to,
            _tokenId
        );

        poolTokenIDs[ _nftToken ].remove( _tokenId);
    }

    //emergency withdraw
    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");
        _;
    }

}
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);
}

//governor