// 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.

///|
/// Try to get this element as a Null
#deprecated("Suggestion: `if json is Null { Some(()) } else { None }`")
pub fn Json::as_null(self : Json) -> Unit? {
  guard self is Null else { return None }
  Some(())
}

///|
/// Try to get this element as a Boolean
#deprecated("Suggestion: `if json is True { Some(true) } else if json is False { Some(false) } else { None }`")
pub fn Json::as_bool(self : Json) -> Bool? {
  match self {
    True => Some(true)
    False => Some(false)
    _ => None
  }
}

///|
/// Try to get this element as a Number
#deprecated("Suggestion: `if json is Number(n) { Some(n) } else { None }`")
pub fn Json::as_number(self : Json) -> Double? {
  guard self is Number(n, ..) else { return None }
  Some(n)
}

///|
/// Try to get this element as a String
#deprecated("Suggestion: `if json is String(s) { Some(s) } else { None }`")
pub fn Json::as_string(self : Json) -> String? {
  guard self is String(s) else { return None }
  Some(s)
}

///|
/// Try to get this element as an Array
#deprecated("Suggestion: `if json is Array(array) { Some(array) } else { None }`")
pub fn Json::as_array(self : Json) -> Array[Json]? {
  guard self is Array(arr) else { return None }
  Some(arr)
}

///|
/// Try to get this element as a Json Array and get the element at the `index` as a Json Value
#deprecated("Suggestion: `if json is Array(array) { array.get(index) } else { None }`")
pub fn Json::item(self : Json, index : Int) -> Json? {
  if self is Array(arr) {
    arr.get(index)
  } else {
    None
  }
}

///|
/// Try to get this element as an Object
#deprecated
pub fn Json::as_object(self : Json) -> Map[String, Json]? {
  guard self is Object(obj) else { return None }
  Some(obj)
}

///|
/// Try to get this element as a Json Object and get the element with the `key` as a Json Value
#deprecated("Suggestion: `if json is Object(obj) { obj.get(key) } else { None }`")
pub fn Json::value(self : Json, key : String) -> Json? {
  if self is Object(obj) {
    obj.get(key)
  } else {
    None
  }
}

///|
fn indent_str(level : Int, indent : Int) -> String {
  if indent == 0 {
    ""
  } else {
    let spaces = indent * level
    match spaces {
      0 => "\n"
      1 => "\n "
      2 => "\n  "
      3 => "\n   "
      4 => "\n    "
      5 => "\n     "
      6 => "\n      "
      7 => "\n       "
      8 => "\n        "
      _ => "\n" + " ".repeat(spaces)
    }
  }
}

///|
/// Internal stack frame used by iterative stringify to avoid recursion
priv enum WriteFrame {
  Array(Array[Json], mut i~ : Int) // (arr, index)
  Object(Iter[(String, Json)], mut first~ : Bool) // (kvs, first)
}

///|
/// A Replacer provides a way to filter and transform JSON object properties during stringification.
/// 
/// Replacers contain a function that takes a property key and value, and returns:
/// - `Some(value)` to include the property in the output (possibly transformed)
/// - `None` to exclude the property from the output
/// 
/// Only applies to object properties, not array elements.
pub struct Replacer {
  priv f : (String, Json) -> Json?
} derive(@debug.Debug)

///|
/// Create a new Replacer with a custom function.
///
/// The function receives `(key, value)` pairs and should return:
/// - `Some(transformed_value)` to include the property
/// - `None` to exclude the property
///
/// ## Example
///
/// ```mbt check
/// test {
///   // Transform numbers and exclude sensitive fields
///   ignore(
///     @json.Replacer((key, value) => {
///       match key {
///         "password" | "secret" => None // exclude sensitive fields
///         _ =>
///           match value {
///             Number(n, ..) => Some(Json::number(n * 2.0)) // double all numbers
///             _ => Some(value) // keep other values as-is
///           }
///       }
///     }),
///   )
/// }
/// ```
#alias(new, deprecated="Use `Replacer()` instead")
pub fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer {
  { f, }
}

///|
/// Create a Replacer that only keeps the specified property keys.
/// All other properties will be excluded from the output.
/// 
/// ## Example
/// 
/// ```mbt check
/// test {
///   let replacer = @json.Replacer::keep(["name", "age", "email"])
///   let json : Json = {
///     "name": "Alice",
///     "age": 30.0,
///     "email": "alice@example.com",
///     "password": "secret",
///   }
///   ignore(json.stringify(replacer~)) // {"name":"Alice","age":30,"email":"alice@example.com"}
/// }
/// ```
pub fn Replacer::keep(array : ArrayView[StringView]) -> Replacer {
  { f: (idx, value) => if array.contains(idx) { Some(value) } else { None } }
}

