contract Bets is Ownable, ReservedValue, BetIntervals, BetLevels, Referrals, Services, CanReclaimToken {
  using SafeMath for uint;

  event BetCreated(address indexed bettor, uint betId, uint index, uint allyRace, uint enemyRace, uint betLevel, uint value, uint utmSource);
  event BetAccepted(uint betId, uint opBetId, uint roundId);
  event BetCanceled(uint betId);
  event BetRewarded(uint winBetId, uint loseBetId, uint reward, bool noWin);
  event BetRoundCalculated(uint roundId, uint winnerRace, uint loserRace, uint betLevel, uint pool, uint bettorCount);
  event StartBetRound(uint roundId, uint startAt, uint finishAt);
  event RoundRaceResult(uint roundId, uint raceId, int32 result);
  event FinishBetRound(uint roundId, uint startCheckedAt, uint finishCheckedAt);

  using PPQueue for PPQueue.Queue;

  struct Bettor {
    uint counter;
    mapping(uint => uint) bets;
  }

  struct Race {
    uint index;
    bool exists;
    uint count;
    int32 result;
  }

  struct BetRound {
    uint startedAt;
    uint finishedAt;
    uint startCheckedAt;
    uint finishCheckedAt;
    uint[] bets;
    mapping(uint => Race) races;
    uint[] raceList;
  }

  uint[] public roundsList;

  //roundId => BetRound
  mapping(uint => BetRound) betRounds;

  struct Bet {
    address payable bettor;
    uint roundId;
    uint allyRace;
    uint enemyRace;
    uint value;
    uint level;
    uint opBetId;
    uint reward;
    bool active;
  }

  struct BetStat {
    uint sum;
    uint pool;
    uint affPool;
    uint count;
    bool taxed;
  }

  uint public lastBetId;
  mapping(uint => Bet)  bets;
  mapping(address => Bettor)  bettors;

  struct BetData {
    mapping(uint => BetStat) stat;
    PPQueue.Queue queue;
  }

  //betLevel => allyRace => enemyRace => BetData
  mapping(uint => mapping(uint => mapping(uint => BetData))) betData;

  //raceId => allowed
  mapping(uint => bool) public allowedRace;

  uint public systemFeePcnt;
  uint public affPoolPcnt;


  constructor(address payable _w, address _aff) ReservedValue(_w) Referrals(_aff) public payable {
    //    systemFee 5% (from loser sum)
    //    affPoolPercent 5% (from loser  sum)
    setFee(500, 500);

    //allow races, BTC,LTC,ETH by default
    allowedRace[1] = true;
    allowedRace[2] = true;
    allowedRace[4] = true;
    allowedRace[10] = true;
    allowedRace[13] = true;
  }

  function setFee(uint _systemFee, uint _affFee) public onlyOwner
  {
    systemFeePcnt = _systemFee;
    affPoolPcnt = _affFee;
  }

  function allowRace(uint _race, bool _allow) external onlyOwner {
    allowedRace[_race] = _allow;
  }

  function makeBet(uint allyRace, uint enemyRace, uint _affCode, uint _source) public payable {
    require(allyRace != enemyRace && allowedRace[allyRace] && allowedRace[enemyRace]);
    //require bet level exists
    uint level = getBetLevel(msg.value);

    Bet storage bet = bets[++lastBetId];
    bet.active = true;
    bet.bettor = msg.sender;
    bet.allyRace = allyRace;
    bet.enemyRace = enemyRace;
    bet.value = msg.value;
    bet.level = level;

    //save bet to bettor list && inc. bets count
    bettors[bet.bettor].bets[++bettors[bet.bettor].counter] = lastBetId;

    emit BetCreated(bet.bettor, lastBetId, bettors[bet.bettor].counter, allyRace, enemyRace, level, msg.value, _source);

    //upd queue
    BetData storage allyData = betData[level][allyRace][enemyRace];
    BetData storage enemyData = betData[level][enemyRace][allyRace];

    //if there is nobody on opposite side
    if (enemyData.queue.len() == 0) {
      allyData.queue.push(lastBetId);
    } else {
      //accepting bet
      uint nextRoundId = _startNextRound();
      uint opBetId = enemyData.queue.popf();
      bet.opBetId = opBetId;
      bet.roundId = nextRoundId;
      bets[opBetId].opBetId = lastBetId;
      bets[opBetId].roundId = nextRoundId;

      //upd fight stat
      allyData.stat[nextRoundId].sum = allyData.stat[nextRoundId].sum.add(msg.value);
      allyData.stat[nextRoundId].count++;

      enemyData.stat[nextRoundId].sum = enemyData.stat[nextRoundId].sum.add(bets[opBetId].value);
      enemyData.stat[nextRoundId].count++;
      if (!betRounds[nextRoundId].races[allyRace].exists) {
        betRounds[nextRoundId].races[allyRace].exists = true;
        betRounds[nextRoundId].races[allyRace].index = betRounds[nextRoundId].raceList.length;
        betRounds[nextRoundId].raceList.push(allyRace);
      }
      betRounds[nextRoundId].races[allyRace].count++;

      if (!betRounds[nextRoundId].races[enemyRace].exists) {
        betRounds[nextRoundId].races[enemyRace].exists = true;
        betRounds[nextRoundId].races[enemyRace].index = betRounds[nextRoundId].raceList.length;
        betRounds[nextRoundId].raceList.push(enemyRace);
      }
      betRounds[nextRoundId].races[enemyRace].count++;
      betRounds[nextRoundId].bets.push(opBetId);
      betRounds[nextRoundId].bets.push(lastBetId);

      emit BetAccepted(opBetId, lastBetId, nextRoundId);
    }
    _incTotalReserved(msg.value);

    // update last affiliate
    aff.setUpAffCodeByAddr(bet.bettor, _affCode);
  }

  function _startNextRound() internal returns (uint nextRoundId) {
    uint nextStartAt;
    uint nextFinishAt;
    (nextRoundId, nextStartAt, nextFinishAt) = getNextRoundInfo();
    
    if (betRounds[nextRoundId].startedAt == 0) {
      betRounds[nextRoundId].startedAt = nextStartAt;
      roundsList.push(nextRoundId);
      emit StartBetRound(nextRoundId, nextStartAt, nextFinishAt);
    }
  }

  function cancelBettorBet(address bettor, uint betIndex) external onlyService {
    _cancelBet(bettors[bettor].bets[betIndex]);
  }

  function cancelMyBet(uint betIndex) external nonReentrant {
    _cancelBet(bettors[msg.sender].bets[betIndex]);
  }

  function cancelBet(uint betId) external nonReentrant {
    require(bets[betId].bettor == msg.sender);
    _cancelBet(betId);
  }

  function _cancelBet(uint betId) internal {
    Bet storage bet = bets[betId];
    require(bet.active);
    //can cancel only not yet accepted bets
    require(bet.roundId == 0);

    //upd queue
    BetData storage allyData = betData[bet.level][bet.allyRace][bet.enemyRace];
    allyData.queue.remove(betId);

    _decTotalReserved(bet.value);
    bet.bettor.transfer(bet.value);
    emit BetCanceled(betId);

    // del bet
    delete bets[betId];
  }


  function _calcRoundLevel(uint level, uint allyRace, uint enemyRace, uint roundId) internal returns (int32 allyResult, int32 enemyResult){
    require(betRounds[roundId].startedAt != 0 && betRounds[roundId].finishedAt != 0);

    allyResult = betRounds[roundId].races[allyRace].result;
    enemyResult = betRounds[roundId].races[enemyRace].result;

    if (allyResult < enemyResult) {
      (allyRace, enemyRace) = (enemyRace, allyRace);
    }
    BetData storage winnerData = betData[level][allyRace][enemyRace];
    BetData storage loserData = betData[level][enemyRace][allyRace];

    if (!loserData.stat[roundId].taxed) {
      loserData.stat[roundId].taxed = true;

      //check if really winner
      if (allyResult != enemyResult) {
        //system fee
        uint fee = _getPercent(loserData.stat[roundId].sum, systemFeePcnt);
        _decTotalReserved(fee);

        //affiliate %
        winnerData.stat[roundId].affPool = _getPercent(loserData.stat[roundId].sum, affPoolPcnt);
        //calc pool for round
        winnerData.stat[roundId].pool = loserData.stat[roundId].sum - fee - winnerData.stat[roundId].affPool;

        emit BetRoundCalculated(roundId, allyRace, enemyRace, level, winnerData.stat[roundId].pool, winnerData.stat[roundId].count);
      }
    }

    if (!winnerData.stat[roundId].taxed) {
      winnerData.stat[roundId].taxed = true;
    }
  }

  function rewardBettorBet(address bettor, uint betIndex) external onlyService {
    _rewardBet(bettors[bettor].bets[betIndex]);
  }

  function rewardMyBet(uint betIndex) external nonReentrant {
    _rewardBet(bettors[msg.sender].bets[betIndex]);
  }

  function rewardBet(uint betId) external nonReentrant {
    require(bets[betId].bettor == msg.sender);
    _rewardBet(betId);
  }


  function _rewardBet(uint betId) internal {
    Bet storage bet = bets[betId];
    require(bet.active);
    //only accepted bets
    require(bet.roundId != 0);
    (int32 allyResult, int32 enemyResult) = _calcRoundLevel(bet.level, bet.allyRace, bet.enemyRace, bet.roundId);

    //disabling bet
    bet.active = false;
    if (allyResult >= enemyResult) {
      bet.reward = bet.value;
      if (allyResult > enemyResult) {
        //win
        BetStat memory s = betData[bet.level][bet.allyRace][bet.enemyRace].stat[bet.roundId];
        bet.reward = bet.reward.add(s.pool / s.count);

        // winner's affiliates + loser's affiliates
        uint affPool = s.affPool / s.count;
        _decTotalReserved(affPool);
        // affiliate pool is 1/2 of total aff. pool, per each winner and loser
        _distributeAffiliateReward(affPool >> 1, aff.getAffCode(uint(bet.bettor)), 0, false); //no cacheback to winner
        _distributeAffiliateReward(affPool >> 1, aff.getAffCode(uint(bets[bet.opBetId].bettor)), 0, true); //cacheback to looser
      }


      bet.bettor.transfer(bet.reward);
      _decTotalReserved(bet.reward);
      emit BetRewarded(betId, bet.opBetId, bet.reward, allyResult == enemyResult);
    }
    _flushBalance();
  }


  function provisionBetReward(uint betId) public view returns (uint reward) {
    Bet storage bet = bets[betId];
    if (!bet.active) {
      return 0;
    }

    int32 allyResult = betRounds[bet.roundId].races[bet.allyRace].result;
    int32 enemyResult = betRounds[bet.roundId].races[bet.enemyRace].result;

    if (allyResult < enemyResult) {
      return 0;
    }
    reward = bet.value;

    BetData storage allyData = betData[bet.level][bet.allyRace][bet.enemyRace];
    BetData storage enemyData = betData[bet.level][bet.enemyRace][bet.allyRace];

    if (allyResult > enemyResult) {
      //win
      if (!enemyData.stat[bet.roundId].taxed) {
        uint pool = enemyData.stat[bet.roundId].sum - _getPercent(enemyData.stat[bet.roundId].sum, systemFeePcnt + affPoolPcnt);
        reward = bet.value.add(pool / allyData.stat[bet.roundId].count);
      } else {
        reward = bet.value.add(allyData.stat[bet.roundId].pool / allyData.stat[bet.roundId].count);
      }
    }
  }

  function provisionBettorBetReward(address bettor, uint betIndex) public view returns (uint reward) {
    return provisionBetReward(bettors[bettor].bets[betIndex]);
  }

  function finalizeBetRound(uint betLevel, uint allyRace, uint enemyRace, uint roundId) external onlyService {
    _calcRoundLevel(betLevel, allyRace, enemyRace, roundId);
    _flushBalance();
  }

  function roundsCount() external view returns (uint) {
    return roundsList.length;
  }

  function getBettorsBetCounter(address bettor) external view returns (uint) {
    return bettors[bettor].counter;
  }

  function getBettorsBetId(address bettor, uint betIndex) external view returns (uint betId) {
    return bettors[bettor].bets[betIndex];
  }

  function getBettorsBets(address bettor) external view returns (uint[] memory betIds) {
    Bettor storage b = bettors[bettor];
    uint j;
    for (uint i = 1; i <= b.counter; i++) {
      if (b.bets[i] != 0) {
        j++;
      }
    }
    if (j > 0) {
      betIds = new uint[](j);
      j = 0;
      for (uint i = 1; i <= b.counter; i++) {
        if (b.bets[i] != 0) {
          betIds[j++] = b.bets[i];
        }
      }
    }
  }


  function getBet(uint betId) public view returns (
    address bettor,
    bool active,
    uint roundId,
    uint allyRace,
    uint enemyRace,
    uint value,
    uint level,
    uint opBetId,
    uint reward
  ) {
    Bet memory b = bets[betId];
    return (b.bettor, b.active, b.roundId, b.allyRace, b.enemyRace, b.value, b.level, b.opBetId, b.reward);
  }

  function getBetRoundStat(uint roundId, uint allyRace, uint enemyRace, uint level) public view returns (
    uint sum,
    uint pool,
    uint affPool,
    uint count,
    bool taxed
  ) {
    BetStat memory bs = betData[level][allyRace][enemyRace].stat[roundId];
    return (bs.sum, bs.pool, bs.affPool, bs.count, bs.taxed);
  }

  function getBetQueueLength(uint allyRace, uint enemyRace, uint level) public view returns (uint) {
    return betData[level][allyRace][enemyRace].queue.len();
  }

  function getCurrentBetRound() public view returns (
    uint roundId,
    uint startedAt,
    uint finishedAt,
    uint startCheckedAt,
    uint finishCheckedAt,
    uint betsCount,
    uint raceCount
  ) {
    roundId = getCurrentRoundId();
    (startedAt, finishedAt, startCheckedAt, finishCheckedAt, betsCount, raceCount) = getBetRound(roundId);
  }

  function getNextBetRound() public view returns (
    uint roundId,
    uint startedAt,
    uint finishedAt,
    uint startCheckedAt,
    uint finishCheckedAt,
    uint betsCount,
    uint raceCount
  ) {
    roundId = getCurrentRoundId() + 1;
    (startedAt, finishedAt, startCheckedAt, finishCheckedAt, betsCount, raceCount) = getBetRound(roundId);
  }

  function getBetRound(uint roundId) public view returns (
    uint startedAt,
    uint finishedAt,
    uint startCheckedAt,
    uint finishCheckedAt,
    uint betsCount,
    uint raceCount
  ) {
    BetRound memory b = betRounds[roundId];
    return (b.startedAt, b.finishedAt, b.startCheckedAt, b.finishCheckedAt, b.bets.length, b.raceList.length);
  }

  function getBetRoundBets(uint roundId) public view returns (uint[] memory betIds) {
    return betRounds[roundId].bets;
  }

  function getBetRoundBetId(uint roundId, uint betIndex) public view returns (uint betId) {
    return betRounds[roundId].bets[betIndex];
  }


  function getBetRoundRaces(uint roundId) public view returns (uint[] memory raceIds) {
    return betRounds[roundId].raceList;
  }

  function getBetRoundRaceStat(uint roundId, uint raceId) external view returns (
    uint index,
    uint count,
    int32 result
  ){
    Race memory r = betRounds[roundId].races[raceId];
    return (r.index, r.count, r.result);
  }

  function setBetRoundResult(uint roundId, uint count, uint[] memory packedRaces, uint[] memory packedResults) public onlyService {
    require(packedRaces.length == packedResults.length);
    require(packedRaces.length * 8 >= count);

    BetRound storage r = betRounds[roundId];
    require(r.startedAt != 0 && r.finishedAt == 0);

    uint raceId;
    int32 result;
    for (uint i = 0; i < count; i++) {
      raceId = _upack(packedRaces[i / 8], i % 8);
      result = int32(_upack(packedResults[i / 8], i % 8));
      r.races[raceId].result = result;
      emit RoundRaceResult(roundId, raceId, result);
    }
  }

  function finishBetRound(uint roundId, uint startCheckedAt, uint finishCheckedAt) public onlyService {
    BetRound storage r = betRounds[roundId];
    require(r.startedAt != 0 && r.finishedAt == 0);
    uint finishAt;
    (, , finishAt) = getRoundInfoAt(r.startedAt, 0);
    require(now >= finishAt);
    r.finishedAt = finishAt;
    r.startCheckedAt = startCheckedAt;
    r.finishCheckedAt = finishCheckedAt;
    emit FinishBetRound(roundId, startCheckedAt, finishCheckedAt);
  }

  //extract n-th 32-bit int from uint
  function _upack(uint _v, uint _n) internal pure returns (uint) {
    //    _n = _n & 7; //be sure < 8
    return (_v >> (32 * _n)) & 0xFFFFFFFF;
  }

}
function _upack(uint _v, uint _n) internal pure returns (uint) {
  //    _n = _n & 7; //be sure < 8
  return (_v >> (32 * _n)) & 0xFFFFFFFF;
}

//extract n-th 32-bit int from uint