require 'sinatra'
require 'bigdecimal/util'

require_relative 'sinatra_ssl'

set :environment,     :production
set :port,            8445
set :ssl_certificate, '/etc/letsencrypt/live/lgdm.uk/fullchain.pem'
set :ssl_key,         '/etc/letsencrypt/live/lgdm.uk/privkey.pem'

WHITE_DECK =  ['0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2c', '2c', '2c']
YELLOW_DECK = ['0', '0', '0', '0', '0', '0', '1', '1', '1', '2', '2', '2', '3', '3', '3', '3c', '3c', '3c']
RED_DECK =    ['0', '0', '0', '0', '0', '0', '2', '2', '2', '3', '3', '3', '3', '3', '3', '4c', '4c', '4c']
BLACK_DECK =  ['0', '0', '0', '0', '0', '0', '3', '3', '3', '3', '3', '3', '4', '4', '4', '5c', '5c', '5c']

def reset_all_decks
  $decks = {
    player: {
      white:  WHITE_DECK.dup,
      yellow: YELLOW_DECK.dup,
      red:    RED_DECK.dup,
      black:  BLACK_DECK.dup
    }, 
    enemy: {
      white:  WHITE_DECK.dup,
      yellow: YELLOW_DECK.dup,
      red:    RED_DECK.dup,
      black:  BLACK_DECK.dup
    }
  }
  $known_decks = {
    player: {
      white:  WHITE_DECK.dup,
      yellow: YELLOW_DECK.dup,
      red:    RED_DECK.dup,
      black:  BLACK_DECK.dup
    }, 
    enemy: {
      white:  WHITE_DECK.dup,
      yellow: YELLOW_DECK.dup,
      red:    RED_DECK.dup,
      black:  BLACK_DECK.dup
    }
  }
  $known_decks_summary = {
    player: {
      white:  WHITE_DECK_SUMMARY.dup,
      yellow: YELLOW_DECK_SUMMARY.dup,
      red:    RED_DECK_SUMMARY.dup,
      black:  BLACK_DECK_SUMMARY.dup
    }, 
    enemy: {
      white:  WHITE_DECK_SUMMARY.dup,
      yellow: YELLOW_DECK_SUMMARY.dup,
      red:    RED_DECK_SUMMARY.dup,
      black:  BLACK_DECK_SUMMARY.dup
    }
  }
  $discard_decks = {
    player: {
      white:  [],
      yellow: [],
      red:    [],
      black:  []
    }, 
    enemy: {
      white:  [],
      yellow: [],
      red:    [],
      black:  []
    }
  }
  $drawn = []
  $revealed = []
end

def update_known_decks
  $known_decks = {
    player: {
      white:  $decks[:player][:white].dup,
      yellow: $decks[:player][:yellow].dup,
      red:    $decks[:player][:red].dup,
      black:  $decks[:player][:black].dup
    }, 
    enemy: {
      white:  $decks[:enemy][:white].dup,
      yellow: $decks[:enemy][:yellow].dup,
      red:    $decks[:enemy][:red].dup,
      black:  $decks[:enemy][:black].dup
    }
  }
  $known_decks_summary = {
    player: {
      white:  calculate_deck_summary($known_decks[:player][:white]),
      yellow: calculate_deck_summary($known_decks[:player][:yellow]),
      red:    calculate_deck_summary($known_decks[:player][:red]),
      black:  calculate_deck_summary($known_decks[:player][:black])
    }, 
    enemy: {
      white:  calculate_deck_summary($known_decks[:enemy][:white]),
      yellow: calculate_deck_summary($known_decks[:enemy][:yellow]),
      red:    calculate_deck_summary($known_decks[:enemy][:red]),
      black:  calculate_deck_summary($known_decks[:enemy][:black])
    }
  }
end

def calculate_deck_summary(deck)
  deck_summary = Hash.new { |key, value| key[value] = 0 }
  deck.each do |value|
    deck_summary[value] += 1
  end
  deck_summary
end

def const_deck_from_colour(colour)
  case colour
  when :white
    WHITE_DECK.dup
  when :yellow
    YELLOW_DECK.dup
  when :red
    RED_DECK.dup
  when :black
    BLACK_DECK.dup
  end
end

