require "json"
require "math"

class Champion
  include JSON::Serializable

  property cost : Int32
  property name : String
  property traits : Array(String)
end

class Trait
  include JSON::Serializable

  class Effect
    include JSON::Serializable

    @[JSON::Field(key: "minUnits")]
    property min_units : Int32

    @[JSON::Field(key: "maxUnits")]
    property max_units : Int32

    property style : Int32
  end

  property effects : Array(Effect)
  property name : String

  def active_at?(unit_count)
    return effects.any? do |effect|
      effect.max_units >= unit_count && effect.min_units <= unit_count
    end
  end

  def tier_at(unit_count)
    return effects.index do |effect|
      effect.max_units >= unit_count && effect.min_units <= unit_count
    end
  end

  def unique?
    return effects.any? do |effect|
      effect.style == 4
    end
  end
end

def comb(n, r)
  return ((n-r+1)..n).product // (1..r).product
end

def check_active(champions, trait_data, emblems = [] of String, uniques = false)
  active = [] of Tuple(Trait, Int32, Int32)
  count = {} of String => Int32

  champions.each do |champion|
    champion.traits.tally(count)
  end
  count.each do |trait_name, count|
    trait = trait_data[trait_name]
    if trait.active_at?(count) && (uniques || !trait.unique?)
      active << {trait, count, trait.tier_at(count).not_nil!}
    end
  end

  return active
end

def score_comp(champions, trait_data)
  traits = check_active(champions, trait_data)
  score = 0
  if champions.any?{|e| e.name == "Vander"}
    score += 100
  end
  if traits.any?{|e| e[0].name == "Family"}
    score += 100
  end
  traits.each do |trait, count, tier|
    score += 2**tier
  end
  return score
end

RES_FILE = "res/en_us.json"

file = File.open(RES_FILE)
data = JSON.parse(file)
trait_data = data["sets"]["13"]["traits"].as_a.to_json
champion_data = data["sets"]["13"]["champions"].as_a.to_json

traits = Array(Trait).from_json(trait_data).index_by(&.name)
champions = Array(Champion).from_json(champion_data)
champions.reject!{ |champ| champ.traits.empty? }
cbn = champions.index_by(&.name)

unit_count  = ARGV[0]?.try(&.to_i) || 7
super_count = ARGV[1]?.try(&.to_i) || 4
other_count = unit_count - super_count
cost_limit  = ARGV[2]?.try(&.to_i) || 10
min_score  = ARGV[3]?.try(&.to_i) || unit_count

champions.select!{ |champ| champ.cost <= cost_limit }
champions.sort_by!{ |champ| champ.name }
super_units = champions.select{ |champ| champ.traits.size == 3 }
other_units = champions.select{ |champ| champ.traits.size == 2 }
puts "#{super_count.to_s.rjust(2)}x #{super_units.size.to_s.rjust(2)} 3-trait champions"
puts "#{other_count.to_s.rjust(2)}x #{other_units.size.to_s.rjust(2)} 2-trait champions"

results = [] of Tuple(Array(Champion), Int32)

counter : UInt64 = 0
total  = comb(super_units.size, super_count)
total *= comb(other_units.size, other_count)

test_array1 = [] of Champion
test_array2 = [] of Champion
super_units.each_combination(super_count, reuse: test_array1) do |units|
  other_units.each_combination(other_count, reuse: test_array2) do |others|
    others.concat(units)
    score = score_comp(others, traits)
    if score >= min_score
      results << {others.dup, score}
    end
    if counter % 100000 == 0
      percent = (counter / total * 100).format(decimal_places: 2).rjust(6) + "%"
      puts "#{percent} | #{counter.to_s.rjust(10)} | #{score.to_s.rjust(3)} | #{others.map(&.name).join(", ")}"
    end
    counter += 1
    others.clear
  end
  units.clear
end

output = results.sort_by{|e| e[1]}.reverse.first(50).map do |list, count|
  "#{list.map(&.name).join(", ").ljust(30)} - #{count} score"
end

File.write("output.txt", output.join("\n"))

puts "\nTop Ten:"
puts "-" * 80
puts output.first(10).reverse.join("\n")
