///|
/// The refuter that checks the conditional independences of a graph against
/// the data.
///
/// This is the port of `dowhy.causal_refuters.graph_refuter`. The upstream
/// tests every conditional independence the graph entails, with the partial
/// correlation of the continuous data and with the conditional mutual
/// information of the discrete data, and it reports how many of them the data
/// satisfies.

///|
/// A conditional independence of a graph.
///
/// This is the port of the implication of the upstream, which is a pair of
/// variables and the variables that are conditioned on.
pub struct Implication {
  /// The first variable of the implication.
  x : String
  /// The second variable of the implication.
  y : String
  /// The variables the implication is conditioned on.
  z : Array[String]
}

///|
/// A conditional independence of a graph.
///
/// This is the port of the implication of the upstream, which is a pair of
/// variables and the variables that are conditioned on.
pub fn Implication::new(
  x : String,
  y : String,
  z : Array[String],
) -> Implication {
  { x, y, z: z.copy(), }
}

///|
/// The kind of a column of the data.
///
/// The upstream tells the columns of its data apart by their type: a column
/// of integers is discrete, a column of floating point numbers is continuous,
/// and a column of integers that holds only zeros and ones is binary.
pub enum VariableKind {
  /// A column of numbers.
  Continuous
  /// A column of categories.
  Discrete
  /// A column of categories that holds only zeros and ones.
  Binary
}

///|
/// The names of the tests of the conditional independence.
///
/// This is the port of the dictionary `independence_test` of the upstream, which
/// names the test of the continuous data and the test of the discrete data.
pub struct IndependenceTests {
  /// The name of the test of the continuous data.
  test_for_continuous : String
  /// The name of the test of the discrete data.
  test_for_discrete : String
}

///|
/// The names of the tests of the conditional independence.
pub fn IndependenceTests::new(
  test_for_continuous : String,
  test_for_discrete : String,
) -> IndependenceTests {
  { test_for_continuous, test_for_discrete, }
}

///|
/// The name of the test of the continuous data.
pub fn IndependenceTests::continuous(self : IndependenceTests) -> String {
  self.test_for_continuous
}

///|
/// The name of the test of the discrete data.
pub fn IndependenceTests::discrete(self : IndependenceTests) -> String {
  self.test_for_discrete
}

///|
/// The result of the test of one conditional independence.
pub struct IndependenceTest {
  /// The value of the test: the p-value of the partial correlation of the
  /// continuous data and the conditional mutual information of the discrete
  /// data.
  ///
  /// It is `None` when the upstream could not run the test.
  value : Double?
  /// Whether the independence holds.
  ///
  /// It is `None` when the upstream could not run the test, the way the
  /// upstream writes the text `"NotImplemented"` in the place of the verdict.
  holds : Bool?
}

///|
/// The result of the refutation of a graph.
pub struct GraphRefutation {
  /// The name of the test of the discrete data.
  method_name_discrete : String
  /// The name of the test of the continuous data.
  method_name_continuous : String
  /// The number of the conditional independences the model entails.
  number_of_constraints_model : Int32
  /// The number of the conditional independences the data satisfies.
  number_of_constraints_satisfied : Int32
  /// Whether the refutation passed.
  ///
  /// The upstream sets it when every conditional independence of the model
  /// holds or when none of them fails, and it leaves it unset when it could
  /// not run a test.
  refutation_result : Bool?
  /// The result of the test of every conditional independence, keyed by the
  /// implication.
  results : Map[String, IndependenceTest]
}

///|
/// The value of the test of one conditional independence.
///
/// It is `None` when the upstream could not run the test.
pub fn IndependenceTest::value(self : IndependenceTest) -> Double? {
  self.value
}

///|
/// Whether the independence of one conditional independence holds.
///
/// It is `None` when the upstream could not run the test.
pub fn IndependenceTest::holds(self : IndependenceTest) -> Bool? {
  self.holds
}

///|
/// The refuter that checks the conditional independences of a graph.
///
/// This is the port of `dowhy.causal_refuters.graph_refuter.GraphRefuter`.
///
/// The upstream copies the data and keeps the results in the refuter, and the
/// port does the same. The results live in the arrays and in the map of the
/// refuter, and a container of the toolchain is mutated in place, so the
/// results of the refutation are the results of the refuter that produced
/// them.
pub struct GraphRefuter {
  /// The data the conditional independences are tested on.
  data : @api.DataFrame
  /// The name of the test of the discrete data.
  method_name_discrete : String
  /// The name of the test of the continuous data.
  method_name_continuous : String
  /// The implications that hold false for the data.
  false_implications : Array[Implication]
  /// The implications that hold true for the data.
  true_implications : Array[Implication]
  /// The result of the test of every implication.
  results : Map[String, IndependenceTest]
}