def calculate_revealed_summary
  num_misses = 0
  total = 0
  $revealed.each do |pos_cards|
    crit = false
    miss = false
    pos_cards.each do |card|
      if card[:value] == '0' && !crit
        miss = true
      elsif card[:value][1] == 'c'
        crit = true
        miss = false
        total += card[:value][0].to_i
      else
        miss = false
        total += card[:value][0].to_i unless card[:redrawn]
      end
    end
    num_misses += 1 if miss
  end
  $revealed_summary = "#{num_misses > 1 ? 'FAILED.' : 'SUCCESS!'} Total: #{total}"
end

# 12 * 12 = 144
# 6 * 12  = 72
# 12 * 6  = 72
#    sum  = 288
# 18 * 18 = 324

# num misses to hit
# 0, 0, 0, 0
# 1, 0, 0, 0
# 0, 1, 0, 0
# 0, 0, 1, 0
# 0, 0, 0, 1
def calculate_drawn_summary
  return if $drawn.size == 0
  
  num_known_misses = 0
  colour_data = Hash.new { |k,v| k[v] = {num_drawn: 0} }
  team = $drawn[0][0][:team]
  $drawn.each do |pos_cards|
    card = pos_cards[0]
    if card[:known]
      num_known_misses += 1 if card[:value] == '0'
    else
      colour_data[card[:colour]][:num_drawn] += 1
    end
  end
  if num_known_misses >= 2
    # $drawn_summary = "Hit%: 0%. Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
    $drawn_summary = "Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
    return
  end
  total_combos = 1
  zero_miss_combos = 1
  colour_data.each do |colour, data|
    deck = $known_decks[team][colour]
    deck_cards = deck.size
    deck_misses = deck.count('0')
    deck_hits = deck_cards - deck_misses
    total_combos *= choose(deck_cards, data[:num_drawn])
    colour_data[colour][:zero_miss_combos] = choose(deck_hits, data[:num_drawn])
    zero_miss_combos *= colour_data[colour][:zero_miss_combos]
    next if num_known_misses == 1 # || data[:num_drawn] == 1
    
    colour_data[colour][:one_miss_combos] = choose(deck_misses, 1) * choose(deck_hits, data[:num_drawn]-1)
  end
  if num_known_misses == 1
    # $drawn_summary = "Hit%: #{(zero_miss_combos / total_combos * 100).round}%. " \
    #                  "Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
    $drawn_summary = "Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
    return
  end
  one_miss_combos = 0
  colour_data.each do |colour, data|
    colour_one_miss_combos = data[:one_miss_combos]
    colour_data.each do |other_colour, other_data|
      next if other_colour == colour
      
      colour_one_miss_combos *= data[:zero_miss_combos]
    end
    one_miss_combos += colour_one_miss_combos if colour_one_miss_combos
  end
  # puts ": #{}"
  # puts "num_known_misses: #{num_known_misses}"
  # puts "colour_data: #{colour_data}"
  # puts "total_combos: #{total_combos}"
  # puts "zero_miss_combos: #{zero_miss_combos}"
  # puts "one_miss_combos: #{one_miss_combos}"
  # $drawn_summary = "Hit%: #{(((zero_miss_combos + one_miss_combos) / total_combos) * 100).round}%. " \
  #                  "Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
  $drawn_summary = "Expected Total: #{$drawn.sum { |cards| cards[0][:average_value] }.round(2).to_f}"
end

def do_maths
  player_white_average = calculate_average_for($known_decks[:player][:white])
  player_yellow_average = calculate_average_for($known_decks[:player][:yellow])
  player_red_average = calculate_average_for($known_decks[:player][:red])
  player_black_average = calculate_average_for($known_decks[:player][:black])
  enemy_white_average = calculate_average_for($known_decks[:enemy][:white])
  enemy_yellow_average = calculate_average_for($known_decks[:enemy][:yellow])
  enemy_red_average = calculate_average_for($known_decks[:enemy][:red])
  enemy_black_average = calculate_average_for($known_decks[:enemy][:black])
  $maths = {
    averages: {
      player: {
        white:        player_white_average,
        yellow:       player_yellow_average,
        red:          player_red_average,
        black:        player_black_average,
        white_delta:  player_white_average - WHITE_AVERAGE,
        yellow_delta: player_yellow_average - YELLOW_AVERAGE,
        red_delta:    player_red_average - RED_AVERAGE,
        black_delta:  player_black_average - BLACK_AVERAGE
      },
      enemy: {
        white:        enemy_white_average,
        yellow:       enemy_yellow_average,
        red:          enemy_red_average,
        black:        enemy_black_average,
        white_delta:  enemy_white_average - WHITE_AVERAGE,
        yellow_delta: enemy_yellow_average - YELLOW_AVERAGE,
        red_delta:    enemy_red_average - RED_AVERAGE,
        black_delta:  enemy_black_average - BLACK_AVERAGE
      }
    },
    hits: {
      player: {
        white:  calculate_hits_for($known_decks[:player][:white], $discard_decks[:player][:white]).map(&:round),
        yellow: calculate_hits_for($known_decks[:player][:yellow], $discard_decks[:player][:yellow]).map(&:round),
        red:    calculate_hits_for($known_decks[:player][:red], $discard_decks[:player][:red]).map(&:round),
        black:  calculate_hits_for($known_decks[:player][:black], $discard_decks[:player][:black]).map(&:round)
      }, 
      enemy: {
        white:  calculate_hits_for($known_decks[:enemy][:white], $discard_decks[:enemy][:white]).map(&:round),
        yellow: calculate_hits_for($known_decks[:enemy][:yellow], $discard_decks[:enemy][:yellow]).map(&:round),
        red:    calculate_hits_for($known_decks[:enemy][:red], $discard_decks[:enemy][:red]).map(&:round),
        black:  calculate_hits_for($known_decks[:enemy][:black], $discard_decks[:enemy][:black]).map(&:round),
      }
    }
  }
