///|
pub(all) struct FeatureProfile {
  name : String
  value_kind : String
  domain_size : Int
  mutable_values : Int
  current_index : Int
  cost : Int
  locked : Bool
  referenced : Bool
  duplicate_values : Int
} derive(Debug, Eq)

///|
pub(all) struct ModelProfile {
  policy_name : String
  features : Array[FeatureProfile]
  goal_variables : Array[String]
  expression_nodes : Int
  expression_depth : Int
  comparisons : Int
  mutable_features : Int
  locked_features : Int
  zero_cost_mutable : Int
  singleton_domains : Int
  duplicate_domain_values : Int
  search_space : Int
} derive(Debug, Eq)

///|
pub fn Policy::profile(self : Policy) -> ModelProfile {
  let stats = self.goal.stats()
  let features : Array[FeatureProfile] = []
  let mut mutable_features = 0
  let mut locked_features = 0
  let mut zero_cost_mutable = 0
  let mut singleton_domains = 0
  let mut duplicate_domain_values = 0
  for item in self.features {
    let duplicate_values = count_duplicate_values(item.domain)
    duplicate_domain_values = duplicate_domain_values + duplicate_values
    if item.locked {
      locked_features = locked_features + 1
    } else {
      mutable_features = mutable_features + 1
      if item.cost == 0 && item.domain.length() > 1 {
        zero_cost_mutable = zero_cost_mutable + 1
      }
    }
    if item.domain.length() <= 1 {
      singleton_domains = singleton_domains + 1
    }
    features.push({
      name: item.name,
      value_kind: item.current.kind(),
      domain_size: item.domain.length(),
      mutable_values: feature_mutable_values(item),
      current_index: feature_current_index(item),
      cost: item.cost,
      locked: item.locked,
      referenced: stats.variables.contains(item.name),
      duplicate_values,
    })
  }
  {
    policy_name: self.name,
    features,
    goal_variables: stats.variables,
    expression_nodes: stats.nodes,
    expression_depth: stats.depth,
    comparisons: stats.comparisons,
    mutable_features,
    locked_features,
    zero_cost_mutable,
    singleton_domains,
    duplicate_domain_values,
    search_space: self.search_space_size(),
  }
}

///|
fn feature_mutable_values(item : Feature) -> Int {
  if item.locked {
    0
  } else {
    let mut count = 0
    for candidate in item.domain {
      if candidate != item.current {
        count = count + 1
      }
    }
    count
  }
}

///|
fn feature_current_index(item : Feature) -> Int {
  for index, value in item.domain {
    if value == item.current {
      return index
    }
  }
  -1
}

///|
fn count_duplicate_values(values : Array[Value]) -> Int {
  let mut duplicates = 0
  for index, value in values {
    let mut seen = false
    for previous in 0.. Bool {
  !self.locked && self.mutable_values > 0 && self.referenced
}

///|
pub fn FeatureProfile::label(self : FeatureProfile) -> String {
  if self.locked {
    "locked"
  } else if self.mutable_values == 0 {
    "constant"
  } else if !self.referenced {
    "unreferenced"
  } else if self.cost == 0 {
    "free"
  } else {
    "actionable"
  }
}

///|
pub fn ModelProfile::actionable_features(self : ModelProfile) -> Array[String] {
  let result : Array[String] = []
  for item in self.features {
    if item.actionable() {
      result.push(item.name)
    }
  }
  result
}

///|
pub fn ModelProfile::unreferenced_features(
  self : ModelProfile,
) -> Array[String] {
  let result : Array[String] = []
  for item in self.features {
    if !item.referenced {
      result.push(item.name)
    }
  }
  result
}

///|
pub fn ModelProfile::locked_referenced_features(
  self : ModelProfile,
) -> Array[String] {
  let result : Array[String] = []
  for item in self.features {
    if item.locked && item.referenced {
      result.push(item.name)
    }
  }
  result
}

///|
pub fn ModelProfile::to_markdown(self : ModelProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("# Model profile: \{self.policy_name}\n\n")
  out.write_string("- Search space: \{self.search_space}\n")
  out.write_string("- Mutable features: \{self.mutable_features}\n")
  out.write_string("- Locked features: \{self.locked_features}\n")
  out.write_string("- Expression nodes: \{self.expression_nodes}\n")
  out.write_string("- Expression depth: \{self.expression_depth}\n")
  out.write_string("- Comparisons: \{self.comparisons}\n")
  out.write_string("- Zero-cost mutable features: \{self.zero_cost_mutable}\n")
  out.write_string("- Singleton domains: \{self.singleton_domains}\n")
  out.write_string(
    "- Duplicate domain values: \{self.duplicate_domain_values}\n\n",
  )
  out.write_string(
    "| feature | kind | domain | mutable values | current index | cost | status | referenced | duplicates |\n",
  )
  out.write_string(
    "| --- | --- | ---: | ---: | ---: | ---: | --- | --- | ---: |\n",
  )
  for item in self.features {
    out.write_string(
      "| \{profile_cell(item.name)} | \{item.value_kind} | \{item.domain_size} | \{item.mutable_values} | \{item.current_index} | \{item.cost} | \{item.label()} | \{item.referenced} | \{item.duplicate_values} |\n",
    )
  }
  out.to_string()
}

///|
pub fn ModelProfile::to_json(self : ModelProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"policy\":\"\{profile_json_escape(self.policy_name)}\",")
  out.write_string("\"search_space\":\{self.search_space},")
  out.write_string("\"mutable_features\":\{self.mutable_features},")
  out.write_string("\"locked_features\":\{self.locked_features},")
  out.write_string("\"expression_nodes\":\{self.expression_nodes},")
  out.write_string("\"expression_depth\":\{self.expression_depth},")
  out.write_string("\"comparisons\":\{self.comparisons},")
  out.write_string("\"zero_cost_mutable\":\{self.zero_cost_mutable},")
  out.write_string("\"singleton_domains\":\{self.singleton_domains},")
  out.write_string(
    "\"duplicate_domain_values\":\{self.duplicate_domain_values},",
  )
  out.write_string("\"goal_variables\":[")
  for index, name in self.goal_variables {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("\"\{profile_json_escape(name)}\"")
  }
  out.write_string("],\"features\":[")
  for index, item in self.features {
    if index > 0 {
      out.write_char(',')
    }
    out.write_string("{")
    out.write_string("\"name\":\"\{profile_json_escape(item.name)}\",")
    out.write_string("\"kind\":\"\{item.value_kind}\",")
    out.write_string("\"domain_size\":\{item.domain_size},")
    out.write_string("\"mutable_values\":\{item.mutable_values},")
    out.write_string("\"current_index\":\{item.current_index},")
    out.write_string("\"cost\":\{item.cost},")
    out.write_string("\"locked\":\{item.locked},")
    out.write_string("\"referenced\":\{item.referenced},")
    out.write_string("\"duplicate_values\":\{item.duplicate_values},")
    out.write_string("\"label\":\"\{item.label()}\"")
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}

///|
fn profile_cell(value : String) -> String {
  value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}

///|
fn profile_json_escape(text : String) -> String {
  let out = StringBuilder::new()
  for ch in text {
    match ch {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => out.write_char(ch)
    }
  }
  out.to_string()
}