///|
/// The name of the test of the discrete data.
pub fn GraphRefutation::discrete_method(self : GraphRefutation) -> String {
  self.method_name_discrete
}

///|
/// The name of the test of the continuous data.
pub fn GraphRefutation::continuous_method(self : GraphRefutation) -> String {
  self.method_name_continuous
}

///|
/// The number of the conditional independences the model entails.
pub fn GraphRefutation::constraints_of_the_model(
  self : GraphRefutation,
) -> Int32 {
  self.number_of_constraints_model
}

///|
/// The number of the conditional independences the data satisfies.
pub fn GraphRefutation::constraints_satisfied_by_the_data(
  self : GraphRefutation,
) -> Int32 {
  self.number_of_constraints_satisfied
}

///|
/// Whether the refutation passed.
///
/// It is `None` when the refutation has not run yet.
pub fn GraphRefutation::refutation_result(self : GraphRefutation) -> Bool? {
  self.refutation_result
}

///|
/// The result of the test of every conditional independence.
pub fn GraphRefutation::results(
  self : GraphRefutation,
) -> Map[String, IndependenceTest] {
  self.results.copy()
}

///|
/// A refuter of the conditional independences of a graph.
///
/// This is the port of the constructor of
/// `dowhy.causal_refuters.graph_refuter.GraphRefuter`.
pub fn GraphRefuter::new(
  data : @api.DataFrame,
  method_name_discrete : String,
  method_name_continuous : String,
) -> GraphRefuter {
  {
    data,
    method_name_discrete,
    method_name_continuous,
    false_implications: [],
    true_implications: [],
    results: Map([]),
  }
}

///|
/// The results of the tests of the implications.
pub fn GraphRefuter::results(
  self : GraphRefuter,
) -> Map[String, IndependenceTest] {
  self.results.copy()
}

///|
/// The implications that hold false for the data.
pub fn GraphRefuter::false_implications(
  self : GraphRefuter,
) -> Array[Implication] {
  self.false_implications.copy()
}

///|
/// The implications that hold true for the data.
pub fn GraphRefuter::true_implications(
  self : GraphRefuter,
) -> Array[Implication] {
  self.true_implications.copy()
}

///|
/// Test the given conditional independences against the data.
///
/// This is the port of
/// `dowhy.causal_refuters.graph_refuter.GraphRefuter.refute_model`.
///
/// The upstream tests an implication with the partial correlation when the
/// data of the implication is continuous, and with the conditional mutual
/// information when it is discrete. A binary column is a discrete column of
/// the upstream, so it is tested with the conditional mutual information when
/// it is one of the two variables and with the partial correlation when it is
/// one of the variables that are conditioned on.
pub fn GraphRefuter::refute_model(
  self : GraphRefuter,
  constraints : Array[Implication],
) -> GraphRefutation {
  for constraint in constraints {
    self.test_implication(constraint)
  }
  let passed = self.set_refutation_result(constraints.length())
  {
    method_name_discrete: self.method_name_discrete,
    method_name_continuous: self.method_name_continuous,
    number_of_constraints_model: constraints.length(),
    number_of_constraints_satisfied: self.true_implications.length(),
    refutation_result: passed,
    results: self.results.copy(),
  }
}

///|
/// Test one conditional independence against the data.
fn GraphRefuter::test_implication(
  self : GraphRefuter,
  constraint : Implication,
) -> Unit {
  let kind_x = self.kind_of(constraint.x)
  let kind_y = self.kind_of(constraint.y)
  let kinds : Array[VariableKind] = []
  for name in constraint.z {
    kinds.push(self.kind_of(name))
  }
  let all_continuous = is_continuous_kind(kind_x) &&
    is_continuous_kind(kind_y) &&
    kinds.all(fn(kind) { is_continuous_kind(kind) })
  let all_discrete = is_discrete_kind(kind_x) &&
    is_discrete_kind(kind_y) &&
    kinds.all(fn(kind) { is_discrete_kind(kind) })
  let all_continuous_or_binary = is_continuous_or_binary_kind(kind_x) &&
    is_continuous_or_binary_kind(kind_y) &&
    kinds.all(fn(kind) { is_continuous_or_binary_kind(kind) })
  if all_continuous {
    if self.method_name_continuous == "" ||
      self.method_name_continuous == "partial_correlation" {
      self.partial_correlation(constraint)
    }
    return
  }
  if all_discrete {
    if self.method_name_discrete == "" ||
      self.method_name_discrete == "conditional_mutual_information" {
      self.conditional_mutual_information(constraint)
    }
    return
  }
  if all_continuous_or_binary {
    self.partial_correlation(constraint)
    return
  }
  if kinds.all(fn(kind) { is_discrete_kind(kind) }) &&
    (is_discrete_kind(kind_x) || is_discrete_kind(kind_y)) {
    self.conditional_mutual_information(constraint)
    return
  }
  if is_discrete_kind(kind_x) && is_discrete_kind(kind_y) {
    self.conditional_mutual_information(constraint)
    return
  }
  // The upstream reports the implication it cannot test, and it writes the
  // text "NotImplemented" in the place of the verdict.
  self.results.set(implication_key(constraint), { value: None, holds: None, })
}

