// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Tree-shaped diff between two `Repr` values.
priv enum ReprDelta {
  Same(Repr, Array[ReprDelta])
  Different(Repr, Repr)
  Extra1(Repr)
  Extra2(Repr)
}

///|
/// Default max relative error for `DoubleLit` comparisons.
const DEFAULT_MAX_RELATIVE_ERROR : Double = 0.000000000001

///|
/// Absolute value for `Double`.
fn double_abs(x : Double) -> Double {
  if x < 0.0 {
    -x
  } else {
    x
  }
}

///|
/// Maximum of two `Double` values.
fn double_max(x : Double, y : Double) -> Double {
  if x > y {
    x
  } else {
    y
  }
}

///|
/// Relative error metric used for approximate `DoubleLit` comparisons.
fn relative_error(x : Double, y : Double) -> Double {
  if x == y {
    0.0
  } else {
    let denom = double_max(double_abs(x), double_abs(y))
    if denom == 0.0 {
      double_abs(x - y)
    } else {
      double_abs(x - y) / denom
    }
  }
}

///|
/// Compare two node "labels", with float tolerance for `DoubleLit`.
fn info_approx_eq(max_relative_error : Double, x : Repr, y : Repr) -> Bool {
  match (x, y) {
    (UnitLit, UnitLit) => true
    (Integer(x1), Integer(y1)) => x1 == y1
    (DoubleLit(x1), DoubleLit(y1)) =>
      relative_error(x1, y1) <= max_relative_error
    (FloatLit(x1), FloatLit(y1)) => x1 == y1
    (BoolLit(x1), BoolLit(y1)) => x1 == y1
    (CharLit(x1), CharLit(y1)) => x1 == y1
    (StringLit(x1), StringLit(y1)) => x1 == y1
    (Tuple(_), Tuple(_)) => true
    (Array(_), Array(_)) => true
    (Record(_), Record(_)) => true
    (Enum(n1, _), Enum(n2, _)) => n1 == n2
    (Map(_), Map(_)) => true
    (RecordField(n1, _), RecordField(n2, _)) => n1 == n2
    (EnumLabeledArg(n1, _), EnumLabeledArg(n2, _)) => n1 == n2
    (Opaque(n1, _), Opaque(n2, _)) => n1 == n2
    (Literal(x1), Literal(y1)) => x1 == y1
    (MapEntry(_, _), MapEntry(_, _)) => true
    (Omitted, Omitted) => true
    _ => false
  }
}

///|
/// Labels that are treated as structural "containers" (may be collapsed in diffs).
fn info_is_unimportant(info : Repr) -> Bool {
  info is (MapEntry(_, _) | Tuple(_) | Array(_) | Record(_))
}

///|
/// Core `Repr` diff: returns a `ReprDelta` describing differences.
fn diff_info_with(
  left : Repr,
  right : Repr,
  max_relative_error~ : Double,
) -> ReprDelta {
  fn go(left_node : Repr, right_node : Repr) -> ReprDelta {
    if info_approx_eq(max_relative_error, left_node, right_node) {
      let xs = left_node.children()
      let ys = right_node.children()
      let xlen = xs.length()
      let ylen = ys.length()
      let min_len = if xlen < ylen { xlen } else { ylen }
      let children : Array[ReprDelta] = []
      for i in 0.. !(d is Same(_, _))) {
        Different(left_node, right_node)
      } else {
        Same(left_node.shallow(), children)
      }
    } else {
      Different(left_node, right_node)
    }
  }

  go(left, right)
}

///|
/// Diff two `Repr` values.
///
/// Optional parameters:
/// - `max_relative_error?`: relative-error tolerance for `DoubleLit`.
///   - If omitted, the default is `1e-12`.
///   - Larger tolerances make doubles less likely to be reported as different.
fn diff_repr(
  x : Repr,
  y : Repr,
  max_relative_error? : Double = DEFAULT_MAX_RELATIVE_ERROR,
) -> ReprDelta {
  diff_info_with(x, y, max_relative_error~)
}