end

def calculate_average_for(deck)
  return 0.to_d if deck.size == 0

  total = 0.to_d
  deck.each_with_index do |card, index|
    if card[1] == 'c'
      new_deck = deck.dup
      new_deck.delete_at(index)
      total += card[0].to_d + calculate_average_for(new_deck)
    else
      total += card.to_d
    end
  end
  return total / deck.size.to_d
end

def calculate_hits_for(deck, discard)
  hits = []
  10.times do |index|
    num_drawn = index + 1
    return hits if deck.size < num_drawn && discard.size < num_drawn-deck.size
      
    hits[index] = calculate_hit_for(deck, discard, num_drawn) * 100
  end
  hits
end

def calculate_hit_for(deck, discard, num_drawn, num_misses = 2)
  deck_cards = deck.size
  return 0 if deck_cards == 0
  
  deck_misses = deck.count('0')
  deck_hits = deck_cards - deck_misses
  return deck_hits.to_d / deck_cards.to_d if num_drawn == 1
  
  wanted_drawn = num_drawn
  num_drawn = deck_cards if num_drawn > deck_cards
  total_combos = choose(deck_cards, num_drawn)
  zero_miss_prob = choose(deck_hits, num_drawn) / total_combos
  one_miss_prob = num_misses == 1 ? 0 : (choose(deck_misses, 1) * choose(deck_hits, num_drawn-1) / total_combos)
  hit_prob = zero_miss_prob + one_miss_prob
  return hit_prob if deck_cards >= wanted_drawn || deck_misses > 1
  
  hit_prob * calculate_hit_for(discard, [], wanted_drawn-deck_cards, 2 - deck_misses)
end

def choose(n, k)
  return 0 if k > n
  
  result = 1
  1.upto(k) do |d|
     result *= n
     result /= d
     n -= 1
  end
  result.to_d
end

get '/' do
  do_maths
  erb :index
end

get '/draw/:team/:colour' do
  team = params[:team].to_sym
  colour = params[:colour].to_sym
  if $decks[team][colour].size == 0
    redirect '/' if $discard_decks[team][colour].size == 0
    
    $decks[team][colour] = $discard_decks[team][colour].dup.sort!
    $discard_decks[team][colour] = []
    $drawn.each do |pos_cards|
      pos_cards[0][:no_undo] = true if pos_cards[0][:colour] == colour && pos_cards[0][:known]
    end
    update_known_decks
  end
  $drawn.each do |pos_cards|
    pos_cards[0][:no_undo] = true if pos_cards[0][:colour] == colour && pos_cards[0][:known]
  end
  $drawn << [{
    team: team,
    colour: colour,
    value: $decks[team][colour].delete_at(rand($decks[team][colour].size)),
    average_value: $maths[:averages][team][colour]
  }]
  if $decks[team][colour].size == 0
    $drawn.each do |pos_cards|
      pos_cards[0][:known] = true if pos_cards[0][:colour] == colour
    end
  end
  calculate_drawn_summary
  redirect '/'
end 

