contract SpinGame is usingOraclize, SpinGameSettings, GameInterface {
    using SafeMath for uint256;

    enum RoundState { Active, Closed, Finished }
    enum OraclizeCallType { None, CloseRound, FinalizeRound, Used }

    struct Round {
        GameType selectedColor;
        RoundState state;
        bytes32 randomHash;
    }

    struct Bet {
        GameType gameType;
        uint128 amount;
        uint64 round;
        bool won;
        bool finished;
        bool bonus;
    }

    /**
    * @dev lastRound id
    * @dev if it is finished then currently there is no active round
    */
    uint64 public lastRound;
    /**
    * @dev keep total commission earned
    */
    uint public commissionEarned;
    /**
    * @dev keep how many ethers is lost by player
    */
    uint public totalFundsLostByPlayers;
    /**
    * @dev keep track of all funds sent directly to contract
    */
    uint public totalFundsSentByOwner;
    /**
    * @dev Mapping that keep what is type of queryId sent to Oraclize
    */
    mapping(bytes32 => OraclizeCallType) public idToType;
    /**
    * @dev Holds all bets of user with specific address
    */
    mapping(address => Bet[]) public bets;
    /**
    * @dev Array containing all played rounds, round id is its place in array
    */
    Round[] public rounds;

    IBadBitCasino public casino;
    IBadBitDistributor public distributor;

    event RoundStarted(uint indexed id);
    event RoundFinished(uint indexed id, GameType selectedColor);
    event BetPlayed(uint indexed round, address indexed user, GameType gameType, uint amount, uint history, uint gameTypes);
    event BetDenied(uint indexed round, address indexed user);

    /**
    * @dev betting amount can be only between MIN_BET and MAX_BET
    */
    modifier onlyAllowedAmount(uint _amount, GameType _type) {
        require(MIN_BET <= _amount && _amount <= maxBetSize(_type));
        _;
    }

    /**
    * @dev we set network and starting gasPrice
    * @dev create dummy finished round to avoid edge cases
    */
    constructor(address payable _casino, address payable _settings) SpinGameSettings(_settings) public {

        casino = IBadBitCasino(_casino);

        rounds.push(Round({
                selectedColor: GameType.NoColor,
                state: RoundState.Finished,
                randomHash: bytes32(0)
            }));

        oraclize_setNetwork();
        // equals to 1 gwei
        oraclize_setCustomGasPrice(10000000000);
    }

    function setDistributor(address _distributor) public onlyOperator {
        distributor = IBadBitDistributor(_distributor);
    }

    function changeCasinoAddress(address payable _casino) public onlyOperator {
        casino = IBadBitCasino(_casino);
    }

    /**
    * @dev Betting function for all game types that should be played with on-contract bonus balance
    * @param _amount amount that user is betting for this specific bet
    * @param _type represents which game will be played
    */
    function placeBonusBet(uint128 _amount, GameType _type) public
        onlyAllowedAmount(_amount, _type) {
        require(getChance(_type) < settings.MAX_CHANCE_FOR_BONUS_BETTING() * 100);

        _bet(_amount, _type, tx.gasprice + 1000000000, true);
    }

    /**
    * @dev Betting function for all game types that should be played with on-contract balance
    * @param _amount amount that user is betting for this specific bet
    * @param _type represents which game will be played
    */
    function placeBetWithContractBalance(uint128 _amount, GameType _type) public
        onlyAllowedAmount(_amount, _type) {

        _bet(_amount, _type, tx.gasprice + 1000000000, false);
    }

    /**
    * @dev Betting function for all game types that should be played with msg.value
    * @param _amount amount that user is betting for this specific bet
    * @param _type represents which game will be played
    */
    function placeBet(uint128 _amount, GameType _type) public
        onlyAllowedAmount(_amount, _type)
        payable {

        require(msg.value >= _amount);

        require(casino.add.value(msg.value)(msg.sender, _amount));

        _bet(_amount, _type, tx.gasprice + 1000000000, false);
    }

    /**
    * @dev Internal betting function that creates actual bet and starts round if there is no active round
    * @param _amount amount that user is betting for this specific bet
    * @param _type represents which game will be played
    * @param _gasPriceInWei sets gasPrice for oraclize call, should always be a bit higher than current average
    * @param _bonus flag that says is it played with bonus balance
    */
    function _bet(uint128 _amount, GameType _type, uint _gasPriceInWei, bool _bonus) internal {
        require(BETS_ALLOWED);
        require(_type != GameType.NoColor && _type != GameType.Gold);

        // @dev we don't want to revert if gasPrice is not valid, we just set it to MIN or MAX
        if (_gasPriceInWei < settings.MIN_GAS_PRICE()) {
            _gasPriceInWei = settings.MIN_GAS_PRICE();
        } else if (_gasPriceInWei > settings.MAX_GAS_PRICE()) {
            _gasPriceInWei = settings.MAX_GAS_PRICE();
        }

        if (address(this).balance < MINIMUM_BALANCE_THRESHOLD) {
            pullEthFromCasino();
        }

        // @dev if last round is finished we need to start new round
        if (rounds[lastRound].state == RoundState.Finished) {
            oraclize_setCustomGasPrice(_gasPriceInWei);

            bytes32 queryId = oraclize_query(ROUND_TIME, "URL", "", ORACLIZE_GAS_LIMIT_CALL_1);
            idToType[queryId] = OraclizeCallType.CloseRound;

            lastRound++;
            rounds.push(Round({
                    selectedColor: GameType.NoColor,
                    state: RoundState.Active,
                    randomHash: bytes32(0)
                }));

            emit RoundStarted(lastRound);
        }

        // @dev at this point we definitely have non finished round, but it can be closed, so we need to check if its active
        if (rounds[lastRound].state == RoundState.Active) {

            // @dev if this is not your first bet you can't bet twice in same round
            if (bets[msg.sender].length > 0) {
                require(bets[msg.sender][bets[msg.sender].length-1].round != lastRound);
            }

            // this will also update last bets
            casino.placeBet(msg.sender, bets[msg.sender].length, _amount, _bonus);

            // @dev returns history of last (up to 5) bets before pushing new bet
            uint history;
            uint types;
            (history, types) = getUsersBettingHistoryWithTypes(msg.sender);

            bets[msg.sender].push(Bet({
                    gameType: _type,
                    amount: _amount,
                    round: lastRound,
                    won: false,
                    finished: false,
                    bonus: _bonus
                }));

            emit BetPlayed(lastRound, msg.sender, _type, _amount, history, types);
        } else {
            // @dev if round is closed, emit event saying that bet is denied
            emit BetDenied(lastRound, msg.sender);
        }
    }

    function __callback(bytes32 myid, string memory result) public {
        if (msg.sender != oraclize_cbAddress() && !settings.isOperatorAddress(msg.sender)) revert();

        // @dev Oraclize sometimes rebroadcast transactions, so we need to make sure thats not the case
        if (idToType[myid] == OraclizeCallType.Used) {
            return;
        }

        if (idToType[myid] == OraclizeCallType.CloseRound) {
            // In case finalization callback was executed first
            if(rounds[lastRound].state == RoundState.Finished) {
                return;
            }

            bytes32 queryId;

            if(settings.USE_BLOCKHASH_RANDOM_SEED()) {
                queryId = oraclize_query(0, "URL", "", ORACLIZE_GAS_LIMIT_CALL_2);
            } else {
                queryId = oraclize_newRandomDSQuery(0, 8, ORACLIZE_GAS_LIMIT_CALL_2);
            }

            idToType[queryId] = OraclizeCallType.FinalizeRound;
            rounds[lastRound].state = RoundState.Closed;
        } else if (idToType[myid] == OraclizeCallType.None || idToType[myid] == OraclizeCallType.FinalizeRound) {
            require(settings.USE_BLOCKHASH_RANDOM_SEED() || bytes(result).length != 0);
            uint randomNumber;

            if(settings.isOperatorAddress(msg.sender) || settings.USE_BLOCKHASH_RANDOM_SEED()) {
                randomNumber = uint224(uint(blockhash(block.number - 1)) % 42);
            } else {
                randomNumber = uint224(uint(keccak256(abi.encodePacked(result, blockhash(block.number - 1)))) % 42);
            }

            rounds[lastRound].randomHash = keccak256(abi.encodePacked(result));
            rounds[lastRound].selectedColor = getColor(randomNumber);
            rounds[lastRound].state = RoundState.Finished;

            emit RoundFinished(lastRound, rounds[lastRound].selectedColor);
        }

        idToType[myid] = OraclizeCallType.Used;
    }

    function finalizeBet(address _user, uint _betId) public returns(uint profit, uint totalWon) {
        require(msg.sender == address(casino));

        Bet memory betObject = bets[_user][_betId];

        if (rounds[betObject.round].selectedColor == GameType.Gold) {
            distributor.sendTokens(_user, distributor.getStandardLot() * STANDARD_LOT_GOLD_MODIFIER / 100);
        }

        uint[] memory betSizes = new uint[](1);
        betSizes[0] = betObject.amount;
        uint[] memory chances = new uint[](1);
        chances[0] = getChance(betObject.gameType);

        if (distributor.shouldWinTokens(address(this), rounds[betObject.round].randomHash, _user, betSizes, chances, 1)) {
            distributor.winTokens(_user);
        }

        profit = getUserProfitForFinishedBet(_user, _betId);

        if (profit > 0) {
            totalWon = bets[_user][_betId].amount + profit;
            bets[_user][_betId].won = true;
        } else if(!betObject.bonus) {
            // @dev updates global states
            totalFundsLostByPlayers = totalFundsLostByPlayers + bets[_user][_betId].amount;
        }

        bets[_user][_betId].finished = true;
    }

    function pullEthFromCasino() internal {
        uint price = oraclize_getPrice("url", ORACLIZE_GAS_LIMIT_CALL_1);
        price += settings.USE_BLOCKHASH_RANDOM_SEED() ? oraclize_getPrice("url", ORACLIZE_GAS_LIMIT_CALL_2) : oraclize_getPrice("random", ORACLIZE_GAS_LIMIT_CALL_2);

        casino.sendEthToGame(price * ROUND_MULTIPLIER_FOR_ORACLIZE_POOL);
    }

    function canFinalizeBet(address _user, uint _betId) public view returns (bool success) {
        if (rounds[bets[_user][_betId].round].state == RoundState.Finished) {
            return true;
        }

        return false;
    }

    /**
    * @dev Checks for player winning history (up to 5 bets)
    * @param _user represents address of user
    * @return returns history of user in format of uint where each decimal 2 means that bet is won, and 1 means that bet is lost
    */
    function getUsersBettingHistoryWithTypes(address _user) public view returns(uint history, uint types) {
        uint len = bets[_user].length;

        if (len > 0) {
            while (len >= 0) {
                len--;

                if (bets[_user][len].won) {
                    history = history * 10 + 2;
                } else {
                    history = history * 10 + 1;
                }

                types = types * 10 + uint(bets[_user][len].gameType);

                if (len == 0 || history > 10000) {
                    break;
                }
            }
        }
    }

    /**
    * @dev Calculate possible winning with specific chance and amount
    * @param _chance represents chance of winning bet
    * @param _amount represents amount that is played for bet
    * @return returns uint of players profit with specific chance and amount
    */
    function getPossibleWinnings(uint _chance, uint _amount) public view returns(uint) {
        // don chance
        if (_chance == 50) {
            return _amount;
        }

        GameType _type = getTypeFromChance(_chance);

        if (_type == GameType.White) {
            return _amount;
        }

        if (_type == GameType.Red) {
            return _amount * 3;
        }

        if (_type == GameType.Blue) {
            return _amount * 4;
        }

        if (_type == GameType.Orange) {
            return _amount * 19;
        }

        return 0;
    }

    /**
    * @dev check chances for specific game type and number
    * @param _gameType represents type of game
    * @return return value that represents chance of winning with specific number and gameType
    */
    function getChance(GameType _gameType) public pure returns(uint) {
        if (_gameType == GameType.White) {
            return 4760;
        }

        if (_gameType == GameType.Red) {
            return 2380;
        }

        if (_gameType == GameType.Blue) {
            return 1904;
        }

        if (_gameType == GameType.Orange) {
            return 476;
        }
    }

    /**
    * @dev check winning for specific bet for user
    * @param _user represents address of user
    * @param _betId represents id of bet for specific user
    * @return return value that represents users profit, in case round is not finished or bet is lost, returns 0
    */
    function getUserProfitForFinishedBet(address _user, uint _betId) public view returns(uint) {
        Bet memory betObject = bets[_user][_betId];
        GameType selectedColor = rounds[betObject.round].selectedColor;
        uint chance = getChance(betObject.gameType);

        if (rounds[betObject.round].state == RoundState.Finished) {
            if (betObject.gameType == selectedColor) {
                return getPossibleWinnings(chance, betObject.amount);
            }
        }

        return 0;
    }

    function getColor(uint _number) public pure returns(GameType) {
        if (_number < 20) {
            return GameType.White;
        }

        if (_number < 30) {
            return GameType.Red;
        }

        if (_number < 38) {
            return GameType.Blue;
        }

        if (_number < 40) {
            return GameType.Orange;
        }

        if (_number < 42) {
            return GameType.Gold;
        }

        return GameType.NoColor;
    }

    function getTokensWonParameters(address _user) public view returns(bytes32, uint, uint) {
        Bet memory lastBet = bets[_user][getTotalBets(_user) - 1];

        return (rounds[lastBet.round].randomHash, lastBet.amount, getChance(lastBet.gameType));
    }

    function maxBetSize(GameType _type) public view returns(uint) {
        if (USE_DYNAMIC_MAX_BET) {
            if (_type == GameType.White) {
                return BENCHMARK_MAXIMUM_BET_SIZE * settings.ETH_TO_WEI();
            }

            if (_type == GameType.Red) {
                return BENCHMARK_MAXIMUM_BET_SIZE * settings.ETH_TO_WEI() * 50 / 100;
            }

            if (_type == GameType.Blue) {
                return BENCHMARK_MAXIMUM_BET_SIZE * settings.ETH_TO_WEI() * 40 / 100;
            }

            if (_type == GameType.Orange) {
                return BENCHMARK_MAXIMUM_BET_SIZE * settings.ETH_TO_WEI() * 10 / 100;
            }
        } else {
            return MAX_BET[uint8(_type)];
        }
    }

    function getBet(address _user, uint _betId) public view returns(uint round, GameType gameType, uint amount, GameType selectedColor, bool won) {
        Bet memory betObject = bets[_user][_betId];

        return (betObject.round, betObject.gameType, betObject.amount, rounds[betObject.round].selectedColor, getUserProfitForFinishedBet(_user, _betId) > 0);
    }

    function getBetInfo(address _user, uint _betId) public view returns(uint amount, bool finalized, bool won, bool isBonus) {
        Bet memory betObject = bets[_user][_betId];

        return (betObject.amount, betObject.finished, betObject.won, betObject.bonus);
    }

    function getCurrentBalance(address _user) public view returns(uint) {
        return casino.getCurrentBalance(_user);
    }

    function getTypeFromChance(uint _chance) public pure returns(GameType _type) {
        if (_chance == 4760) {
            _type = GameType.White;
        } else if (_chance == 2380) {
            _type = GameType.Red;
        } else if (_chance == 1904) {
            _type = GameType.Blue;
        } else if (_chance == 476) {
            _type = GameType.Orange;
        }
    }

    function getParamsForTokenCaluclation(uint _chance) public view returns(uint minB, uint maxB, uint gbs, uint gwp) {
        minB = MIN_BET;
        maxB = maxBetSize(getTypeFromChance(_chance));
        gbs = GBS;
        gwp = GWP;
    }

    /**
    * @dev Checks for number of bets user played
    * @param _user address of user
    * @return returns uint representing number of total bets played by user
    */
    function getTotalBets(address _user) public view returns(uint) {
        return bets[_user].length;
    }

    function emergencyWithdraw(address payable _sender) public {
        require(msg.sender == address(casino));

        _sender.transfer(address(this).balance);
    }

    /**
    * @dev Allows anyone to just send ether to contract
    */
    function() external payable {
    }
}
function getUsersBettingHistoryWithTypes(address _user) public view returns(uint history, uint types) {
    uint len = bets[_user].length;

    if (len > 0) {
        while (len >= 0) {
            len--;

            if (bets[_user][len].won) {
                history = history * 10 + 2;
            } else {
                history = history * 10 + 1;
            }

            types = types * 10 + uint(bets[_user][len].gameType);

            if (len == 0 || history > 10000) {
                break;
            }
        }
    }
}

/**
 * @dev Checks for player winning history (up to 5 bets)
 * @param _user represents address of user
 * @return returns history of user in format of uint where each decimal 2 means that bet is won, and 1 means that bet is lost
 */