///|
/// Create a Replacer that excludes the specified property keys.
/// All other properties will be included in the output.
///
/// ## Example
///
/// ```mbt check
/// test {
///   let replacer = @json.Replacer::exclude(["password", "secret", "private"])
///   let json : Json = {
///     "name": "Alice",
///     "age": 30.0,
///     "password": "secret",
///     "email": "alice@example.com",
///   }
///   ignore(json.stringify(replacer~)) // {"name":"Alice","age":30,"email":"alice@example.com"}
/// }
/// ```
pub fn Replacer::exclude(array : ArrayView[StringView]) -> Replacer {
  { f: (idx, value) => if array.contains(idx) { None } else { Some(value) } }
}

///|
/// Convert this Json value to a String
/// - `escape_slash`: Whether to escape '/' as '\/' (default: false)
/// - `indent`: Number of spaces to indent nested structures (default: 0 = non-indented)
/// - `replacer`: An optional Replacer function to transform or filter values during stringification
/// 
/// ## Replacer
/// 
/// The replacer parameter allows you to control which object properties are included in the output
/// and optionally transform values during stringification. Only applies to object properties, not array elements.
/// 
/// ### Creating Replacers
/// 
/// 1. **Replacer(f)** - Create a custom replacer with a function `(String, Json) -> Json?`:
///    - Return `Some(value)` to include the property (possibly transformed)
///    - Return `None` to exclude the property
/// 
/// 2. **Replacer::keep(keys)** - Include only the specified property keys
/// 
/// 3. **Replacer::exclude(keys)** - Exclude the specified property keys
/// 
/// ### Examples
/// 
/// ```mbt check
/// test {
///   let json : Json = { "a": 1.0, "b": 2.0, "c": 3.0, "password": "secret" }
///
///   // Keep only specific keys
///   let keep_replacer = @json.Replacer::keep(["a", "c"])
///   ignore(json.stringify(replacer=keep_replacer)) // {"a":1,"c":3}
///
///   // Exclude sensitive keys
///   let exclude_replacer = @json.Replacer::exclude(["password"])
///   ignore(json.stringify(replacer=exclude_replacer)) // {"a":1,"b":2,"c":3}
///
///   // Custom transformation
///   let transform_replacer = @json.Replacer((_key, value) => {
///     match value {
///       Number(n, ..) => Some(Json::number(n * 10.0)) // multiply numbers by 10
///       _ => Some(value) // keep other values unchanged
///     }
///   })
///   ignore(json.stringify(replacer=transform_replacer)) // {"a":10,"b":20,"c":30,"password":"secret"}
///
///   // Filter and transform
///   let filter_replacer = @json.Replacer((key, value) => {
///     match key {
///       "password" => None // exclude password
///       _ =>
///         match value {
///           Number(n, ..) => Some(Json::number(n + 100.0)) // add 100 to numbers
///           _ => Some(value)
///         }
///     }
///   })
///   ignore(json.stringify(replacer=filter_replacer)) // {"a":101,"b":102,"c":103}
/// }
/// ```
/// 
/// ### Nested Objects
/// 
/// Replacers work recursively on nested objects:
/// 
/// ```mbt check
/// test {
///   let nested : Json = {
///     "user": { "name": "Alice", "password": "secret" },
///     "id": 123.0,
///   }
///   let safe_replacer = @json.Replacer::exclude(["password"])
///   ignore(nested.stringify(replacer=safe_replacer)) // {"user":{"name":"Alice"},"id":123}
/// }
/// ```
pub fn Json::stringify(
  self : Json,
  escape_slash? : Bool = false,
  indent? : Int = 0,
  replacer? : Replacer,
) -> String {
  let buf = StringBuilder(size_hint=0)

  // Explicit stack to replace recursive calls
  let stack : Array[WriteFrame] = []
  let mut depth = 0
  for x = Some(self) {
    match x {
      Some(value) => {
        match value {
          Object(members) =>
            if members.is_empty() {
              buf.write_string("{}")
            } else {
              depth += 1
              buf.write_char('{')
              buf.write_string(indent_str(depth, indent))
              // After child value printed, we resume from this frame
              stack.push(Object(members.iter(), first=true))
            }
          Array(arr) =>
            if arr.is_empty() {
              buf.write_string("[]")
            } else {
              depth += 1
              buf.write_char('[')
              buf.write_string(indent_str(depth, indent))
              stack.push(Array(arr, i=0))
            }
          String(s) => {
            buf.write_char('\"')
            buf.write_string(escape(s, escape_slash~))
            buf.write_char('\"')
          }
          Number(n, repr~) =>
            match repr {
              None => buf.write_object(n)
              Some(r) => buf.write_string(r)
            }
          True => buf.write_string("true")
          False => buf.write_string("false")
          Null => buf.write_string("null")
        }
        continue None
      }
      None =>
        // No current node to write; try to resume a pending container
        match stack {
          [] => break
          [.., Array(arr, i~) as frame] =>
            if i < arr.length() {
              let element = arr[i]
              frame.i = i + 1
              if i > 0 {
                buf.write_char(',')
                buf.write_string(indent_str(depth, indent))
              }
              continue Some(element)
            } else {
              depth -= 1
              ignore(stack.pop())
              buf.write_string(indent_str(depth, indent))
              buf.write_char(']')
              continue None
            }
          [.., Object(iterator, first~) as frame] =>
            match iterator.next() {
              Some((k, v)) => {
                let mut v2 = v
                if replacer is Some(replacer) {
                  if (replacer.f)(k, v) is Some(v) {
                    v2 = v
                  } else {
                    continue None
                  }
                }
                if !first {
                  buf.write_char(',')
                  buf.write_string(indent_str(depth, indent))
                }
                buf.write_char('\"')
                buf.write_string(escape(k, escape_slash~))
                buf.write_char('\"')
                buf.write_char(':')
                if indent > 0 {
                  buf.write_char(' ')
                }
                frame.first = false
                continue Some(v2)
              }
              None => {
                depth -= 1
                ignore(stack.pop())
                buf.write_string(indent_str(depth, indent))
                buf.write_char('}')
                continue None
              }
            }
        }
    }
  }
  buf.to_string()
}