///|
/// Pretty-print a `ReprDelta`.
///
/// Optional parameters:
/// - `max_depth?`: maximum expansion depth; deeper subtrees are folded.
///   Defaults to `4` when omitted. Values `<= 0` are treated as `1`.
/// - `compact_threshold?`: compact-vs-multiline layout threshold (heuristic one-line vs multiline).
///   Larger values prefer single-line output. Defaults to `30` when omitted.
/// - `use_ansi?`: whether to emit ANSI color escape codes (for +/- markers).
///   Defaults to `true` when omitted.
fn pretty_print_delta(
  d : ReprDelta,
  max_depth? : Int,
  compact_threshold? : Int = default_threshold,
  use_ansi? : Bool = true,
) -> String {
  let max_depth : Int? = match max_depth {
    Some(_) => max_depth
    None => Some(default_max_depth)
  }
  let delta = prune_delta(max_depth, d)
  print_content(render_delta(compact_threshold, use_ansi, delta).no_wrap())
}

///|
/// Prune a `ReprDelta` using max_depth (replacing with `Same(Omitted, [])`).
fn prune_delta(max_depth : Int?, delta : ReprDelta) -> ReprDelta {
  fn go(d : Int, node : ReprDelta) -> ReprDelta {
    if d <= 0 {
      match node {
        Same(label, children) =>
          if children.is_empty() {
            node
          } else if !label.info_adds_depth() {
            Same(label, children.map(child => go(d, child)))
          } else {
            Same(Repr::omitted(), [])
          }
        Different(_, _) | Extra1(_) | Extra2(_) => Same(Repr::omitted(), [])
      }
    } else {
      match node {
        Same(label, children) => {
          let next_depth = if label.info_adds_depth() { d - 1 } else { d }
          Same(label, children.map(child => go(next_depth, child)))
        }
        Different(left, right) =>
          Different(
            left.prune_info(max_depth=Some(d)),
            right.prune_info(max_depth=Some(d)),
          )
        Extra1(x) => Extra1(x.prune_info(max_depth=Some(d)))
        Extra2(x) => Extra2(x.prune_info(max_depth=Some(d)))
      }
    }
  }

  match max_depth {
    None => delta
    Some(depth) => go(Int::max(1, depth), delta)
  }
}

///|
/// Heuristic "size" of a delta for compact-vs-multiline decisions.
fn delta_root_size(delta : ReprDelta) -> Int {
  match delta {
    Same(label, _) => info_size(label)
    _ => 0
  }
}

///|
/// ANSI escape reset.
const ANSI_RESET : String = "\u001b[0m"

///|
/// ANSI escape for red.
const ANSI_RED : String = "\u001b[31m"

///|
/// ANSI escape for green.
const ANSI_GREEN : String = "\u001b[32m"

///|
/// Mark removed content with `-` (and optionally ANSI color).
fn mark_removed(use_ansi : Bool, x : ContentParens) -> ContentParens {
  if use_ansi {
    surround(ANSI_RED + "-", ANSI_RESET, x)
  } else {
    surround("-", "", x)
  }
}

///|
/// Mark added content with `+` (and optionally ANSI color).
fn mark_added(use_ansi : Bool, x : ContentParens) -> ContentParens {
  if use_ansi {
    surround(ANSI_GREEN + "+", ANSI_RESET, x)
  } else {
    surround("+", "", x)
  }
}

///|
/// Render a `ReprDelta` as `Content` with resizing decisions.
fn render_delta(threshold : Int, use_ansi : Bool, delta : ReprDelta) -> Content {
  match delta {
    Same(label, children_delta) => {
      let children = children_delta.map(child => {
        render_delta(threshold, use_ansi, child)
      })
      with_resizing(
        delta_root_size(delta),
        threshold,
        label.pretty_print(children),
      )
    }
    Different(left, right) => {
      let children : Array[Content] = [
        left.render_repr(threshold),
        right.render_repr(threshold),
      ]
      with_resizing(
        0,
        threshold,
        match children {
          [left, right] =>
            no_parens(
              mark_removed(use_ansi, left.no_wrap()) +
              mark_added(use_ansi, right.no_wrap()),
            )
          _ => empty_content()
        },
      )
    }
    Extra1(x) => {
      let children : Array[Content] = [x.render_repr(threshold)]
      with_resizing(
        0,
        threshold,
        match children {
          [x] => no_parens(mark_removed(use_ansi, x.no_wrap()))
          _ => empty_content()
        },
      )
    }
    Extra2(x) => {
      let children : Array[Content] = [x.render_repr(threshold)]
      with_resizing(
        0,
        threshold,
        match children {
          [x] => no_parens(mark_added(use_ansi, x.no_wrap()))
          _ => empty_content()
        },
      )
    }
  }
}