pragma solidity ^0.4.24;

contract InterbetCore {

    /* Global constants */
    uint constant oddsDecimals = 2; // Max. decimal places of odds
    uint constant feeRateDecimals = 1; // Max. decimal places of fee rate

    uint public minMakerBetFund = 100 * 1 finney; // Minimum fund of a maker bet

    uint public maxAllowedTakerBetsPerMakerBet = 100; // Limit the number of taker-bets in 1 maker-bet
    uint public minAllowedStakeInPercentage = 1; // 100 ÷ maxAllowedTakerBetsPerMakerBet

    /* Owner and admins */
    address private owner;
    mapping(address => bool) private admins;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function changeOwner(address newOwner) external onlyOwner {
        owner = newOwner;
    }

    function addAdmin(address addr) external onlyOwner {
        admins[addr] = true;
    }

    function removeAdmin(address addr) external onlyOwner {
        admins[addr] = false;
    }

    modifier onlyAdmin() {
        require(admins[msg.sender] == true);
        _;
    }

    function changeMinMakerBetFund(uint weis) external onlyAdmin {
        minMakerBetFund = mul(weis, 1 wei);
    }

    function changeTakerBetConstraints(uint maxCount, uint minPercentage) external onlyAdmin {
        maxAllowedTakerBetsPerMakerBet = maxCount;
        minAllowedStakeInPercentage = minPercentage;
    }

    /* Events */
    event LogUpdateVerifier(address indexed addr, uint oldFeeRate, uint newFeeRate, uint oldBaseFee, uint newBaseFee);
    event LogMakeBet(uint indexed makerBetId, address indexed maker);
    event LogAddFund(uint indexed makerBetId, address indexed maker, uint oldTotalFund, uint newTotalFund);
    event LogUpdateOdds(uint indexed makerBetId, address indexed maker, uint oldOdds, uint newOdds);
    event LogPauseBet(uint indexed makerBetId, address indexed maker);
    event LogReopenBet(uint indexed makerBetId, address indexed maker);
    event LogCloseBet(uint indexed makerBetId, address indexed maker);
    event LogTakeBet(uint indexed makerBetId, address indexed maker, uint indexed takerBetId, address taker);
    event LogSettleBet(uint indexed makerBetId, address indexed maker);
    event LogWithdraw(uint indexed makerBetId, address indexed maker, address indexed addr);

    /* Betting Core */
    enum BetStatus {
        Open, 
        Paused, 
        Closed, 
        Settled
    }

    enum BetOutcome {
        NotSettled,
        MakerWin,
        TakerWin,
        Draw,
        Canceled
    }

    struct MakerBet {
        uint makerBetId;
        address maker;
        uint odds;
        uint totalFund;
        Verifier trustedVerifier;
        uint expiry;
        BetStatus status;
        uint reservedFund;
        uint takerBetsCount;
        uint totalStake;
        TakerBet[] takerBets;
        BetOutcome outcome;
        bool makerFundWithdrawn;
        bool trustedVerifierFeeSent;
    }

    struct TakerBet {
        uint takerBetId;
        address taker;
        uint odds;
        uint stake;
        bool settled;
    }

    struct Verifier {
        address addr;
        uint feeRate;
        uint baseFee;
    }

    uint public makerBetsCount;
    mapping(uint => mapping(address => MakerBet)) private makerBets;

    mapping(address => Verifier) private verifiers;

    constructor() public {
        owner = msg.sender;
        makerBetsCount = 0;
    }

    function() external payable {
        revert();
    }

    /// Update verifier's data
    function updateVerifier(uint feeRate, uint baseFee) external {
        require(feeRate >= 0 && feeRate <= ((10 ** feeRateDecimals) * 100));
        require(baseFee >= 0 && baseFee <= 100000000 * 1 ether);

        Verifier storage verifier = verifiers[msg.sender];

        uint oldFeeRate = verifier.feeRate;
        uint oldBaseFee = verifier.baseFee;

        verifier.addr = msg.sender;
        verifier.feeRate = feeRate;
        verifier.baseFee = baseFee;

        emit LogUpdateVerifier(msg.sender, oldFeeRate, feeRate, oldBaseFee, baseFee);
    }

    /// Make a bet
    function makeBet(uint makerBetId, uint odds, address trustedVerifier, uint trustedVerifierFeeRate, uint trustedVerifierBaseFee, uint expiry) external payable {
        require(odds > (10 ** oddsDecimals) && odds < ((10 ** 8) * (10 ** oddsDecimals)));
        require(expiry > now);

        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];

        require(makerBet.makerBetId == 0);

        Verifier memory verifier = verifiers[trustedVerifier];

        require(verifier.addr != address(0x0));
        require(trustedVerifierFeeRate == verifier.feeRate);
        require(trustedVerifierBaseFee == verifier.baseFee);

        uint fund = sub(msg.value, trustedVerifierBaseFee);
        require(fund >= minMakerBetFund);

        makerBet.makerBetId = makerBetId;
        makerBet.maker = msg.sender;
        makerBet.odds = odds;
        makerBet.totalFund = fund;
        makerBet.trustedVerifier = Verifier(verifier.addr, verifier.feeRate, verifier.baseFee);
        makerBet.expiry = expiry;
        makerBet.status = BetStatus.Open;
        makerBet.reservedFund = 0;
        makerBet.takerBetsCount = 0;
        makerBet.totalStake = 0;

        makerBetsCount++;

        emit LogMakeBet(makerBetId, msg.sender);
    }

    /// Increase total fund of a bet
    function addFund(uint makerBetId) external payable {
        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];
        require(makerBet.makerBetId != 0);

        require(now < makerBet.expiry);

        require(makerBet.status == BetStatus.Open || makerBet.status == BetStatus.Paused);

        require(msg.sender == makerBet.maker);

        require(msg.value > 0);

        uint oldTotalFund = makerBet.totalFund;

        makerBet.totalFund = add(makerBet.totalFund, msg.value);

        emit LogAddFund(makerBetId, msg.sender, oldTotalFund, makerBet.totalFund);
    }

    /// Update odds of a bet
    function updateOdds(uint makerBetId, uint odds) external {
        require(odds > (10 ** oddsDecimals) && odds < ((10 ** 8) * (10 ** oddsDecimals)));

        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];
        require(makerBet.makerBetId != 0);

        require(now < makerBet.expiry);

        require(makerBet.status == BetStatus.Open || makerBet.status == BetStatus.Paused);

        require(msg.sender == makerBet.maker);

        require(odds != makerBet.odds);

        uint oldOdds = makerBet.odds;

        makerBet.odds = odds;

        emit LogUpdateOdds(makerBetId, msg.sender, oldOdds, makerBet.odds);
    }

    /// Pause a bet
    function pauseBet(uint makerBetId) external {
        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];
        require(makerBet.makerBetId != 0);

        require(makerBet.status == BetStatus.Open);

        require(msg.sender == makerBet.maker);

        makerBet.status = BetStatus.Paused;

        emit LogPauseBet(makerBetId, msg.sender);
    }

    /// Reopen a bet
    function reopenBet(uint makerBetId) external {
        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];
        require(makerBet.makerBetId != 0);

        require(makerBet.status == BetStatus.Paused);

        require(msg.sender == makerBet.maker);

        makerBet.status = BetStatus.Open;

        emit LogReopenBet(makerBetId, msg.sender);
    }

    /// Close a bet and withdraw unused fund
    function closeBet(uint makerBetId) external {
        MakerBet storage makerBet = makerBets[makerBetId][msg.sender];
        require(makerBet.makerBetId != 0);

        require(makerBet.status == BetStatus.Open || makerBet.status == BetStatus.Paused);

        require(msg.sender == makerBet.maker);

        makerBet.status = BetStatus.Closed;

        // refund unused fund to maker
        uint unusedFund = sub(makerBet.totalFund, makerBet.reservedFund);

        if (unusedFund > 0) {
            makerBet.totalFund = makerBet.reservedFund;

            uint refundAmount = unusedFund;
            if (makerBet.totalStake == 0) {
                refundAmount = add(refundAmount, makerBet.trustedVerifier.baseFee); // Refund base verifier fee too if no taker-bets, because verifier do not need to settle the bet with no takers
                makerBet.makerFundWithdrawn = true;
            }

            if (!makerBet.maker.send(refundAmount)) {
                makerBet.totalFund = add(makerBet.totalFund, unusedFund);
                makerBet.status = BetStatus.Paused;
                makerBet.makerFundWithdrawn = false;
            } else {
                emit LogCloseBet(makerBetId, msg.sender);
            }
        } else {
            emit LogCloseBet(makerBetId, msg.sender);
        }
    }

    /// Take a bet
    function takeBet(uint makerBetId, address maker, uint odds, uint takerBetId) external payable {
        require(msg.sender != maker);

        require(msg.value > 0);

        MakerBet storage makerBet = makerBets[makerBetId][maker];
        require(makerBet.makerBetId != 0);

        require(msg.sender != makerBet.trustedVerifier.addr);

        require(now < makerBet.expiry);

        require(makerBet.status == BetStatus.Open);

        require(makerBet.odds == odds);

        // Avoid too many taker-bets in one maker-bet
        require(makerBet.takerBetsCount < maxAllowedTakerBetsPerMakerBet);

        // Avoid too many tiny bets
        uint minAllowedStake = mul(mul(makerBet.totalFund, (10 ** oddsDecimals)), minAllowedStakeInPercentage) / sub(odds, (10 ** oddsDecimals)) / 100;
        uint maxAvailableStake = mul(sub(makerBet.totalFund, makerBet.reservedFund), (10 ** oddsDecimals)) / sub(odds, (10 ** oddsDecimals));
        if (maxAvailableStake >= minAllowedStake) {
            require(msg.value >= minAllowedStake);
        } else {
            require(msg.value >= sub(maxAvailableStake, (maxAvailableStake / 10)) && msg.value <= maxAvailableStake);
        }

        // If remaining fund is not enough, send the money back.
        require(msg.value <= maxAvailableStake);

        makerBet.takerBets.length++;
        makerBet.takerBets[makerBet.takerBetsCount] = TakerBet(takerBetId, msg.sender, odds, msg.value, false);
        makerBet.reservedFund = add(makerBet.reservedFund, mul(msg.value, sub(odds, (10 ** oddsDecimals))) / (10 ** oddsDecimals));   
        makerBet.totalStake = add(makerBet.totalStake, msg.value);
        makerBet.takerBetsCount++;

        emit LogTakeBet(makerBetId, maker, takerBetId, msg.sender);
    }

    /// Settle a bet by trusted verifier
    function settleBet(uint makerBetId, address maker, uint outcome) external {
        require(outcome == 1 || outcome == 2 || outcome == 3 || outcome == 4);

        MakerBet storage makerBet = makerBets[makerBetId][maker];
        require(makerBet.makerBetId != 0);

        require(msg.sender == makerBet.trustedVerifier.addr);

        require(makerBet.totalStake > 0);

        require(makerBet.status != BetStatus.Settled);

        BetOutcome betOutcome = BetOutcome(outcome);
        makerBet.outcome = betOutcome;
        makerBet.status = BetStatus.Settled;

        payMaker(makerBet);
        payVerifier(makerBet);

        emit LogSettleBet(makerBetId, maker);
    }

    /// Manual withdraw fund from a bet after outcome is set
    function withdraw(uint makerBetId, address maker) external {
        MakerBet storage makerBet = makerBets[makerBetId][maker];
        require(makerBet.makerBetId != 0);

        require(makerBet.outcome != BetOutcome.NotSettled);

        require(makerBet.status == BetStatus.Settled);

        bool fullyWithdrawn = false;

        if (msg.sender == maker) {
            fullyWithdrawn = payMaker(makerBet);
        } else if (msg.sender == makerBet.trustedVerifier.addr) {
            fullyWithdrawn = payVerifier(makerBet);
        } else {
            fullyWithdrawn = payTaker(makerBet, msg.sender);
        }

        if (fullyWithdrawn) {
            emit LogWithdraw(makerBetId, maker, msg.sender);
        }
    }

    /// Payout to maker
    function payMaker(MakerBet storage makerBet) private returns (bool fullyWithdrawn) {
        fullyWithdrawn = false;

        if (!makerBet.makerFundWithdrawn) {
            makerBet.makerFundWithdrawn = true;

            uint payout = 0;
            if (makerBet.outcome == BetOutcome.MakerWin) {
                uint trustedVerifierFeeMakerWin = mul(makerBet.totalStake, makerBet.trustedVerifier.feeRate) / ((10 ** feeRateDecimals) * 100);
                payout = sub(add(makerBet.totalFund, makerBet.totalStake), trustedVerifierFeeMakerWin);
            } else if (makerBet.outcome == BetOutcome.TakerWin) {
                payout = sub(makerBet.totalFund, makerBet.reservedFund);
            } else if (makerBet.outcome == BetOutcome.Draw || makerBet.outcome == BetOutcome.Canceled) {
                payout = makerBet.totalFund;
            }

            if (payout > 0) {
                fullyWithdrawn = true;

                if (!makerBet.maker.send(payout)) {
                    makerBet.makerFundWithdrawn = false;
                    fullyWithdrawn = false;
                }
            }
        }

        return fullyWithdrawn;
    }

    /// Payout to taker
    function payTaker(MakerBet storage makerBet, address taker) private returns (bool fullyWithdrawn) {
        fullyWithdrawn = false;

        uint payout = 0;

        for (uint betIndex = 0; betIndex < makerBet.takerBetsCount; betIndex++) {
            if (makerBet.takerBets[betIndex].taker == taker) {
                if (!makerBet.takerBets[betIndex].settled) {
                    makerBet.takerBets[betIndex].settled = true;

                    if (makerBet.outcome == BetOutcome.MakerWin) {
                        continue;
                    } else if (makerBet.outcome == BetOutcome.TakerWin) {
                        uint netProfit = mul(mul(makerBet.takerBets[betIndex].stake, sub(makerBet.takerBets[betIndex].odds, (10 ** oddsDecimals))), sub(((10 ** feeRateDecimals) * 100), makerBet.trustedVerifier.feeRate)) / (10 ** oddsDecimals) / ((10 ** feeRateDecimals) * 100);
                        payout = add(payout, add(makerBet.takerBets[betIndex].stake, netProfit));
                    } else if (makerBet.outcome == BetOutcome.Draw || makerBet.outcome == BetOutcome.Canceled) {
                        payout = add(payout, makerBet.takerBets[betIndex].stake);
                    }
                }
            }
        }

        if (payout > 0) {
            fullyWithdrawn = true;

            if (!taker.send(payout)) {
                fullyWithdrawn = false;

                for (uint betIndex2 = 0; betIndex2 < makerBet.takerBetsCount; betIndex2++) {
                    if (makerBet.takerBets[betIndex2].taker == taker) {
                        if (makerBet.takerBets[betIndex2].settled) {
                            makerBet.takerBets[betIndex2].settled = false;
                        }
                    }
                }
            }
        }

        return fullyWithdrawn;
    }

    /// Payout to verifier
    function payVerifier(MakerBet storage makerBet) private returns (bool fullyWithdrawn) {
        fullyWithdrawn = false;

        if (!makerBet.trustedVerifierFeeSent) {
            makerBet.trustedVerifierFeeSent = true;

            uint payout = 0;
            if (makerBet.outcome == BetOutcome.MakerWin) {
                uint trustedVerifierFeeMakerWin = mul(makerBet.totalStake, makerBet.trustedVerifier.feeRate) / ((10 ** feeRateDecimals) * 100);
                payout = add(makerBet.trustedVerifier.baseFee, trustedVerifierFeeMakerWin);
            } else if (makerBet.outcome == BetOutcome.TakerWin) {
                uint trustedVerifierFeeTakerWin = mul(makerBet.reservedFund, makerBet.trustedVerifier.feeRate) / ((10 ** feeRateDecimals) * 100);
                payout = add(makerBet.trustedVerifier.baseFee, trustedVerifierFeeTakerWin);
            } else if (makerBet.outcome == BetOutcome.Draw || makerBet.outcome == BetOutcome.Canceled) {
                payout = makerBet.trustedVerifier.baseFee;
            }

            if (payout > 0) {
                fullyWithdrawn = true;

                if (!makerBet.trustedVerifier.addr.send(payout)) {
                    makerBet.trustedVerifierFeeSent = false;
                    fullyWithdrawn = false;
                }
            }
        }

        return fullyWithdrawn;
    }

    /* External views */
    function getOwner() external view returns(address) {
        return owner;
    }

    function isAdmin(address addr) external view returns(bool) {
        return admins[addr];
    }

    function getVerifier(address addr) external view returns(address, uint, uint) {
        Verifier memory verifier = verifiers[addr];
        return (verifier.addr, verifier.feeRate, verifier.baseFee);
    }

    function getMakerBetBasicInfo(uint makerBetId, address maker) external view returns(uint, address, address, uint, uint, uint) {
        MakerBet memory makerBet = makerBets[makerBetId][maker];
        return (makerBet.makerBetId, makerBet.maker, makerBet.trustedVerifier.addr, makerBet.trustedVerifier.feeRate, makerBet.trustedVerifier.baseFee, makerBet.expiry);
    }

    function getMakerBetDetails(uint makerBetId, address maker) external view returns(uint, BetStatus, uint, uint, uint, uint, uint, BetOutcome, bool, bool) {
        MakerBet memory makerBet = makerBets[makerBetId][maker];
        return (makerBet.makerBetId, makerBet.status, makerBet.odds, makerBet.totalFund, makerBet.reservedFund, makerBet.takerBetsCount, makerBet.totalStake, makerBet.outcome, makerBet.makerFundWithdrawn, makerBet.trustedVerifierFeeSent);
    }

    function getTakerBet(uint makerBetId, address maker, uint takerBetId, address taker) external view returns(uint, address, uint, uint, bool) {
        MakerBet memory makerBet = makerBets[makerBetId][maker];
        for (uint betIndex = 0; betIndex < makerBet.takerBetsCount; betIndex++) {
            TakerBet memory takerBet = makerBet.takerBets[betIndex];

            if (takerBet.takerBetId == takerBetId && takerBet.taker == taker) {
                return (takerBet.takerBetId, takerBet.taker, takerBet.odds, takerBet.stake, takerBet.settled);
            }
        }
    }

    /* Math utilities */
    function mul(uint256 _a, uint256 _b) private pure returns(uint256 c) {
        if (_a == 0) {
            return 0;
        }

        c = _a * _b;
        assert(c / _a == _b);
        return c;
    }

    function sub(uint256 _a, uint256 _b) private pure returns(uint256) {
        assert(_b <= _a);
        return _a - _b;
    }

    function add(uint256 _a, uint256 _b) private pure returns(uint256 c) {
        c = _a + _b;
        assert(c >= _a);
        return c;
    }

}