// Copyright (c) 2022 Dominic Masters
// 
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

#include "PokerPlayer.hpp"
#include "PokerGame.hpp"

using namespace Dawn;

PokerPlayer::PokerPlayer(std::weak_ptr<SceneItem> item) : SceneItemComponent(item) {
  
}

void PokerPlayer::onStart() {
  SceneItemComponent::onStart();
  this->pokerGame = this->getScene()->findComponent<PokerGame>();
}

void PokerPlayer::addChips(int32_t chips) {
  assertTrue(chips > 0);

  this->chips += chips;
  if(this->chips > 0) this->isOut = false;
  eventChipsChanged.invoke();
}

void PokerPlayer::setChips(int32_t chips) {
  this->chips = 0;
  this->addChips(chips);
}

bool_t PokerPlayer::needsToBetThisRound() {
  if(this->isFolded) return false;
  if(this->chips <= 0) return false;
  if(!this->hasBetThisRound) return true;
  if(this->currentBet < this->pokerGame->getCurrentCallValue()) return true;
  return false;
}

void PokerPlayer::bet(struct PokerPot *pot, int32_t chips) {
  assertNotNull(pot);
  assertTrue(chips >= 0);
  assertTrue(!this->isFolded);
  assertTrue(!this->isOut);
  this->setChips(this->chips - chips);
  this->currentBet += chips;
  this->hasBetThisRound = true;
  if(chips > 0) {
    this->timesRaised++;
  } else {
    this->timesRaised = 0;
  }

  pot->chips += chips;
  pot->call = mathMax<int32_t>(pot->call, this ->currentBet);

  auto existing = std::find(pot->players.begin(), pot->players.end(), this);
  if(existing == pot->players.end()) pot->players.push_back(this);
}

void PokerPlayer::bet(int32_t chips) {
  assertTrue(this->pokerGame->pots.size() > 0);
  this->bet(&this->pokerGame->pots.back(), chips);
}

void PokerPlayer::fold() {
  this->isFolded = true;
  this->hasBetThisRound = true;
  this->timesRaised = 0;
}

bool_t PokerPlayer::canCheck() {
  return this->pokerGame->getCurrentCallValue() <= this->currentBet;
}

struct PokerTurn PokerPlayer::getAITurn() {
  struct PokerTurn turn;
  float_t confidence;
  int32_t callBet;
  float_t potOdds;

  // Can the player do anything?
  if(this->isFolded || this->isOut) {
    turn.type = POKER_TURN_TYPE_OUT;
    return turn;
  }

  // The following logic is heavily inspired by;
  // https://github.com/gorel/C-Poker-AI/blob/master/src/common/pokerai.c
  // But with some changes and smarts added by me. The original source code will
  // essentially just run a crap tun of simulated games and get the times that
  // they are expected to win from those games, but I'm just going to use the
  // odds of the winning hand.


  // Is this preflop?
  if(this->pokerGame->community.size() == 0) {
    assertTrue(this->hand.size() == POKER_PLAYER_HAND_SIZE_MAX);

    // Get the hand weight
    auto cardNumber0 = this->hand[0].getValue();
    auto suitNumber0 = this->hand[0].getSuit();
    auto cardNumber1 = this->hand[1].getValue();
    auto suitNumber1 = this->hand[1].getSuit();

    // Get delta between cards
    auto i = (uint8_t)mathAbs<int8_t>(
      (int8_t)cardNumber0 - (int8_t)cardNumber1
    );

    // Get card weight
    confidence = (float_t)cardNumber0 + (float_t)cardNumber1;
    if(cardNumber0 == cardNumber1) {// Pairs
      confidence += 6;
    } else if(suitNumber0 == suitNumber1) {// Same suit
      confidence += 4;
    }

    // Get difference from cards for guessing flush
    if(i > 4) {
      confidence -= 4;
    } else if(i > 2) {
      confidence -= i;
    }

    // Get the confidence delta 0-1
    confidence = confidence / 30.0f;

    // This may change in future, but I was finding the AI did not want to bet
    // during the preflop enough, this curves the AI to want to preflop call 
    // often.
    confidence = easeOutCubic(confidence);
  } else {
    // Simulate my hand being the winning hand, use that as the confidence
    auto winning = this->getWinning();
    confidence = PokerWinning::getWinningTypeConfidence(winning.type);
  }

  // Now we know how confident the AI is, let's put a chip value to that weight
  // How many chips to call?
  callBet = this->getCallBet();

