pragma solidity >=0.0;
import "../Oracles/Oracle.sol";
import "../Events/EventFactory.sol";
import "../Markets/MarketFactory.sol";


/// @title Futarchy oracle contract - Allows to create an oracle based on market behaviour
/// @author Stefan George - <stefan@gnosis.pm>
contract FutarchyOracle is Oracle {
    using Math for *;

    /*
     *  Events
     */
    event FutarchyFunding(uint funding);
    event FutarchyClosing();
    event OutcomeAssignment(uint winningMarketIndex);

    /*
     *  Constants
     */
    uint8 public constant LONG = 1;

    /*
     *  Storage
     */
    address creator;
    Market[] public markets;
    CategoricalEvent public categoricalEvent;
    uint public deadline;
    uint public winningMarketIndex;
    bool public isSet;

    /*
     *  Modifiers
     */
    modifier isCreator () {
        // Only creator is allowed to proceed
        require(msg.sender == creator);
        _;
    }

    /*
     *  Public functions
     */
    /// @dev Constructor creates events and markets for futarchy oracle
    /// @param _creator Oracle creator
    /// @param eventFactory Event factory contract
    /// @param collateralToken Tokens used as collateral in exchange for outcome tokens
    /// @param oracle Oracle contract used to resolve the event
    /// @param outcomeCount Number of event outcomes
    /// @param lowerBound Lower bound for event outcome
    /// @param upperBound Lower bound for event outcome
    /// @param marketFactory Market factory contract
    /// @param marketMaker Market maker contract
    /// @param fee Market fee
    /// @param _deadline Decision deadline
    constructor(
        address _creator,
        EventFactory eventFactory,
        Token collateralToken,
        Oracle oracle,
        uint8 outcomeCount,
        int lowerBound,
        int upperBound,
        MarketFactory marketFactory,
        MarketMaker marketMaker,
        uint24 fee,
        uint _deadline
    )
    {
        // Deadline is in the future
        require(_deadline > block.timestamp);
        // Create decision event
        categoricalEvent = eventFactory.createCategoricalEvent(collateralToken, this, outcomeCount);
        // Create outcome events
        for (uint8 i = 0; i < categoricalEvent.getOutcomeCount(); i++) {
            ScalarEvent scalarEvent = eventFactory.createScalarEvent(
                categoricalEvent.outcomeTokens(i),
                oracle,
                lowerBound,
                upperBound
            );
            markets.push(marketFactory.createMarket(scalarEvent, marketMaker, fee));
        }
        creator = _creator;
        deadline = _deadline;
    }

    /// @dev Funds all markets with equal amount of funding
    /// @param funding Amount of funding
    function fund(uint funding)
        public
        isCreator
    {
        // Buy all outcomes
        require(   categoricalEvent.collateralToken().transferFrom(creator, address(this), funding)
                && categoricalEvent.collateralToken().approve(address(categoricalEvent), funding));
        categoricalEvent.buyAllOutcomes(funding);
        // Fund each market with outcome tokens from categorical event
        for (uint8 i = 0; i < markets.length; i++) {
            Market market = markets[i];
            // Approve funding for market
            require(market.eventContract().collateralToken().approve(address(market), funding));
            market.fund(funding);
        }
        emit FutarchyFunding(funding);
    }

    /// @dev Closes market for winning outcome and redeems winnings and sends all collateral tokens to creator
    function close()
        public
        isCreator
    {
        // Winning outcome has to be set
        Market market = markets[uint(getOutcome())];
        require(categoricalEvent.isOutcomeSet() && market.eventContract().isOutcomeSet());
        // Close market and transfer all outcome tokens from winning outcome to this contract
        market.close();
        market.eventContract().redeemWinnings();
        market.withdrawFees();
        // Redeem collateral token for winning outcome tokens and transfer collateral tokens to creator
        categoricalEvent.redeemWinnings();
        require(categoricalEvent.collateralToken().transfer(creator, categoricalEvent.collateralToken().balanceOf(address(this))));
        emit FutarchyClosing();
    }

    /// @dev Allows to set the oracle outcome based on the market with largest long position
    function setOutcome()
        public
    {
        // Outcome is not set yet and deadline has passed
        require(!isSet && deadline <= block.timestamp);
        // Find market with highest marginal price for long outcome tokens
        uint highestMarginalPrice = markets[0].marketMaker().calcMarginalPrice(markets[0], LONG);
        uint highestIndex = 0;
        for (uint8 i = 1; i < markets.length; i++) {
            uint marginalPrice = markets[i].marketMaker().calcMarginalPrice(markets[i], LONG);
            if (marginalPrice > highestMarginalPrice) {
                highestMarginalPrice = marginalPrice;
                highestIndex = i;
            }
        }
        winningMarketIndex = highestIndex;
        isSet = true;
        emit OutcomeAssignment(winningMarketIndex);
    }

    /// @dev Returns if winning outcome is set
    /// @return Is outcome set?
    function isOutcomeSet()
        public
        override
        view
        returns (bool)
    {
        return isSet;
    }

    /// @dev Returns winning outcome
    /// @return Outcome
    function getOutcome()
        public
        override
        view
        returns (int)
    {
        return int(winningMarketIndex);
    }
}
