contract ProphetPool is VRFConsumerBase, ReentrancyGuard, Ownable {
    using Address for address;
    using Uint256ArrayUtils for uint256[];
    using AddressArrayUtils for address[];

    /* ============ Structs ============ */

    struct PoolConfig {
        uint256 numOfWinners;
        uint256 participantLimit;
        uint256 enterAmount;
        uint256 feePercentage;
        uint256 randomSeed;
        uint256 startedAt;
    }

    /* ============ Enums ============ */

    enum PoolStatus { NOTSTARTED, INPROGRESS, CLOSED }

    /* ============ Events ============ */

    event FeeRecipientSet(address indexed _feeRecipient);
    event MaxParticipationCompleted(address indexed _from);
    event RandomNumberGenerated(uint256 indexed randomness);
    event WinnersGenerated(uint256[] winnerIndexes);
    event PoolSettled();
    event PoolStarted(
        uint256 participantLimit,
        uint256 numOfWinners,
        uint256 enterAmount,
        uint256 feePercentage,
        uint256 startedAt
    );
    event PoolReset();
    event EnteredPool(address indexed _participant, uint256 _amount, uint256 indexed _participantIndex);

    /* ============ State Variables ============ */

    IProphetPoolFactory private factory;
    address private feeRecipient;
    string private poolName;
    IERC20 private enterToken;
    PoolStatus private poolStatus;
    PoolConfig private poolConfig;

    address[] private participants;
    uint256[] private winnerIndexes;
    uint256 private totalEnteredAmount;
    uint256 private rewardPerParticipant;

    uint256 internal randomResult;
    bool internal areWinnersGenerated;

    /* ============ Modifiers ============ */

    modifier onlyValidPool() {
        require(participants.length < poolConfig.participantLimit, "Max Participation for the Pool Reached");
        require(poolStatus == PoolStatus.INPROGRESS, "The Pool is not started or closed");
        _;
    }

    modifier onlyEOA() {
        require(tx.origin == msg.sender, "Only EOA allowed");
        _;
    }

    /* ============ Constructor ============ */

    /**
     * Create the ProphetPool with Chainlink VRF configuration for Random number generation.
     *
     * @param _poolName             Pool name
     * @param _enterToken           ERC20 token to enter the pool. If it's ETH pool, it should be WETH address
     * @param _factory              ProphetPoolFactory
     * @param _feeRecipient         Where the fee go
     */
    constructor(
        string memory _poolName,
        address _enterToken,
        address _factory,
        address _feeRecipient
    )
        public
        VRFConsumerBase(IProphetPoolFactory(_factory).getVrfCoordinator(), IProphetPoolFactory(_factory).getLinkToken())
    {
        poolName = _poolName;
        enterToken = IERC20(_enterToken);
        factory = IProphetPoolFactory(_factory);
        feeRecipient = _feeRecipient;

        poolStatus = PoolStatus.NOTSTARTED;
    }

    /* ============ External/Public Functions ============ */

    /**
     * Set the Pool Config, initializes an instance of and start the pool.
     *
     * @param _numOfWinners         Number of winners in the pool
     * @param _participantLimit     Maximum number of paricipants
     * @param _enterAmount          Exact amount to enter this pool
     * @param _feePercentage        Manager fee of this pool
     * @param _randomSeed           Seed for Random Number Generation
     */
    function setPoolRules(
        uint256 _numOfWinners,
        uint256 _participantLimit,
        uint256 _enterAmount,
        uint256 _feePercentage,
        uint256 _randomSeed
    ) external onlyOwner {
        require(poolStatus == PoolStatus.NOTSTARTED, "A Pool is in progress");
        require(_numOfWinners != 0, "Number of winner must not be 0");
        require(_numOfWinners < _participantLimit, "Too much number of winners");

        poolConfig = PoolConfig(
            _numOfWinners,
            _participantLimit,
            _enterAmount,
            _feePercentage,
            _randomSeed,
            block.timestamp
        );
        poolStatus = PoolStatus.INPROGRESS;
        emit PoolStarted(
            _participantLimit,
            _numOfWinners,
            _enterAmount,
            _feePercentage,
            block.timestamp
        );
    }

    /**
     * Set the Pool Config, initializes an instance of and start the pool.
     *
     * @param _feeRecipient         Number of winners in the pool
     */
    function setFeeRecipient(address _feeRecipient) external onlyOwner {
        require(_feeRecipient != address(0), "Invalid address");
        feeRecipient = _feeRecipient;

        emit FeeRecipientSet(feeRecipient);
    }

    /**
     * Enter pool with ETH
     */
    function enterPoolEth() external payable onlyValidPool onlyEOA returns (uint256) {
        require(msg.value == poolConfig.enterAmount, "Insufficent registration amount provided");
        if (!_isEthPool()) {
            revert("ETH isn't accepted");
        }
        // wrap ETH to WETH
        IWETH(factory.getWeth()).deposit{ value: msg.value }();

        return _enterPool();
    }

    /**
     * Enter pool with ERC20 token
     */
    function enterPool() external onlyValidPool onlyEOA returns (uint256) {
        enterToken.transferFrom(
                msg.sender,
                address(this),
                poolConfig.enterAmount
            );

        return _enterPool();
    }

    /**
     * Settle the pool, the winners are selected randomly and fee is transfer to the manager.
     */
    function settlePool() external {
        require(randomResult > 0, "RND in progress");
        require(poolStatus == PoolStatus.INPROGRESS, "The Pool is in progress");

        // generate winnerIndexes until the numOfWinners reach
        uint256 newRandom = randomResult;
        uint256 offset = 0;
        while(winnerIndexes.length < poolConfig.numOfWinners) {
            uint256 winningIndex = newRandom.mod(poolConfig.participantLimit);
            if (!winnerIndexes.contains(winningIndex)) {
                winnerIndexes.push(winningIndex);
            }
            offset.add(1);
            newRandom = _getRandomNumberBlockchain(offset, newRandom);
        }
        areWinnersGenerated = true;
        emit WinnersGenerated(winnerIndexes);

        // set pool CLOSED status
        poolStatus = PoolStatus.CLOSED;

        // transfer fees
        uint256 feeAmount = totalEnteredAmount.mul(poolConfig.feePercentage).div(100);
        rewardPerParticipant = (totalEnteredAmount.sub(feeAmount)).div(poolConfig.numOfWinners);
        _transferEnterToken(feeRecipient, feeAmount);

        // collectRewards();
        emit PoolSettled();
    }

    /**
     * The winners of the pool can call this function to transfer their winnings
     * from the pool contract to their own address.
     */
    function collectRewards() external nonReentrant {
        require(poolStatus == PoolStatus.CLOSED, "The Pool is not settled.");

        for (uint256 i = 0; i < poolConfig.participantLimit; i = i.add(1)) {
            address player = participants[i];
            if (winnerIndexes.contains(i)) {
                // if winner
                _transferEnterToken(player, rewardPerParticipant);
            }
        }
        _resetPool();
    }

    /**
     * The contract will receive Ether
     */
    receive() external payable {}

    /**
     * Getter for factory
     */
    function getFactory() external view returns (address) {
        return address(factory);
    }

    /**
     * Getter for fee recipient
     */
    function getFeeRecipient() external view returns (address) {
        return feeRecipient;
    }

    /**
     * Getter for poolName
     */
    function getPoolName() external view returns (string memory) {
        return poolName;
    }

    /**
     * Getter for enterToken
     */
    function getEnterToken() external view returns (address) {
        return address(enterToken);
    }

    /**
     * Getter for poolStatus
     */
    function getPoolStatus() external view returns (PoolStatus) {
        return poolStatus;
    }

    /**
     * Getter for poolConfig
     */
    function getPoolConfig() external view returns (PoolConfig memory) {
        return poolConfig;
    }

    /**
     * Getter for totalEnteredAmount
     */
    function getTotalEnteredAmount() external view returns (uint256) {
        return totalEnteredAmount;
    }

    /**
     * Getter for rewardPerParticipant
     */
    function getRewardPerParticipant() external view returns (uint256) {
        return rewardPerParticipant;
    }

    /**
     * Get all participants
     */
    function getParticipants() external view returns(address[] memory) {
        return participants;
    }

    /**
     * Get one participant by index
     * @param _index                 Index of the participants array
     */
    function getParticipant(uint256 _index) external view returns(address) {
        return participants[_index];
    }

    /**
     * Getter for winnerIndexes
     */
    function getWinnerIndexes() external view returns(uint256[] memory) {
        return winnerIndexes;
    }

    /**
     * Get if the account is winner
     */
    function isWinner(address _account) external view returns(bool) {
        (uint256 index, bool isExist) = participants.indexOf(_account);
        if (isExist) {
            return winnerIndexes.contains(index);
        } else {
            return false;
        }
    }

    /* ============ Private/Internal Functions ============ */

    /**
     * Participant enters the pool and enter amount is transferred from the user to the pool.
     */
    function _enterPool() internal returns(uint256 _participantIndex) {
        participants.push(msg.sender);

        totalEnteredAmount = totalEnteredAmount.add(poolConfig.enterAmount);

        if (participants.length == poolConfig.participantLimit) {
            emit MaxParticipationCompleted(msg.sender);
            _getRandomNumber(poolConfig.randomSeed);
        }

        _participantIndex = (participants.length).sub(1);
        emit EnteredPool(msg.sender, poolConfig.enterAmount, _participantIndex);
    }

    /**
     * Reset the pool, clears the existing state variable values and the pool can be initialized again.
     */
    function _resetPool() internal {
        poolStatus = PoolStatus.INPROGRESS;
        delete totalEnteredAmount;
        delete rewardPerParticipant;
        randomResult = 0;
        areWinnersGenerated = false;
        delete winnerIndexes;
        delete participants;
        emit PoolReset();

        uint256 tokenBalance = enterToken.balanceOf(address(this));
        if (tokenBalance > 0) {
            enterToken.transfer(feeRecipient, tokenBalance);
        }
    }

    /**
     * Transfer enterToken even it's ETH or ERC20.
     *
     * @param _to                   Offset to generate the random number
     * @param _amount               Random number to generate the other random number
     */
    function _transferEnterToken(address _to, uint256 _amount) internal {
        if (_isEthPool()) {
            IWETH(factory.getWeth()).withdraw(_amount);
            (bool status, ) = payable(_to).call{value: _amount}("");
            require(status, "ETH not transferred");
        } else {
            enterToken.transfer(address(_to), _amount);
        }
    }

    /**
     * Check pool is ETH pool or not
     */
    function _isEthPool() internal view returns (bool) {
        return address(enterToken) == factory.getWeth();
    }

    /**
     * Generate a random number based on the blockHash and random offset
     *
     * @param _offset               Offset to generate the random number
     * @param _randomness           Random number to generate the other random number
     */
    function _getRandomNumberBlockchain(uint256 _offset, uint256 _randomness)
        internal
        view
        returns (uint256)
    {
        bytes32 baseHash = keccak256(
            abi.encodePacked(
                blockhash(block.number),
                bytes32(_offset),
                bytes32(_randomness)
            )
        );
        return uint256(baseHash);
    }

    /**
     * Calls ChainLink Oracle's inherited function for Random Number Generation.
     * The contract must have enough LINK required for VRF.
     *
     * @param _userProvidedSeed     Seed to generate the random number
     */
    function _getRandomNumber(uint256 _userProvidedSeed)
        internal
        returns (bytes32 requestId)
    {
        require(
            IERC20(factory.getLinkToken()).balanceOf(address(this)) >= factory.getVrfFee(),
            "Not enough LINK for VRF"
        );
        randomResult = 0;
        return
            requestRandomness(
                factory.getVrfKeyHash(),
                factory.getVrfFee(),
                _userProvidedSeed
            );
    }

    /**
     * Callback function used by VRF Coordinator.
     *
     * @param _randomness     Generated random number
     */
    function fulfillRandomness(bytes32, uint256 _randomness) internal override {
        randomResult = _randomness;
        emit RandomNumberGenerated(_randomness);
    }
}
/**
 * @title ProphetPool
 * @author Prophecy
 *
 * Smart contract that facilitates that draws lucky winners in the pool and distribut rewards to the winners.
 */
function getPoolStatus() external view returns (PoolStatus) {
    return poolStatus;
}

/**
 * Getter for poolStatus
 */