  // Do they need chips to call, or is it possible to check?
  if(callBet > 0) {
    potOdds = (float_t)callBet / (
      (float_t)callBet +
      (float_t)this->getSumOfChips()
    );
  } else {
    potOdds = 1.0f / (float_t)this->pokerGame->getRemainingBettersCount();
  }

  // Now determine the expected ROI
  auto expectedGain = confidence / potOdds;

  // Now get a random 0-100
  auto random = randomGenerate<int32_t>() % 100;

  // Determine the max bet that the AI is willing to make
  auto maxBet = (int32_t)((float_t)this->chips / 1.75f) - (random / 2);
  maxBet -= callBet;

  // Determine what's a good bluff bet.
  auto bluffBet = random * maxBet / 100 / 2;

  // Now prep the output
  auto isBluff = false;
  auto amount = 0;

  // Now the actual AI can happen. This is basically a weight to confidence
  // ratio. The higher the gains and the confidence then the more likely the AI
  // is to betting. There are also bluff chances within here.
  if(expectedGain < 0.8f && confidence < 0.8f) {
    if(random < 85) {
      amount = 0;
    } else {
      amount = bluffBet;
      isBluff = true;
    }
  } else if((expectedGain < 1.0f && confidence < 0.85f) || confidence < 0.1f) {
    if(random < 80) {
      amount = 0;
    } else if(random < 5) {
      amount = callBet;
      isBluff = true;
    } else {
      amount = bluffBet;
      isBluff = true;
    }
  } else if((expectedGain < 1.3f && confidence < 0.9f) || confidence < 0.5f) {
    if(random < 60 || confidence < 0.5f) {
      amount = callBet;
    } else {
      amount = maxBet;
    }
  } else if(confidence < 0.95f || this->pokerGame->community.size() < 4) {
    if(random < 20) {
      amount = callBet;
    } else {
      amount = maxBet;
    }
  } else {
    amount = (this->chips - callBet) * 9 / 10;
  }
  
  // TODO: We can nicely round the amounts here to get us to a more "human"
  // number.

  // If this is the first round... make it a lot less likely I'll bet
  if(this->pokerGame->community.size() == 0 && amount > callBet) {
    if(random > 5) amount = callBet;
  }

  // Did we actually bet?
  if(amount > 0) {
    std::cout << "AI is betting " << amount << " chips, bluff:" <<  isBluff << std::endl;

    // Let's not get caught in a raising loop with AI.
    if(this->timesRaised >= POKER_PLAYER_MAX_RAISES) {
      amount = callBet;
    }

    amount = mathMax<int32_t>(amount, callBet);
    turn = PokerTurn::bet(this, amount);
    turn.confidence = confidence;
  } else if(this->canCheck()) {
    turn = PokerTurn::bet(this, 0);
    turn.confidence = 1;
  } else {
    turn = PokerTurn::fold(this);
    turn.confidence = 1 - confidence;
  }

  return turn;
}

int32_t PokerPlayer::getCallBet() {
  return this->pokerGame->getCurrentCallValue() - this->currentBet;
}

int32_t PokerPlayer::getSumOfChips() {
  int32_t count = 0;
  auto it = this->pokerGame->pots.begin();
  while(it != this->pokerGame->pots.end()) {
    if(std::find(it->players.begin(), it->players.end(), this) != it->players.end()) {
      count += it->chips;
    }
    ++it;
  }
  return count;
}

struct PokerWinning PokerPlayer::getWinning() {
  struct PokerWinning winning;
  struct Card card(0x00);
  uint8_t i, j;
  int32_t index;
  enum CardValue number, look;
  enum CardSuit suit;
  std::vector<struct Card> pairs;

  winning.player = this;

  // Get the full poker hand (should be a 7 card hand, but MAY not be)
  for(i = 0; i < this->pokerGame->community.size(); i++) {
    winning.full.push_back(this->pokerGame->community[i]);
  }
  for(i = 0; i < this->hand.size(); i++) {
    winning.full.push_back(this->hand[i]);
  }
  Card::sort(&winning.full);
  
  //////////////////////// Now look for the winning set ////////////////////////

