contract C10Token is ERC20Detailed, ERC20Burnable, Ownable, Pausable, MinterRole {

    // Maps participant addresses to the eth balance pending token issuance
    mapping(address => uint256) public pendingBuys;
    // The participant accounts waiting for token issuance
    address[] public participantAddresses;

    // Maps participant addresses to the withdrawal request
    mapping (address => uint256) public pendingWithdrawals;
    address payable[] public withdrawals;

    uint256 public minimumWei = 50 finney;
    uint256 public entryFee = 50;  // 0.5% , or 50 bips
    uint256 public exitFee = 50;  // 0.5% , or 50 bips
    uint256 public minTokenRedemption = 1 ether;
    uint256 public maxAllocationsPerTx = 50;
    uint256 public maxWithdrawalsPerTx = 50;
    Price public price;

    address public whitelistContract;

    struct Price {
        uint256 numerator;
        uint256 denominator;
    }

    event PriceUpdate(uint256 numerator, uint256 denominator);
    event AddLiquidity(uint256 value);
    event RemoveLiquidity(uint256 value);
    event DepositReceived(address indexed participant, uint256 value);
    event TokensIssued(address indexed participant, uint256 amountTokens, uint256 etherAmount);
    event WithdrawRequest(address indexed participant, uint256 amountTokens);
    event Withdraw(address indexed participant, uint256 amountTokens, uint256 etherAmount);
    event WithdrawInvalidAddress(address indexed participant, uint256 amountTokens);
    event WithdrawFailed(address indexed participant, uint256 amountTokens);
    event TokensClaimed(address indexed token, uint256 balance);

    constructor (uint256 priceNumeratorInput, address whitelistContractInput)
        ERC20Detailed("CRYPTO10 Hedged", "C10", 18)
        ERC20Burnable()
        Pausable() public {
            price = Price(priceNumeratorInput, 1000);
            require(priceNumeratorInput > 0, "Invalid price numerator");
            require(whitelistContractInput != address(0), "Invalid whitelist address");
            whitelistContract = whitelistContractInput;
    }

    /**
     * @dev fallback function that buys tokens if the sender is whitelisted.
     */
    function () external payable {
        buyTokens(msg.sender);
    }

    /**
     * @dev Explicitly buy via contract.
     */
    function buy() external payable {
        buyTokens(msg.sender);
    }

    /**
     * Sets the maximum number of allocations in a single transaction.
     * @dev Allows us to configure batch sizes and avoid running out of gas.
     */
    function setMaxAllocationsPerTx(uint256 newMaxAllocationsPerTx) external onlyOwner {
        require(newMaxAllocationsPerTx > 0, "Must be greater than 0");
        maxAllocationsPerTx = newMaxAllocationsPerTx;
    }

    /**
     * Sets the maximum number of withdrawals in a single transaction.
     * @dev Allows us to configure batch sizes and avoid running out of gas.
     */
    function setMaxWithdrawalsPerTx(uint256 newMaxWithdrawalsPerTx) external onlyOwner {
        require(newMaxWithdrawalsPerTx > 0, "Must be greater than 0");
        maxWithdrawalsPerTx = newMaxWithdrawalsPerTx;
    }

    function setEntryFee(uint256 newFee) external onlyOwner {
        require(newFee < 10000, "Must be less than 100 percent");
        entryFee = newFee;
    }

    function setExitFee(uint256 newFee) external onlyOwner {
        require(newFee < 10000, "Must be less than 100 percent");
        exitFee = newFee;
    }

    /// Sets the minimum wei when buying tokens.
    function setMinimumBuyValue(uint256 newMinimumWei) external onlyOwner {
        require(newMinimumWei > 0, "Minimum must be greater than 0");
        minimumWei = newMinimumWei;
    }

    /// Sets the minimum number of tokens to redeem.
    function setMinimumTokenRedemption(uint256 newMinTokenRedemption) external onlyOwner {
        require(newMinTokenRedemption > 0, "Minimum must be greater than 0");
        minTokenRedemption = newMinTokenRedemption;
    }

    /// Updates the price numerator.
    function updatePrice(uint256 newNumerator) external onlyMinter {
        require(newNumerator > 0, "Must be positive value");

        price.numerator = newNumerator;

        allocateTokens();
        processWithdrawals();
        emit PriceUpdate(price.numerator, price.denominator);
    }

    /// Updates the price denominator.
    function updatePriceDenominator(uint256 newDenominator) external onlyMinter {
        require(newDenominator > 0, "Must be positive value");

        price.denominator = newDenominator;
    }

    /**
     * Whitelisted token holders can request token redemption, and withdraw ETH.
     * @param amountTokensToWithdraw The number of tokens to withdraw.
     * @dev withdrawn tokens are burnt.
     */
    function requestWithdrawal(uint256 amountTokensToWithdraw) external whenNotPaused 
        onlyWhitelisted {

        address payable participant = msg.sender;
        require(balanceOf(participant) >= amountTokensToWithdraw, 
            "Cannot withdraw more than balance held");
        require(amountTokensToWithdraw >= minTokenRedemption, "Too few tokens");

        burn(amountTokensToWithdraw);

        uint256 pendingAmount = pendingWithdrawals[participant];
        if (pendingAmount == 0) {
            withdrawals.push(participant);
        }
        pendingWithdrawals[participant] = pendingAmount.add(amountTokensToWithdraw);
        emit WithdrawRequest(participant, amountTokensToWithdraw);
    }

    /// Allows owner to claim any ERC20 tokens.
    function claimTokens(ERC20 token) external onlyOwner {
        require(address(token) != address(0), "Invalid address");
        uint256 balance = token.balanceOf(address(this));
        token.transfer(owner(), token.balanceOf(address(this)));
        emit TokensClaimed(address(token), balance);
    }
    
    /**
     * @dev Allows the owner to burn a specific amount of tokens on a participant's behalf.
     * @param value The amount of tokens to be burned.
     */
    function burnForParticipant(address account, uint256 value) external onlyOwner {
        _burn(account, value);
    }


    /// Adds liquidity to the contract, allowing anyone to deposit ETH
    function addLiquidity() external payable {
        require(msg.value > 0, "Must be positive value");
        emit AddLiquidity(msg.value);
    }

    /// Removes liquidity, allowing owner to transfer eth to the owner.
    function removeLiquidity(uint256 amount) external onlyOwner {
        require(amount <= address(this).balance, "Insufficient balance");

        msg.sender.transfer(amount);
        emit RemoveLiquidity(amount);
    }

    /// Allow the owner to remove a minter
    function removeMinter(address account) external onlyOwner {
        require(account != msg.sender, "Use renounceMinter");
        _removeMinter(account);
    }

    /// Allow the owner to remove a pauser
    function removePauser(address account) external onlyOwner {
        require(account != msg.sender, "Use renouncePauser");
        _removePauser(account);
    }

    /// returns the number of withdrawals pending.
    function numberWithdrawalsPending() external view returns (uint256) {
        return withdrawals.length;
    }

    /// returns the number of pending buys, waiting for token issuance.
    function numberBuysPending() external view returns (uint256) {
        return participantAddresses.length;
    }

    /**
     * @dev Function to mint tokens when not paused.
     * @param to The address that will receive the minted tokens.
     * @param value The amount of tokens to mint.
     * @return A boolean that indicates if the operation was successful.
     */
    function mint(address to, uint256 value) public onlyMinter whenNotPaused returns (bool) {
        _mint(to, value);
        return true;
    }

    /**
     * First phase of the 2-part buy, the participant deposits eth and waits
     * for a price to be set so the tokens can be minted.
     * @param participant whitelisted buyer.
     */
    function buyTokens(address participant) internal whenNotPaused onlyWhitelisted {
        assert(participant != address(0));

        // Ensure minimum investment is met
        require(msg.value >= minimumWei, "Minimum wei not met");

        uint256 pendingAmount = pendingBuys[participant];
        if (pendingAmount == 0) {
            participantAddresses.push(participant);
        }

        // Increase the pending balance and wait for the price update
        pendingBuys[participant] = pendingAmount.add(msg.value);

        emit DepositReceived(participant, msg.value);
    }

    /// Internal function to allocate token.
    function allocateTokens() internal {
        uint256 numberOfAllocations = min(participantAddresses.length, maxAllocationsPerTx);
        uint256 startingIndex = participantAddresses.length;
        uint256 endingIndex = participantAddresses.length.sub(numberOfAllocations);

        for (uint256 i = startingIndex; i > endingIndex; i--) {
            handleAllocation(i - 1);
        }
    }

    function handleAllocation(uint256 index) internal {
        address participant = participantAddresses[index];
        uint256 deposit = pendingBuys[participant];
        uint256 feeAmount = deposit.mul(entryFee) / 10000;
        uint256 balance = deposit.sub(feeAmount);

        uint256 newTokens = balance.mul(price.numerator) / price.denominator;
        pendingBuys[participant] = 0;
        participantAddresses.pop();

        if (feeAmount > 0) {
            address(uint160(owner())).transfer(feeAmount);
        }

        mint(participant, newTokens);
        emit TokensIssued(participant, newTokens, balance);
    }

    /// Internal function to process withdrawals.
    function processWithdrawals() internal {
        uint256 numberOfWithdrawals = min(withdrawals.length, maxWithdrawalsPerTx);
        uint256 startingIndex = withdrawals.length;
        uint256 endingIndex = withdrawals.length.sub(numberOfWithdrawals);

        for (uint256 i = startingIndex; i > endingIndex; i--) {
            handleWithdrawal(i - 1);
        }
    }

    function handleWithdrawal(uint256 index) internal {
        address payable participant = withdrawals[index];
        uint256 tokens = pendingWithdrawals[participant];
        uint256 withdrawValue = tokens.mul(price.denominator) / price.numerator;
        pendingWithdrawals[participant] = 0;
        withdrawals.pop();

        if (address(this).balance < withdrawValue) {
            mint(participant, tokens);
            emit WithdrawFailed(participant, tokens);
            return;
        }

        uint256 feeAmount = withdrawValue.mul(exitFee) / 10000;
        uint256 balance = withdrawValue.sub(feeAmount);
        if (participant.send(balance)) {
            if (feeAmount > 0) {
                address(uint160(owner())).transfer(feeAmount);
            }
            emit Withdraw(participant, tokens, balance);
        } else {
            mint(participant, tokens);
            emit WithdrawInvalidAddress(participant, tokens);
        }
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    modifier onlyWhitelisted() {
        require(InvictusWhitelist(whitelistContract).isWhitelisted(msg.sender), "Must be whitelisted");
        _;
    }
}
/**
 * Contract for CRYPTO10 Hedged (C10) fund.
 *
 */
function () external payable {
    buyTokens(msg.sender);
}

/**
 * @dev fallback function that buys tokens if the sender is whitelisted.
 */