///|
pub(all) enum MeasureKind {
  Mass
  Volume
  Count
} derive(Eq, Debug)

///|
pub(all) enum MeasureUnit {
  Gram
  Kilogram
  Milliliter
  Liter
  Teaspoon
  Tablespoon
  Cup
  Piece
  Slice
  Pinch
} derive(Eq, Debug)

///|
pub fn unit_kind(unit : MeasureUnit) -> MeasureKind {
  match unit {
    Gram | Kilogram => Mass
    Milliliter | Liter | Teaspoon | Tablespoon | Cup => Volume
    Piece | Slice | Pinch => Count
  }
}

///|
pub fn unit_text(unit : MeasureUnit) -> String {
  match unit {
    Gram => "g"
    Kilogram => "kg"
    Milliliter => "ml"
    Liter => "l"
    Teaspoon => "tsp"
    Tablespoon => "tbsp"
    Cup => "cup"
    Piece => "piece"
    Slice => "slice"
    Pinch => "pinch"
  }
}

///|
fn canonical_factor(unit : MeasureUnit) -> Int {
  match unit {
    Gram => 1
    Kilogram => 1000
    Milliliter => 1
    Liter => 1000
    Teaspoon => 5
    Tablespoon => 15
    Cup => 240
    Piece => 1
    Slice => 1
    Pinch => 1
  }
}

///|
pub(all) struct Quantity {
  amount : Int
  unit : MeasureUnit
} derive(Eq, Debug)

///|
pub fn Quantity::Quantity(
  amount? : Int = 0,
  unit? : MeasureUnit = Gram,
) -> Quantity {
  { amount, unit }
}

///|
pub(all) struct Nutrition {
  kcal : Int
  protein_g : Int
  fat_g : Int
  carb_g : Int
  fiber_g : Int
  sodium_mg : Int
} derive(Eq, Debug)

///|
pub fn Nutrition::Nutrition(
  kcal? : Int = 0,
  protein_g? : Int = 0,
  fat_g? : Int = 0,
  carb_g? : Int = 0,
  fiber_g? : Int = 0,
  sodium_mg? : Int = 0,
) -> Nutrition {
  { kcal, protein_g, fat_g, carb_g, fiber_g, sodium_mg }
}

///|
pub(all) struct Ingredient {
  name : String
  group : String
  quantity : Quantity
  nutrition : Nutrition
  cost_cents : Int
  allergens : Array[String]
  tags : Array[String]
} derive(Eq, Debug)

///|
pub fn Ingredient::Ingredient(
  name? : String = "",
  group? : String = "",
  quantity? : Quantity = Quantity(),
  nutrition? : Nutrition = Nutrition(),
  cost_cents? : Int = 0,
  allergens? : Array[String] = [],
  tags? : Array[String] = [],
) -> Ingredient {
  { name, group, quantity, nutrition, cost_cents, allergens, tags }
}

///|
pub(all) struct Recipe {
  title : String
  servings : Int
  ingredients : Array[Ingredient]
  steps : Array[String]
  notes : Array[String]
} derive(Eq, Debug)

///|
pub fn Recipe::Recipe(
  title? : String = "",
  servings? : Int = 1,
  ingredients? : Array[Ingredient] = [],
  steps? : Array[String] = [],
  notes? : Array[String] = [],
) -> Recipe {
  { title, servings, ingredients, steps, notes }
}

///|
pub(all) struct PantryItem {
  name : String
  quantity : Quantity
  expires_in_days : Int
  opened : Bool
  preferred : Bool
} derive(Eq, Debug)

///|
pub fn PantryItem::PantryItem(
  name? : String = "",
  quantity? : Quantity = Quantity(),
  expires_in_days? : Int = 30,
  opened? : Bool = false,
  preferred? : Bool = false,
) -> PantryItem {
  { name, quantity, expires_in_days, opened, preferred }
}

///|
pub(all) struct DietProfile {
  avoid_allergens : Array[String]
  prefer_tags : Array[String]
  dislike_tags : Array[String]
  max_total_cost_cents : Int
  max_total_kcal : Int
  min_total_protein_g : Int
} derive(Eq, Debug)