///|
fn escape(str : String, escape_slash~ : Bool) -> String {
  let buf = StringBuilder(size_hint=str.length())
  for c in str {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '/' =>
        if escape_slash {
          buf.write_string("\\/")
        } else {
          buf.write_char(c)
        }
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\b' => buf.write_string("\\b")
      '\t' => buf.write_string("\\t")
      _ => {
        let code = c.to_int()
        if code == 0x0C {
          buf.write_string("\\f")
        } else if code < ' ' {
          buf.write_string("\\u00")
          buf.write_string(code.to_byte().to_hex())
        } else {
          buf.write_char(c)
        }
      }
    }
  }
  buf.to_string()
}

///|
/// Transform a JSON value by applying a replacer recursively to all object properties.
/// 
/// Unlike `stringify(replacer~)` which only affects the string output, `transform()` 
/// returns a new JSON value with properties filtered and transformed according to the replacer.
/// 
/// This is useful when you want to create a modified JSON structure that can be further 
/// processed, rather than just converting to a string.
/// 
/// ## Example
/// 
/// ```mbt check
/// test {
///   let json : Json = {
///     "user": { "name": "Alice", "password": "secret", "age": 30.0 },
///     "id": 123.0,
///   }
///
///   // Remove sensitive data and double numeric values  
///   ignore(
///     json.transform(
///       Replacer((key, value) => {
///         match key {
///           "password" => None // exclude password fields
///           _ =>
///             match value {
///               Number(n, ..) => Some(Json::number(n * 2.0)) // double numbers
///               _ => Some(value) // keep other values
///             }
///         }
///       }),
///     ),
///   )
///   // Result: { "user": { "name": "Alice", "age": 60 }, "id": 246 }
/// }
/// ```
/// 
/// ## Behavior
/// 
/// - Recursively applies the replacer to all nested objects
/// - Non-object values (arrays, strings, numbers, etc.) are returned unchanged
/// - The original JSON value is not modified; a new value is returned
pub fn Json::transform(self : Self, replacer : Replacer) -> Json {
  match self {
    Object(members) =>
      members
      .iter()
      .filter_map(pair => {
        let (k, v) = pair
        if (replacer.f)(k, v) is Some(v2) {
          Some((k, v2.transform(replacer)))
        } else {
          None
        }
      })
      |> Map::from_iter()
      |> Object
    Array(members) => Array(members.map(m => m.transform(replacer)))
    value => value
  }
}

///|
/// Useful for json interpolation
pub impl ToJson for Json with fn to_json(self) {
  self
}

///|
/// Inspect JSON value with snapshot-friendly formatting.
#callsite(autofill(args_loc, loc))
#alias(inspect, deprecated="Use `json_inspect` without package name instead.")
pub fn json_inspect(
  obj : &ToJson,
  content? : Json,
  loc~ : SourceLoc,
  args_loc~ : ArgsLoc,
) -> Unit raise InspectError {
  let loc = loc.to_json_string()
  let args_loc = args_loc.to_json()
  let actual = obj.to_json().stringify(escape_slash=false)
  let want = match content {
    Some(x) | (None with x = "".to_json()) => x.stringify(escape_slash=false)
  }
  if actual != want {
    raise InspectError(
      (
        $|@EXPECT_FAILED {"loc": \{loc}, "args_loc": \{args_loc}, "expect": \{want.escape()}, "actual": \{actual.escape()}, "mode": "json"}
      ),
    )
  }
}