  // Royal / Straight Flush
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];
    number = card.getValue();
    if(number < CARD_FIVE) continue;

    suit = card.getSuit();
    
    winning.set.clear();
    winning.set.push_back(card);

    // Now look for the matching cards (Reverse order to order from A to 10)
    for(j = 1; j <= 4; j++) {
      // Ace low.
      look = (
        number == CARD_FIVE && j == 4 ? 
        (enum CardValue)CARD_ACE :
        (enum CardValue)(number - j)
      );
      index = Card::contains(&winning.full, Card(suit, look));
      if(index == -1) break;
      winning.set.push_back(winning.full[index]);
    }

    // Check if has all necessary cards.
    if(winning.set.size() < POKER_WINNING_SET_SIZE) continue;

    // Add self to array
    winning.type = (
      number == CARD_ACE ? POKER_WINNING_TYPE_ROYAL_FLUSH : 
      POKER_WINNING_TYPE_STRAIGHT_FLUSH
    );
    winning.fillRemaining();
    return winning;
  }

  // Four of a kind.
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];
    number = card.getValue();
    pairs = Card::countPairs(&winning.full, number);
    if(pairs.size() < CARD_SUIT_COUNT) continue;

    winning.set = pairs;
    winning.type = POKER_WINNING_TYPE_FOUR_OF_A_KIND;
    winning.fillRemaining();
    return winning;
  }

  // Full House
  winning.set.clear();
  for(i = 0; i < winning.full.size(); i++) {
    // Check we haven't already added this card.
    card = winning.full[i];
    if(Card::contains(&winning.set, card) != -1) {
      continue;
    }

    number = card.getValue();
    pairs = Card::countPairs(&winning.full, number);

    // Did we find either two pair or three pair?
    if(pairs.size() != 2 && pairs.size() != 3) continue;
    if(winning.set.size() == 3) {//Clamp to 5 max.
      pairs.pop_back();
    }

    // Copy found pairs.
    for(j = 0; j < pairs.size(); j++) {
      winning.set.push_back(pairs[j]);
    }

    // Winned?
    if(winning.set.size() != POKER_WINNING_SET_SIZE) continue;
    winning.type = POKER_WINNING_TYPE_FULL_HOUSE;
    winning.fillRemaining();
    return winning;
  }

  // Flush (5 same suit)
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];
    suit = card.getSuit();
    
    winning.set.clear();
    winning.set.push_back(card);

    for(j = i+1; j < winning.full.size(); j++) {
      if(winning.full[j].getSuit() != suit) continue;
      winning.set.push_back(winning.full[j]);
      if(winning.set.size() == POKER_WINNING_SET_SIZE) break;
    }
    if(winning.set.size() < POKER_WINNING_SET_SIZE) continue;
    winning.type = POKER_WINNING_TYPE_FLUSH;
    winning.fillRemaining();
    return winning;
  }

  // Straight (sequence any suit)
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];
    number = card.getValue();
    if(number < CARD_FIVE) continue;

    winning.set.clear();
    winning.set.push_back(card);
    
    for(j = 1; j <= 4; j++) {
      // Ace low.
      look = (
        number == CARD_FIVE && j == 4 ?
        (enum CardValue)CARD_ACE :
        (enum CardValue)(number - j)
      );
      index = Card::containsNumber(&winning.full, look);
      if(index == -1) break;
      winning.set.push_back(winning.full[index]);
    }

    // Check if has all necessary cards.
    if(winning.set.size() < POKER_WINNING_SET_SIZE) continue;
    winning.type = POKER_WINNING_TYPE_STRAIGHT;
    winning.fillRemaining();
    return winning;
  }

  // Three of a kind
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];
    number = card.getValue();
    pairs = Card::countPairs(&winning.full, number);
    if(pairs.size() != 3) continue;

    winning.set = pairs;
    winning.type = POKER_WINNING_TYPE_THREE_OF_A_KIND;
    winning.fillRemaining();
    return winning;
  }

  // Two Pair
  winning.set.clear();
  for(i = 0; i < winning.full.size(); i++) {
    card = winning.full[i];// Check we haven't already added this card.
    if(
      winning.set.size() > 0 &&
      Card::contains(&winning.set, card) != -1
    ) {
      continue;
    }

    number = card.getValue();
    pairs = Card::countPairs(&winning.full, number);
    if(pairs.size() != 2) continue;
    for(j = 0; j < pairs.size(); j++) {
      winning.set.push_back(pairs[j]);
    }
    if(winning.set.size() != 4) continue;
    winning.type = POKER_WINNING_TYPE_TWO_PAIR;
    winning.fillRemaining();
    return winning;
  }

  // Pair
  if(winning.set.size() == 2) {
    winning.type = POKER_WINNING_TYPE_PAIR;
    winning.fillRemaining();
    return winning;
  }

  // High card
  winning.set.clear();
  winning.fillRemaining();
  winning.type = POKER_WINNING_TYPE_HIGH_CARD;
  return winning;
}