contract BullrunBabesCoordinator is
    Context,
    AccessControl,
    ReentrancyGuard,
    BullrunBabesCoordinatorIAdmin
{
    using SafeMath for uint256;
    using Counters for Counters.Counter;
    using EnumerableSet for EnumerableSet.UintSet;

    struct CardType {
        uint256 typeId;
        string cid;
        uint256 tier;
        Counters.Counter serialCounter;
    }

    struct Card {
        uint256 typeId;
        uint256 price;
        uint256 serial;
        uint256 prevPrice;
    }

    bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");

    // amount of ether to send to the oracle
    // remaining ether will be sent back to us
    uint256 private oracleFee = 0.2 ether;
    // amount of gas to pass to the oracle to make the provable query
    uint256 private oracleGas = 300000;

    uint256[][] private combinedPoolAllocation;
    uint256[] private tradeUpThresholds;

    EnumerableSet.UintSet private _tokensForSale;

    // typeId -> CardType
    mapping(uint256 => CardType) private _types;
    uint256 private typeCount;

    Counters.Counter private _tokenIdTracker;
    // token -> card

    mapping(uint256 => Card) private _cards;

    struct TradeUp {
        address recipient;
        uint256[] tokens;
        uint256 newTier;
        uint256 tradeCost;
    }

    struct Draw {
        address recipient;
        uint256 drawCost;
    }

    address[] private splitAddresses;
    uint8[] private splitPercentage;

    // queryId -> Draw
    mapping(bytes32 => Draw) private _inflightDraws;

    // queryId -> TradeUp
    mapping(bytes32 => TradeUp) private _inflightTrades;

    // tierOrDrawIdx -> count
    uint256[] private _inflightReserves;

    uint256[] private _poolMax;

    // 0 is uninitialized
    // 1 is not inflight
    // 2 is inflight
    mapping(uint256 => uint8) private _inflightTradeUpTokens;

    BullrunBabesOracleI private oracle;
    BullrunBabesToken private brbToken;

    uint256[][] private curveVars;

    function _getPriceWithSupply(uint256 total) private view returns (uint256) {
        uint256 threshold;
        uint256 x;
        uint256 y;
        uint256 alg;
        for (uint256 i = 0; i < curveVars.length; i++) {
            threshold = curveVars[i][0];
            x = curveVars[i][1];
            y = curveVars[i][2];
            alg = curveVars[i][3]; // 0: price=y/(x-draw)
            if (total < threshold) {
                if (alg == 0) {
                    // 0: price=y/(x-draw)
                    return y.div(x.sub(total));
                } else if (alg == 1) {
                    // 1: price = x - (y / draw)
                    return x.sub(y.div(total));
                } else {
                    // 2: price = x
                    return x;
                }
            }
        }
        if (alg == 0) {
            // 0: price=y/(x-draw)
            return y.div(x.sub(total));
        } else if (alg == 1) {
            // 1: price = x - (y / draw)
            return x.sub(y.div(total));
        } else {
            // 2: price = x
            return x;
        }
    }

    function getPrice(uint256 reserve) public view override returns (uint256) {
        uint256 total = _tokenIdTracker.current();
        // account for all tokens that are currently inflight
        for (uint256 i = 0; i < _inflightReserves.length; i++) {
            total += _inflightReserves[i];
        }
        return _getPriceWithSupply(total + reserve);
    }

    /**
     * @dev Grants `DEFAULT_ADMIN_ROLE` and `PAUSER_ROLE` to the
     * account that deploys the contract.
     *
     * Token URIs will be autogenerated based on `baseURI` and their token IDs.
     * See {ERC721-tokenURI}.
     */
    constructor(
        BullrunBabesOracleI _oracle,
        BullrunBabesToken _token,
        address[] memory _splitAddresses,
        uint8[] memory _splitPercentage
    ) public {
        require(address(_oracle) != address(0));
        require(address(_token) != address(0));
        require(_splitAddresses.length == _splitPercentage.length);

        uint8 sum = 0;
        for (uint8 i = 0; i < _splitPercentage.length; i++) {
            sum += _splitPercentage[i];
        }

        require(sum == 100);

        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());

        _setupRole(OWNER_ROLE, _msgSender());
        _setupRole(PAUSER_ROLE, _msgSender());
        _setupRole(ORACLE_ROLE, _msgSender());

        oracle = _oracle;
        brbToken = _token;
        splitAddresses = _splitAddresses;
        splitPercentage = _splitPercentage;
    }

    function getOracleGasFee() public view override returns (uint256, uint256) {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        return (oracleGas, oracleFee);
    }

    function setOracleGasFee(uint256 _gas, uint256 _fee) public override {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        oracleGas = _gas;
        oracleFee = _fee;
    }

    function initCardTypes(
        string[] memory _typeCids,
        uint256[] memory _typeTiers
    ) public {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        require(_typeCids.length == _typeTiers.length);
        // incoming types MUST be higher or same
        // already checked that _typeCids and _typeTiers are of same length
        require(_typeCids.length >= typeCount);

        // loop through existing to ensure that they are the same
        for (uint256 i = 0; i < typeCount; i++) {
            require(_types[i].typeId == i);
            require(
                keccak256(bytes(_types[i].cid)) ==
                    keccak256(bytes(_typeCids[i]))
            );
            require(_types[i].tier == _typeTiers[i]);
        }
        // copy over new values
        for (uint256 i = typeCount; i < _typeCids.length; i++) {
            _types[i] = CardType({
                typeId: i,
                cid: _typeCids[i],
                tier: _typeTiers[i],
                serialCounter: Counters.Counter({_value: 0})
            });
        }
        // set new typeCount
        typeCount = _typeCids.length;
    }

    function initAllocation(
        uint256[] memory _tradeUpThresholds,
        uint256[][] memory _combinedPoolAllocation,
        uint256[][] memory _curveVars
    ) public {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        require(
            _tradeUpThresholds.length == _combinedPoolAllocation.length - 1
        );
        // combined pools are by [drawTier, ...Tiers][type] = count
        // require same or mor tiers
        require(
            _combinedPoolAllocation.length >= combinedPoolAllocation.length
        );

        // set tradeUpThreshold
        tradeUpThresholds = _tradeUpThresholds;

        // verify existing is the same
        for (uint256 tier = 0; tier < combinedPoolAllocation.length; tier++) {
            require(
                _combinedPoolAllocation[tier].length >=
                    combinedPoolAllocation[tier].length
            );
            for (uint256 c = 0; c < combinedPoolAllocation[tier].length; c++) {
                // any existing allocated values CANNOT be changed
                require(_combinedPoolAllocation[tier][c] == 0);
                // ensure that we use the existing allocation values
                _combinedPoolAllocation[tier][c] = combinedPoolAllocation[tier][
                    c
                ];
            }
        }
        combinedPoolAllocation = _combinedPoolAllocation;

        _calculatePoolMax();

        // same size as tiers
        uint256[] memory newReserves =
            new uint256[](_combinedPoolAllocation.length);
        for (uint256 i = 0; i < _inflightReserves.length; i++) {
            newReserves[i] = _inflightReserves[i];
        }
        _inflightReserves = newReserves;

        // no need to initialize _tokenIdTracker since it is always pre-initiated to 0
        // and we never want to reinitialize it

        // set curveVars
        curveVars = _curveVars;
    }

    function cancelRandom(bytes32 _queryId)
        public
        payable
        override
        nonReentrant
    {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        if (_inflightDraws[_queryId].recipient != address(0)) {
            uint256 drawCost = _inflightDraws[_queryId].drawCost;

            // user wanted to draw
            _inflightReserves[0]--;

            // refund draw cost
            Address.sendValue(
                payable(_inflightDraws[_queryId].recipient),
                drawCost.sub(oracleFee)
            );
            _inflightDraws[_queryId].recipient = address(0);
        } else if (_inflightTrades[_queryId].recipient != address(0)) {
            uint256 tradeCost = _inflightTrades[_queryId].tradeCost;
            // user wanted to trade
            _inflightReserves[_inflightTrades[_queryId].newTier + 1]--;
            for (
                uint256 i = 0;
                i < _inflightTrades[_queryId].tokens.length;
                i++
            ) {
                _inflightTradeUpTokens[_inflightTrades[_queryId].tokens[i]] = 2;
            }
            // refund trade cost
            Address.sendValue(
                payable(_inflightTrades[_queryId].recipient),
                tradeCost.sub(oracleFee)
            );
            _inflightTrades[_queryId].recipient = address(0);
        } else {
            revert("No outstanding queries for ID");
        }
    }

    function checkInflight(bytes32[] memory _queryIds)
        external
        view
        override
        returns (bytes32[] memory)
    {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");

        for (uint256 i = 0; i < _queryIds.length; i++) {
            if (
                _inflightDraws[_queryIds[i]].recipient == address(0) &&
                _inflightTrades[_queryIds[i]].recipient == address(0)
            ) {
                _queryIds[i] = 0;
            }
        }
        return _queryIds;
    }

    function inflightReserves()
        public
        view
        override
        returns (uint256[] memory)
    {
        require(hasRole(OWNER_ROLE, _msgSender()), "Not Owner");
        return _inflightReserves;
    }

    function handleRandom(uint256 randomNumber, bytes32 _queryId)
        public
        payable
        nonReentrant
    {
        require(hasRole(ORACLE_ROLE, _msgSender()), "Not Oracle");
        emit RandomReceived(_queryId);

        if (_inflightDraws[_queryId].recipient != address(0)) {
            // user wanted to draw
            _inflightReserves[0]--;
            uint256 poolMax = _getPoolMax(0);
            uint256 randIdx = randomNumber % poolMax;
            uint256 selectedType = _allocatePool(0, randIdx);
            _mintWithType(
                selectedType,
                _inflightDraws[_queryId].recipient,
                _queryId,
                _inflightDraws[_queryId].drawCost
            );
        } else if (_inflightTrades[_queryId].recipient != address(0)) {
            // user wanted to trade
            _inflightReserves[_inflightTrades[_queryId].newTier + 1]--;
            uint256 poolMax =
                _getPoolMax(_inflightTrades[_queryId].newTier + 1);
            uint256 randIdx = randomNumber % poolMax;
            for (
                uint256 i = 0;
                i < _inflightTrades[_queryId].tokens.length;
                i++
            ) {
                _inflightTradeUpTokens[_inflightTrades[_queryId].tokens[i]] = 2;
                _tokensForSale.remove(_inflightTrades[_queryId].tokens[i]);
                brbToken.burn(_inflightTrades[_queryId].tokens[i]);
            }

            uint256 selectedType =
                _allocatePool(_inflightTrades[_queryId].newTier + 1, randIdx);

            _mintWithType(
                selectedType,
                _inflightTrades[_queryId].recipient,
                _queryId,
                _inflightTrades[_queryId].tradeCost
            );
        } else {
            revert("invalid queryId");
        }
    }

    function tradeUp(uint256[] memory tokens)
        public
        payable
        override
        nonReentrant
    {
        uint256 tradeCost = getPrice(0);
        require(msg.value >= tradeCost, "Not enough ether to trade");
        uint256 excess = msg.value.sub(tradeCost);
        if (excess > 0) {
            Address.sendValue(payable(_msgSender()), excess);
        }

        require(tokens.length > 0, "Requires tokens to trade");
        uint256 firstTier = tierOf(tokens[0]);
        uint256 cardsRequiredForTradeUp = tradeUpThresholds[firstTier];
        require(
            tokens.length == cardsRequiredForTradeUp,
            "Selected set does not match trade requirement."
        );
        address firstOwner = brbToken.ownerOf(tokens[0]);
        require(firstOwner == _msgSender(), "Tokens must all be owned by you.");
        bool tokensTiersMatches = true;
        bool tokensOwnerMatches = true;
        bool tokensNotInflight = true;
        for (uint256 i = 1; i < tokens.length; i++) {
            tokensTiersMatches = firstTier == tierOf(tokens[i]);
            tokensOwnerMatches = firstOwner == brbToken.ownerOf(tokens[i]);
            tokensNotInflight = _inflightTradeUpTokens[tokens[i]] != 1;
        }
        require(tokensNotInflight, "All tokens must not be in flight");
        require(
            tokensTiersMatches,
            "All tokens provided must be of the same tier"
        );
        require(tokensOwnerMatches, "Tokens must all be owned by you.");
        uint256 newTier = firstTier + 1;
        require(
            newTier < tradeUpThresholds.length, // new tier still exists within the byte array
            "There must higher tiers to trade up to"
        );

        require(
            _getPoolMax(newTier + 1) - _inflightReserves[newTier + 1] > 0,
            "Exhausted"
        );

        // reserve tokens
        for (uint256 i = 0; i < tokens.length; i++) {
            _inflightTradeUpTokens[tokens[i]] = 1;
        }
        // reserve newTier tokens
        _inflightReserves[newTier + 1]++;

        bytes32 queryId =
            oracle._init_random{value: oracleFee, gas: oracleGas}();

        _inflightTrades[queryId] = TradeUp({
            tokens: tokens,
            recipient: _msgSender(),
            newTier: newTier,
            tradeCost: tradeCost
        });
        emit RandomInitiated(queryId);
    }

    function draw() public payable override nonReentrant {
        require(_getPoolMax(0) - _inflightReserves[0] > 0, "Exhausted");
        uint256 drawCost = getPrice(0);
        require(msg.value >= drawCost, "Not enough ether to draw");
        uint256 excess = msg.value.sub(drawCost);
        if (excess > 0) {
            Address.sendValue(payable(_msgSender()), excess);
        }
        _inflightReserves[0]++;

        bytes32 queryId =
            oracle._init_random{value: oracleFee, gas: oracleGas}();

        _inflightDraws[queryId] = Draw({
            recipient: _msgSender(),
            drawCost: drawCost
        });
        emit RandomInitiated(queryId);
    }

    /* Private allocation functions */
    function _calculatePoolMax() private {
        uint256[] memory poolMax = new uint256[](combinedPoolAllocation.length);
        for (uint256 i = 0; i < combinedPoolAllocation.length; i++) {
            uint256 max = 0;
            for (uint256 j = 0; j < combinedPoolAllocation[i].length; j++) {
                max += combinedPoolAllocation[i][j];
            }
            poolMax[i] = max;
        }
        _poolMax = poolMax;
    }

    function _getPoolMax(uint256 tierOrDrawIdx) private view returns (uint256) {
        return _poolMax[tierOrDrawIdx];
    }

    function _allocatePool(uint256 tierOrDrawIdx, uint256 idx)
        private
        returns (uint256)
    {
        uint256 curIdx = 0;
        for (
            uint256 i = 0;
            i < combinedPoolAllocation[tierOrDrawIdx].length;
            i++
        ) {
            if (idx < curIdx + combinedPoolAllocation[tierOrDrawIdx][i]) {
                combinedPoolAllocation[tierOrDrawIdx][i]--;
                _poolMax[tierOrDrawIdx]--;
                return i;
            }
            curIdx += combinedPoolAllocation[tierOrDrawIdx][i];
        }
        revert("Exhausted");
    }

    function _mintWithType(
        uint256 cardTypeId,
        address recipient,
        bytes32 queryId,
        uint256 prevPrice
    ) private {
        require(bytes(_types[cardTypeId].cid).length != 0, "Invalid type");
        require(recipient != address(this), "Contract cannot own cards");

        Counters.Counter storage serialCounter =
            _types[cardTypeId].serialCounter;
        uint256 curId = _tokenIdTracker.current();
        brbToken.mint(recipient, curId);
        _cards[curId] = Card({
            typeId: cardTypeId,
            price: 0,
            serial: serialCounter.current(),
            prevPrice: prevPrice
        });
        _tokenIdTracker.increment();
        serialCounter.increment();

        emit CardAllocated(
            recipient,
            curId,
            _cards[curId].serial,
            cardTypeId,
            _types[cardTypeId].tier,
            _types[cardTypeId].cid,
            queryId
        );
    }

    /* PUBLIC ADMIN ONLY */
    function withdraw() public payable override nonReentrant {
        require(
            hasRole(OWNER_ROLE, _msgSender()),
            "must have owner role to withdraw"
        );
        uint256 remainder = (address(this).balance);
        uint256 division = remainder.div(100);
        for (uint8 i = 0; i < splitAddresses.length; i++) {
            // if at last address send the remainder
            if (i == splitAddresses.length - 1) {
                Address.sendValue(payable(splitAddresses[i]), remainder);
            } else {
                uint256 alloc = division.mul(splitPercentage[i]);
                Address.sendValue(payable(splitAddresses[i]), alloc);
                remainder -= alloc;
            }
        }
    }

    /* Public Getters */
    function getAllocations()
        public
        view
        override
        returns (uint256[][] memory)
    {
        return combinedPoolAllocation;
    }

    function getCard(uint256 id)
        public
        view
        override
        returns (CardView memory)
    {
        Card storage card = _cards[id];
        CardType storage cardType = _types[card.typeId];
        uint256 cardTypeCurrentSerial = cardType.serialCounter.current();
        return
            CardView({
                id: id,
                price: card.price,
                prevPrice: card.prevPrice,
                serial: card.serial,
                cid: cardType.cid,
                tier: cardType.tier,
                cardTypeId: card.typeId,
                currentSerialForType: cardTypeCurrentSerial
            });
    }

    function tierOfType(uint256 typeId) public view returns (uint256) {
        return _types[typeId].tier;
    }

    function cidOfType(uint256 typeId) public view returns (string memory) {
        return _types[typeId].cid;
    }

    function cardTypeOf(uint256 tokenId) public view returns (uint256) {
        return _cards[tokenId].typeId;
    }

    function cidOf(uint256 tokenId) public view returns (string memory) {
        return cidOfType(cardTypeOf(tokenId));
    }

    function priceOf(uint256 tokenId) public view returns (uint256) {
        return _cards[tokenId].price;
    }

    function serialOf(uint256 tokenId) public view returns (uint256) {
        return _cards[tokenId].serial;
    }

    function tierOf(uint256 tokenId) public view returns (uint256) {
        return tierOfType(cardTypeOf(tokenId));
    }

    function setPrice(uint256 tokenId, uint256 price) public override {
        require(
            brbToken.ownerOf(tokenId) == _msgSender(),
            "Unable to set price if not owned"
        );
        _cards[tokenId].price = price;
        if (price > 0) {
            _tokensForSale.add(tokenId);
        } else {
            _tokensForSale.remove(tokenId);
        }
    }

    function totalSaleSupply() public view override returns (uint256) {
        return _tokensForSale.length();
    }

    function saleTokenByIndex(uint256 index)
        public
        view
        override
        returns (uint256)
    {
        return _tokensForSale.at(index);
    }

    function purchase(uint256 tokenId) public payable override nonReentrant {
        address seller = brbToken.ownerOf(tokenId);
        address buyer = _msgSender();
        uint256 price = priceOf(tokenId);
        uint256 payment = msg.value;

        // do not allow purchasing directly from the contract
        // cards can only be purchased from other people or traded up
        require(seller != address(this));
        require(seller != address(0));
        require(buyer != address(0));
        require(seller != buyer);
        require(price > 0, "Price must be more than 0 to sell");
        require(payment >= price, "Payment must be more than price");

        brbToken.transfer(seller, buyer, tokenId);
        uint256 excess = payment.sub(price);
        uint256 contractCut = price.mul(2).div(100);
        _cards[tokenId].prevPrice = price;
        setPrice(tokenId, 0);

        Address.sendValue(payable(seller), price.sub(contractCut));
        if (excess > 0) {
            Address.sendValue(payable(buyer), excess);
        }
    }
}
function getAllocations()
    public
    view
    override
    returns (uint256[][] memory)
{
    return combinedPoolAllocation;
}

/* Public Getters */