contract MarketMaker is Ownable, IERC1155TokenReceiver {
    using SignedSafeMath for int;
    using SafeMath for uint;
    /*
     *  Constants
     */    
    uint64 public constant FEE_RANGE = 10**18;

    /*
     *  Events
     */
    event AMMCreated(uint initialFunding);
    event AMMPaused();
    event AMMResumed();
    event AMMClosed();
    event AMMFundingChanged(int fundingChange);
    event AMMFeeChanged(uint64 newFee);
    event AMMFeeWithdrawal(uint fees);
    event AMMOutcomeTokenTrade(address indexed transactor, int[] outcomeTokenAmounts, int outcomeTokenNetCost, uint marketFees);
    
    /*
     *  Storage
     */
    PredictionMarketSystem public pmSystem;
    IERC20 public collateralToken;
    bytes32[] public conditionIds;
    uint public atomicOutcomeSlotCount;
    uint64 public fee;
    uint public funding;
    Stage public stage;
    enum Stage {
        Running,
        Paused,
        Closed
    }

    /*
     *  Modifiers
     */
    modifier atStage(Stage _stage) {
        // Contract has to be in given stage
        require(stage == _stage);
        _;
    }

    constructor(PredictionMarketSystem _pmSystem, IERC20 _collateralToken, bytes32[] memory _conditionIds, uint64 _fee, uint initialFunding, address marketOwner)
        public
    {
        // Validate inputs
        require(address(_pmSystem) != address(0) && _fee < FEE_RANGE);
        pmSystem = _pmSystem;
        collateralToken = _collateralToken;
        conditionIds = _conditionIds;
        fee = _fee;

        atomicOutcomeSlotCount = 1;
        for (uint i = 0; i < conditionIds.length; i++) {
            atomicOutcomeSlotCount *= pmSystem.getOutcomeSlotCount(conditionIds[i]);
        }
        require(atomicOutcomeSlotCount > 1, "conditions must be valid");

        require(collateralToken.transferFrom(marketOwner, address(this), initialFunding) && collateralToken.approve(address(pmSystem), initialFunding));

        splitPositionThroughAllConditions(initialFunding, conditionIds.length, 0);

        funding = initialFunding;

        stage = Stage.Running;
        emit AMMCreated(funding);
    }

    function calcNetCost(int[] memory outcomeTokenAmounts) public view returns (int netCost);

    /// @dev Allows to fund the market with collateral tokens converting them into outcome tokens
    /// Note for the future: should combine splitPosition and mergePositions into one function, as code duplication causes things like this to happen.
    function changeFunding(int fundingChange)
        public
        onlyOwner
        atStage(Stage.Paused)
    {
        require(fundingChange != 0, "A fundingChange of zero is not a fundingChange at all. It is unacceptable.");
        // Either add or subtract funding based off whether the fundingChange parameter is negative or positive
        if (fundingChange > 0) {
            require(collateralToken.transferFrom(msg.sender, address(this), uint(fundingChange)) && collateralToken.approve(address(pmSystem), uint(fundingChange)));
            splitPositionThroughAllConditions(uint(fundingChange), conditionIds.length, 0);
            funding = funding.add(uint(fundingChange));
            emit AMMFundingChanged(fundingChange);
        }
        if (fundingChange < 0) {
            mergePositionsThroughAllConditions(uint(-fundingChange), conditionIds.length, 0);
            funding = funding.sub(uint(-fundingChange));
            require(collateralToken.transfer(owner(), uint(-fundingChange)));
            emit AMMFundingChanged(fundingChange);
        }
    }

    function pause() public onlyOwner atStage(Stage.Running) {
        stage = Stage.Paused;
        emit AMMPaused();
    }
    
    function resume() public onlyOwner atStage(Stage.Paused) {
        stage = Stage.Running;
        emit AMMResumed();
    }

    function changeFee(uint64 _fee) public onlyOwner atStage(Stage.Paused) {
        fee = _fee;
        emit AMMFeeChanged(fee);
    }

    /// @dev Allows market owner to close the markets by transferring all remaining outcome tokens to the owner
    function close()
        public
        onlyOwner
    {
        require(stage == Stage.Running || stage == Stage.Paused, "This Market has already been closed");
        for (uint i = 0; i < atomicOutcomeSlotCount; i++) {
            uint positionId = generateAtomicPositionId(i);
            pmSystem.safeTransferFrom(address(this), owner(), positionId, pmSystem.balanceOf(address(this), positionId), "");
        }
        stage = Stage.Closed;
        emit AMMClosed();
    }

    /// @dev Allows market owner to withdraw fees generated by trades
    /// @return Fee amount
    function withdrawFees()
        public
        onlyOwner
        returns (uint fees)
    {
        fees = collateralToken.balanceOf(address(this));
        // Transfer fees
        require(collateralToken.transfer(owner(), fees));
        emit AMMFeeWithdrawal(fees);
    }

    /// @dev Allows to trade outcome tokens and collateral with the market maker
    /// @param outcomeTokenAmounts Amounts of each atomic outcome token to buy or sell. If positive, will buy this amount of outcome token from the market. If negative, will sell this amount back to the market instead. The indices of this array range from 0 to product(all conditions' outcomeSlotCounts)-1. For example, with two conditions with three outcome slots each and one condition with two outcome slots, you will have 3*3*2=18 total atomic outcome tokens, and the indices will range from 0 to 17. The indices map to atomic outcome slots depending on the order of the conditionIds. Let's say the first condition has slots A, B, C the second has slots X, Y, and the third has slots I, J, K. We can associate each atomic outcome token with indices by this map:
    /// A&X&I == 0
    /// B&X&I == 1
    /// C&X&I == 2
    /// A&Y&I == 3
    /// B&Y&I == 4
    /// C&Y&I == 5
    /// A&X&J == 6
    /// B&X&J == 7
    /// C&X&J == 8
    /// A&Y&J == 9
    /// B&Y&J == 10
    /// C&Y&J == 11
    /// A&X&K == 12
    /// B&X&K == 13
    /// C&X&K == 14
    /// A&Y&K == 15
    /// B&Y&K == 16
    /// C&Y&K == 17
    /// This order is calculated via the generateAtomicPositionId function below: C&Y&I -> (2, 1, 0) -> 2 + 3 * (1 + 2 * (0 + 3 * (0 + 0)))
    /// @param collateralLimit If positive, this is the limit for the amount of collateral tokens which will be sent to the market to conduct the trade. If negative, this is the minimum amount of collateral tokens which will be received from the market for the trade. If zero, there is no limit.
    /// @return If positive, the amount of collateral sent to the market. If negative, the amount of collateral received from the market. If zero, no collateral was sent or received.
    function trade(int[] memory outcomeTokenAmounts, int collateralLimit)
        public
        atStage(Stage.Running)
        returns (int netCost)
    {
        require(outcomeTokenAmounts.length == atomicOutcomeSlotCount);

        // Calculate net cost for executing trade
        int outcomeTokenNetCost = calcNetCost(outcomeTokenAmounts);
        int fees;
        if(outcomeTokenNetCost < 0)
            fees = int(calcMarketFee(uint(-outcomeTokenNetCost)));
        else
            fees = int(calcMarketFee(uint(outcomeTokenNetCost)));

        require(fees >= 0);
        netCost = outcomeTokenNetCost.add(fees);

        require(
            (collateralLimit != 0 && netCost <= collateralLimit) ||
            collateralLimit == 0
        );

        if(outcomeTokenNetCost > 0) {
            require(
                collateralToken.transferFrom(msg.sender, address(this), uint(netCost)) &&
                collateralToken.approve(address(pmSystem), uint(outcomeTokenNetCost))
            );

            splitPositionThroughAllConditions(uint(outcomeTokenNetCost), conditionIds.length, 0);
        }

        for (uint i = 0; i < atomicOutcomeSlotCount; i++) {
            if(outcomeTokenAmounts[i] != 0) {
                uint positionId = generateAtomicPositionId(i);
                if(outcomeTokenAmounts[i] < 0) {
                    pmSystem.safeTransferFrom(msg.sender, address(this), positionId, uint(-outcomeTokenAmounts[i]), "");
                } else {
                    pmSystem.safeTransferFrom(address(this), msg.sender, positionId, uint(outcomeTokenAmounts[i]), "");
                }

            }
        }

        if(outcomeTokenNetCost < 0) {
            // This is safe since
            // 0x8000000000000000000000000000000000000000000000000000000000000000 ==
            // uint(-int(-0x8000000000000000000000000000000000000000000000000000000000000000))
            mergePositionsThroughAllConditions(uint(-outcomeTokenNetCost), conditionIds.length, 0);
            if(netCost < 0) {
                require(collateralToken.transfer(msg.sender, uint(-netCost)));
            }
        }

        emit AMMOutcomeTokenTrade(msg.sender, outcomeTokenAmounts, outcomeTokenNetCost, uint(fees));
    }

    /// @dev Calculates fee to be paid to market maker
    /// @param outcomeTokenCost Cost for buying outcome tokens
    /// @return Fee for trade
    function calcMarketFee(uint outcomeTokenCost)
        public
        view
        returns (uint)
    {
        return outcomeTokenCost * fee / FEE_RANGE;
    }

    function onERC1155Received(address operator, address /*from*/, uint256 /*id*/, uint256 /*value*/, bytes calldata /*data*/) external returns(bytes4) {
        if (operator == address(this)) {
            return 0xf23a6e61;
        }
        return 0x0;
    }

    function onERC1155BatchReceived(address _operator, address /*from*/, uint256[] calldata /*ids*/, uint256[] calldata /*values*/, bytes calldata /*data*/) external returns(bytes4) {
        if (_operator == address(this)) {
            return 0xf23a6e61;
        }
        return 0x0;
    }

    function generateBasicPartition(bytes32 conditionId)
        private
        view
        returns (uint[] memory partition)
    {
        partition = new uint[](pmSystem.getOutcomeSlotCount(conditionId));
        for(uint i = 0; i < partition.length; i++) {
            partition[i] = 1 << i;
        }
    }

    function generateAtomicPositionId(uint i)
        internal
        view
        returns (uint)
    {
        uint collectionId = 0;

        for(uint k = 0; k < conditionIds.length; k++) {
            uint curOutcomeSlotCount = pmSystem.getOutcomeSlotCount(conditionIds[k]);
            collectionId += uint(keccak256(abi.encodePacked(
                conditionIds[k],
                1 << (i % curOutcomeSlotCount))));
            i /= curOutcomeSlotCount;
        }
        return uint(keccak256(abi.encodePacked(
            collateralToken,
            collectionId)));
    }

    function splitPositionThroughAllConditions(uint amount, uint conditionsLeft, uint parentCollectionId)
        private
    {
        if(conditionsLeft == 0) return;
        conditionsLeft--;

        uint[] memory partition = generateBasicPartition(conditionIds[conditionsLeft]);
        pmSystem.splitPosition(collateralToken, bytes32(parentCollectionId), conditionIds[conditionsLeft], partition, amount);
        for(uint i = 0; i < partition.length; i++) {
            splitPositionThroughAllConditions(
                amount,
                conditionsLeft,
                parentCollectionId + uint(keccak256(abi.encodePacked(
                    conditionIds[conditionsLeft],
                    partition[i]))));
        }
    }

    function mergePositionsThroughAllConditions(uint amount, uint conditionsLeft, uint parentCollectionId)
        private
    {
        if(conditionsLeft == 0) return;
        conditionsLeft--;

        uint[] memory partition = generateBasicPartition(conditionIds[conditionsLeft]);
        for(uint i = 0; i < partition.length; i++) {
            mergePositionsThroughAllConditions(
                amount,
                conditionsLeft,
                parentCollectionId + uint(keccak256(abi.encodePacked(
                    conditionIds[conditionsLeft],
                    partition[i]))));
        }
        pmSystem.mergePositions(collateralToken, bytes32(parentCollectionId), conditionIds[conditionsLeft], partition, amount);
    }
}
function withdrawFees()
    public
    onlyOwner
    returns (uint fees)
{
    fees = collateralToken.balanceOf(address(this));
    // Transfer fees
    require(collateralToken.transfer(owner(), fees));
    emit AMMFeeWithdrawal(fees);
}

/// @dev Allows market owner to withdraw fees generated by trades
/// @return Fee amount