///|
/// Test one conditional independence with the partial correlation.
///
/// This is the port of
/// `dowhy.causal_refuters.graph_refuter.GraphRefuter.partial_correlation`.
fn GraphRefuter::partial_correlation(
  self : GraphRefuter,
  constraint : Implication,
) -> Unit {
  let names = [constraint.x, constraint.y] + constraint.z
  let columns : Array[Array[Double]] = []
  for name in names {
    columns.push(numeric_values(self.data, name))
  }
  let conditioning : Array[Int32] = []
  for i in 2.. Unit {
  let conditioning : Array[Array[String]] = []
  for name in constraint.z {
    conditioning.push(text_values(self.data, name))
  }
  let information = @utils.conditional_mi(conditioning)
  if information <= 0.05 {
    self.true_implications.push(constraint)
    self.results.set(implication_key(constraint), {
      value: Some(information),
      holds: Some(true),
    })
  } else {
    self.false_implications.push(constraint)
    self.results.set(implication_key(constraint), {
      value: Some(information),
      holds: Some(false),
    })
  }
}

///|
/// The result of the refutation, once every implication has been tested.
///
/// This is the port of
/// `dowhy.causal_refuters.graph_refuter.GraphRefuter.set_refutation_result`.
fn GraphRefuter::set_refutation_result(
  self : GraphRefuter,
  number_of_constraints_model : Int32,
) -> Bool? {
  if self.true_implications.length() == number_of_constraints_model {
    return Some(true)
  }
  if self.false_implications.length() == 0 {
    return Some(true)
  }
  Some(false)
}

///|
/// The kind of the given column of the data.
fn GraphRefuter::kind_of(self : GraphRefuter, name : String) -> VariableKind {
  match self.data.column(name) {
    Some(@api.Column::Discrete(values)) =>
      if is_binary_text(values) {
        Binary
      } else {
        Discrete
      }
    _ => Continuous
  }
}

///|
/// The key of an implication.
///
/// The upstream keys the results of the implications with the pair of the two
/// variables and with the variables that are conditioned on.
pub fn implication_key(implication : Implication) -> String {
  let mut key = implication.x + "|" + implication.y
  for name in implication.z {
    key = key + "|" + name
  }
  key
}

///|
/// Whether a column of categories holds only zeros and ones.
fn is_binary_text(values : Array[String]) -> Bool {
  for value in values {
    if value != "0" && value != "1" {
      return false
    }
  }
  true
}

///|
/// The values of a column, as numbers.
///
/// The upstream reads the columns of its integer columns as numbers, and the
/// columns of the port hold the categories of a discrete column as text.
fn numeric_values(data : @api.DataFrame, name : String) -> Array[Double] {
  match data.column(name) {
    Some(@api.Column::Numeric(values)) => values
    Some(@api.Column::Discrete(values)) => {
      let numbers : Array[Double] = []
      for value in values {
        numbers.push(if value == "1" { 1.0 } else { 0.0 })
      }
      numbers
    }
    None => []
  }
}

///|
/// The text of the values of a column.
fn text_values(data : @api.DataFrame, name : String) -> Array[String] {
  match data.column(name) {
    Some(@api.Column::Discrete(values)) => values
    Some(@api.Column::Numeric(values)) => {
      let texts : Array[String] = []
      for value in values {
        texts.push(value.to_string())
      }
      texts
    }
    None => []
  }
}

///|
/// Whether the kind is a continuous one.
fn is_continuous_kind(kind : VariableKind) -> Bool {
  match kind {
    Continuous => true
    Discrete => false
    Binary => false
  }
}

///|
/// Whether the kind is a discrete one.
fn is_discrete_kind(kind : VariableKind) -> Bool {
  match kind {
    Continuous => false
    Discrete => true
    Binary => true
  }
}

///|
/// Whether the kind is a continuous one or a binary one.
fn is_continuous_or_binary_kind(kind : VariableKind) -> Bool {
  match kind {
    Continuous => true
    Discrete => false
    Binary => true
  }
}