get '/undraw/:position' do
  position = params[:position].to_i
  team = $drawn[position][0][:team]
  colour = $drawn[position][0][:colour]
  if $drawn[position][0][:known]
    shuffle = true
    positions = []
    $drawn.each_with_index do |pos_cards, index|
      positions << index if pos_cards[0][:colour] == colour && pos_cards[0][:known]
    end
    position = positions.sample
    if $decks[team][colour].size > 0
      $discard_decks[team][colour] = $decks[team][colour].dup
      $decks[team][colour] = []
      update_known_decks
    end
  end
  $decks[team][colour] << $drawn[position][0][:value]
  $decks[team][colour].sort!
  $drawn.delete_at(position)
  unknown_count = 0
  $drawn.each do |pos_cards|
    unknown_count += 1 if pos_cards[0][:colour] == colour && !pos_cards[0][:known]
  end
  $drawn.each do |pos_cards|
    if pos_cards[0][:colour] == colour
      pos_cards[0][:no_undo] = false if unknown_count == 0
      pos_cards[0][:known] = false if shuffle
    end
  end
  if shuffle
    positions.shuffle.each do |pos|
      pos_cards = $drawn[pos].dup
      $drawn[pos] = nil
      $drawn << pos_cards
    end
    $drawn.compact!
    $drawn.sort_by! do |pos_cards|
      case pos_cards[0][:colour]
      when :white
        1
      when :yellow
        2
      when :red
        3
      when :black
        4
      end
    end
  end
  calculate_drawn_summary
  redirect '/'
end

get '/reveal_drawn' do
  $revealed = $drawn.dup
  $drawn = []
  calculate_revealed_summary
  update_known_decks
  redirect '/'
end

get '/reveal/:position/:row/:team/:colour' do
  position = params[:position].to_i
  row = params[:row].to_i
  team = params[:team].to_sym
  colour = params[:colour].to_sym
  if $decks[team][colour].size == 0
    $decks[team][colour] = $discard_decks[team][colour].dup
    $discard_decks[team][colour] = []
  end
  unless $revealed[position][row][:value][1] == 'c'
    $revealed[position][row][:redrawn] = true
  end
  ($revealed[position] ||= []) << {
    team: team,
    colour: colour,
    value: $decks[team][colour].delete_at(rand($decks[team][colour].size))
  }
  calculate_revealed_summary
  update_known_decks
  redirect '/'
end

get '/discard_revealed' do
  $revealed.each do |card_position|
    card_position.each do |card|
      $discard_decks[card[:team]][card[:colour]] << card[:value]
      $discard_decks[card[:team]][card[:colour]].sort!
    end
  end
  $revealed = []
  empty_deck = false
  $decks.each do |team, decks|
    decks.each do |colour, deck|
      next unless deck.size == 0
      
      $decks[team][colour] = $discard_decks[team][colour].dup.sort!
      $discard_decks[team][colour] = []
      empty_deck = true
    end
  end
  update_known_decks if empty_deck
  redirect '/'
end

get '/reset_all' do
  reset_all_decks
  redirect '/'
end

get '/reset/:team/:colour' do
  team = params[:team].to_sym
  colour = params[:colour].to_sym
  $decks[team][colour] = const_deck_from_colour(colour)
  $known_decks[team][colour] = const_deck_from_colour(colour)
  $known_decks_summary[team][colour] = calculate_deck_summary($known_decks[team][colour])
  $discard_decks[team][colour] = []
  redirect '/'
end

get '/set/:team/:colour/:cards' do
  team = params[:team].to_sym
  colour = params[:colour].to_sym
  cards = params[:cards].split(',')
  $decks[team][colour] = cards.dup
  $known_decks[team][colour] = cards.dup
  $known_decks_summary[team][colour] = calculate_deck_summary(cards)
  $discard_decks[team][colour] = const_deck_from_colour(colour)
  cards.each do |value|
    $discard_decks[team][colour].delete(value)
  end
  redirect '/'
end

WHITE_AVERAGE  = calculate_average_for(WHITE_DECK)
YELLOW_AVERAGE = calculate_average_for(YELLOW_DECK)
RED_AVERAGE    = calculate_average_for(RED_DECK)
BLACK_AVERAGE  = calculate_average_for(BLACK_DECK)

WHITE_DECK_SUMMARY  = calculate_deck_summary(WHITE_DECK)
YELLOW_DECK_SUMMARY = calculate_deck_summary(YELLOW_DECK)
RED_DECK_SUMMARY    = calculate_deck_summary(RED_DECK)
BLACK_DECK_SUMMARY  = calculate_deck_summary(BLACK_DECK)

reset_all_decks
puts 'Started Oathsworn Decks'