///|
pub fn DietProfile::DietProfile(
  avoid_allergens? : Array[String] = [],
  prefer_tags? : Array[String] = [],
  dislike_tags? : Array[String] = [],
  max_total_cost_cents? : Int = 0,
  max_total_kcal? : Int = 0,
  min_total_protein_g? : Int = 0,
) -> DietProfile {
  {
    avoid_allergens,
    prefer_tags,
    dislike_tags,
    max_total_cost_cents,
    max_total_kcal,
    min_total_protein_g,
  }
}

///|
pub(all) struct Substitution {
  source : String
  target : Ingredient
  ratio_percent : Int
  reason : String
  removes_allergens : Array[String]
  adds_tags : Array[String]
} derive(Eq, Debug)

///|
pub fn Substitution::Substitution(
  source? : String = "",
  target? : Ingredient = Ingredient(),
  ratio_percent? : Int = 100,
  reason? : String = "",
  removes_allergens? : Array[String] = [],
  adds_tags? : Array[String] = [],
) -> Substitution {
  { source, target, ratio_percent, reason, removes_allergens, adds_tags }
}

///|
pub(all) struct ShoppingLine {
  name : String
  needed : Quantity
  available : Quantity
  buy : Quantity
  estimated_cost_cents : Int
} derive(Eq, Debug)

///|
pub fn ShoppingLine::ShoppingLine(
  name? : String = "",
  needed? : Quantity = Quantity(),
  available? : Quantity = Quantity(),
  buy? : Quantity = Quantity(),
  estimated_cost_cents? : Int = 0,
) -> ShoppingLine {
  { name, needed, available, buy, estimated_cost_cents }
}

///|
pub(all) struct PantryWarning {
  code : String
  message : String
  severity : String
} derive(Eq, Debug)

///|
pub fn PantryWarning::PantryWarning(
  code? : String = "",
  message? : String = "",
  severity? : String = "info",
) -> PantryWarning {
  { code, message, severity }
}

///|
pub(all) struct PantryPlan {
  verdict : String
  recipe_title : String
  servings : Int
  total_cost_cents : Int
  total_kcal : Int
  total_protein_g : Int
  allergen_hit_count : Int
  missing_count : Int
  substitution_count : Int
  shopping : Array[ShoppingLine]
  substitutions : Array[Substitution]
  warnings : Array[PantryWarning]
} derive(Eq, Debug)

///|
pub fn PantryPlan::PantryPlan(
  verdict? : String = "",
  recipe_title? : String = "",
  servings? : Int = 1,
  total_cost_cents? : Int = 0,
  total_kcal? : Int = 0,
  total_protein_g? : Int = 0,
  allergen_hit_count? : Int = 0,
  missing_count? : Int = 0,
  substitution_count? : Int = 0,
  shopping? : Array[ShoppingLine] = [],
  substitutions? : Array[Substitution] = [],
  warnings? : Array[PantryWarning] = [],
) -> PantryPlan {
  {
    verdict,
    recipe_title,
    servings,
    total_cost_cents,
    total_kcal,
    total_protein_g,
    allergen_hit_count,
    missing_count,
    substitution_count,
    shopping,
    substitutions,
    warnings,
  }
}

///|
fn lower(text : String) -> String {
  text.to_lower()
}

///|
fn same_name(a : String, b : String) -> Bool {
  lower(a.trim().to_owned()) == lower(b.trim().to_owned())
}

///|
fn text_in(values : Array[String], target : String) -> Bool {
  for value in values {
    if same_name(value, target) {
      return true
    }
  }
  false
}

///|
fn count_intersection(left : Array[String], right : Array[String]) -> Int {
  let mut count = 0
  for item in left {
    if text_in(right, item) {
      count += 1
    }
  }
  count
}

///|
fn join_text(values : Array[String]) -> String {
  let out = StringBuilder()
  for i in 0.. 0 {
      out.write_string(", ")
    }
    out.write_string(values[i])
  }
  out.to_string()
}

///|
fn money_text(cents : Int) -> String {
  let yuan = cents / 100
  let fen = cents % 100
  if fen < 10 {
    yuan.to_string() + ".0" + fen.to_string()
  } else {
    yuan.to_string() + "." + fen.to_string()
  }
}