///|
pub(all) enum Js {
  Nl
  Bl(Bool)
  Nu(Double)
  St(String)
  Ar(Array[Js])
  Ob(Map[String, Js])
} derive(Debug)

///|
fn emp(v : Js, o : Bool) -> Bool {
  match v {
    Nl => true
    St(s) => s == ""
    Nu(n) => o && n == 0.0
    Ar(a) => a.is_empty()
    Ob(m) => m.is_empty()
    Bl(_) => false
  }
}

///|
fn d(v : Js, o : Bool, depth : Int) -> Js {
  if depth <= 0 {
    v
  } else {
    match v {
      Ar(a) => {
        let m = a.map(fn(i) { d(i, o, depth - 1) })
        let f = m.filter(fn(i) { !emp(i, o) })
        if f.is_empty() {
          Nl
        } else {
          Ar(f)
        }
      }
      Ob(m) => {
        let new_map : Map[String, Js] = Map([], capacity=8)
        for k, val in m {
          let c = d(val, o, depth - 1)
          if !emp(c, o) {
            new_map[k] = c
          }
        }
        if new_map.is_empty() {
          Nl
        } else {
          Ob(new_map)
        }
      }
      _ => v
    }
  }
}

///|
pub fn strip(v : Js, oz : Bool) -> Js {
  let c = d(v, oz, 1000)
  match c {
    Nl => Ob(Map([], capacity=0))
    Ob(m) if m.is_empty() => Ob(Map([], capacity=0))
    Ob(_) => c
    _ => Ob(Map([], capacity=0))
  }
}

///|
fn is_null(v : Js) -> Bool {
  match v {
    Nl => true
    _ => false
  }
}

///|
pub fn strip_null(v : Js) -> Js {
  match v {
    Ar(a) => {
      let mapped = a.map(fn(x) { strip_null(x) })
      Ar(mapped.filter(fn(x) { !is_null(x) }))
    }
    Ob(m) => {
      let new_m : Map[String, Js] = Map([], capacity=8)
      for k, val in m {
        let c = strip_null(val)
        if !is_null(c) {
          new_m[k] = c
        }
      }
      Ob(new_m)
    }
    _ => v
  }
}

///|
pub fn strip_empty(v : Js) -> Js {
  d(v, false, 1000)
}