// Parsing and validating a Vega-Lite specification (JSON) into `Spec`.

///|
let supported_marks : Array[String] = [
  "bar", "line", "point", "area", "rule", "tick", "rect", "text", "circle", "square",
]

///|
let known_unsupported_marks : Array[String] = [
  "arc", "trail", "geoshape", "image", "boxplot", "errorbar", "errorband",
]

///|
let supported_channels : Array[String] = [
  "x", "y", "x2", "y2", "color", "size", "shape", "opacity", "text",
]

///|
let unsupported_channels : Array[String] = [
  "detail", "order", "tooltip", "href", "key", "fill", "stroke", "strokeWidth", "strokeDash",
  "fillOpacity", "strokeOpacity", "xOffset", "yOffset", "theta", "theta2", "radius",
  "radius2", "latitude", "longitude", "latitude2", "longitude2", "row", "column",
  "facet", "url", "description", "time", "angle", "xError", "yError", "xError2",
  "yError2",
]

///|
/// Mark properties that are implemented (the others are rejected with a path).
let supported_mark_props : Array[String] = [
  "color", "fill", "stroke", "opacity", "fillOpacity", "strokeOpacity", "strokeWidth",
  "strokeDash", "strokeDashOffset", "strokeCap", "strokeJoin", "size", "filled",
  "orient", "interpolate", "tension", "cornerRadius", "cornerRadiusEnd", "cornerRadiusTopLeft",
  "cornerRadiusTopRight", "cornerRadiusBottomLeft", "cornerRadiusBottomRight", "binSpacing",
  "width", "height", "align", "baseline", "dx", "dy", "angle", "font", "fontSize",
  "fontWeight", "fontStyle", "text", "limit", "ellipsis", "lineBreak", "lineHeight",
  "thickness", "minBandSize", "discreteBandSize", "shape", "invalid", "x", "y", "x2",
  "y2", "radius", "blend", "ariaRole",
]

///|
let unsupported_mark_props : Array[String] = [
  "point", "line", "tooltip", "clip", "href", "cursor", "style", "innerRadius", "outerRadius",
  "padAngle", "theta", "theta2", "radius2", "aspect", "url", "smooth", "radiusOffset",
  "thetaOffset", "timeUnitBandPosition", "timeUnitBandSize", "bandSize", "xOffset",
  "yOffset", "x2Offset", "y2Offset", "dir", "description", "ariaRoleDescription",
  "aria", "endAngle", "startAngle", "tooltipContent", "overlay",
]

///|
let supported_config_keys : Array[String] = [
  "view", "mark", "point", "circle", "square", "bar", "line", "area", "tick", "rule",
  "text", "rect", "axis", "axisX", "axisY", "axisBand", "axisLeft", "axisRight",
  "axisTop", "axisBottom", "scale", "range", "legend", "background", "padding", "countTitle",
  "font", "fieldTitle", "invalid", "style", "title",
]

///|
let supported_scale_types : Array[String] = [
  "linear", "log", "sqrt", "pow", "time", "utc", "band", "point", "ordinal",
]

///|
let known_scale_types : Array[String] = [
  "symlog", "quantile", "quantize", "threshold", "bin-ordinal", "sequential",
]

///|
let supported_scale_props : Array[String] = [
  "type", "domain", "domainMin", "domainMax", "range", "scheme", "zero", "nice",
  "round", "padding", "paddingInner", "paddingOuter", "reverse", "clamp", "base",
  "exponent", "align", "rangeMin", "rangeMax",
]

///|
let supported_axis_props : Array[String] = [
  "orient", "title", "format", "formatType", "labelAngle", "labelOverlap", "labels",
  "ticks", "domain", "grid", "tickCount", "values", "labelPadding", "tickSize", "labelFlush",
  "labelFontSize", "titleFontSize", "titlePadding", "offset", "position", "labelLimit",
  "labelColor", "titleColor", "domainColor", "gridColor", "tickColor", "labelBound",
  "gridDash", "gridWidth", "gridOpacity", "domainWidth", "tickWidth", "tickMinStep",
  "zindex", "labelSeparation", "labelAlign", "labelBaseline", "titleAnchor", "titleAngle",
  "titleAlign", "titleBaseline", "titleFontWeight", "labelFontWeight", "minExtent",
  "maxExtent", "labelOffset", "tickOffset", "tickRound", "tickBand", "bandPosition",
  "titleOpacity", "labelOpacity", "gridCap", "domainCap", "tickCap", "domainDash",
  "tickDash", "domainOpacity", "tickOpacity", "titleLimit", "titleFont", "labelFont",
  "titleX", "titleY",
]

///|
let supported_legend_props : Array[String] = [
  "orient", "title", "format", "formatType", "type", "values", "symbolType", "direction",
  "labelLimit", "titleLimit", "symbolSize", "symbolStrokeWidth", "gradientLength",
  "gradientThickness", "padding", "offset", "tickCount", "fillColor", "labelFontSize",
  "titleFontSize", "titlePadding", "labelOffset", "symbolOffset", "columns", "columnPadding",
  "rowPadding", "clipHeight", "cornerRadius", "strokeColor", "gradientStrokeColor",
  "labelColor", "titleColor", "titleOrient", "titleAnchor", "titleFontWeight", "labelFontWeight",
  "labelAlign", "labelBaseline", "gridAlign", "symbolDirection", "symbolFillColor",
  "symbolStrokeColor", "gradientDirection", "zindex", "labelOverlap",
]

///|
fn check_props(
  path : String,
  props : Map[String, @value.Value],
  supported : Array[String],
) -> Unit raise SpecError {
  for k, _ in props {
    if !supported.contains(k) {
      raise unsupported(join_path(path, k))
    }
  }
}

///|
fn obj_props(
  path : String,
  j : Json,
) -> Map[String, @value.Value] raise SpecError {
  match j {
    Object(m) => {
      let out : Map[String, @value.Value] = Map([])
      for k, v in m {
        out[k] = @value.from_json(v)
      }
      out
    }
    _ => raise invalid(path, "应为对象")
  }
}

///|
fn full_type(path : String, t : String) -> String raise SpecError {
  match t {
    "quantitative" | "Q" | "q" => "quantitative"
    "ordinal" | "O" | "o" => "ordinal"
    "nominal" | "N" | "n" => "nominal"
    "temporal" | "T" | "t" => "temporal"
    "geojson" => raise unsupported(path)
    _ => raise invalid(path, "未知的字段类型 " + t)
  }
}

///|
fn parse_guide(
  r : Reader,
  key : String,
  supported : Array[String],
) -> Guide raise SpecError {
  match r.take(key) {
    None => Default
    Some(Null) => Off
    Some(True) => Props(Map([]))
    Some(False) => Off
    Some(j) => {
      let path = r.path_of(key)
      let props = obj_props(path, j)
      check_props(path, props, supported)
      Props(props)
    }
  }
}

///|
fn parse_channel(path : String, j : Json) -> ChannelDef raise SpecError {
  let r = Reader::new(path, j)
  if r.has("condition") {
    raise unsupported(r.path_of("condition"))
  }
  let field = r.string("field")
  let typ = match r.string("type") {
    Some(t) => Some(full_type(r.path_of("type"), t))
    None => None
  }
  let aggregate = r.string("aggregate")
  let mut binned = false
  let bin : Map[String, @value.Value]? = match r.take("bin") {
    None => None
    Some(False) | Some(Null) => None
    Some(True) => Some(Map([]))
    Some(String("binned")) => {
      binned = true
      Some(Map([]))
    }
    Some(Object(m)) => {
      let props = obj_props(r.path_of("bin"), Json::object(m))
      check_props(r.path_of("bin"), props, [
        "maxbins", "step", "nice", "extent", "base", "divide", "minstep", "anchor",
        "steps",
      ])
      Some(props)
    }
    Some(_) => raise invalid(r.path_of("bin"), "应为 true 或对象")
  }
  let time_unit = match r.take("timeUnit") {
    None => None
    Some(String(s)) => Some(s)
    Some(_) => raise unsupported(r.path_of("timeUnit"))
  }
  let sort = r.value("sort")
  let scale_off = match r.take("scale") {
    Some(Null) => true
    Some(_) | None => false
  }
  let scale = match r.obj.get("scale") {
    Some(Object(_) as o) => {
      let props = obj_props(r.path_of("scale"), o)
      check_props(r.path_of("scale"), props, supported_scale_props)
      match props.get("type") {
        Some(Str(t)) =>
          if !supported_scale_types.contains(t) {
            let p = join_path(r.path_of("scale"), "type")
            if known_scale_types.contains(t) {
              raise unsupported(p)
            }
            raise invalid(p, "未知的 scale 类型 " + t)
          }
        _ => ()
      }
      Some(props)
    }
    Some(Null) | None => None
    Some(_) => raise invalid(r.path_of("scale"), "应为对象或 null")
  }
  let axis = parse_guide(r, "axis", supported_axis_props)
  let legend = parse_guide(r, "legend", supported_legend_props)
  let title : String?? = match r.take("title") {
    None => None
    Some(Null) => Some(None)
    Some(String(s)) => Some(Some(s))
    Some(_) => raise unsupported(r.path_of("title"))
  }
  let format = match r.take("format") {
    None => None
    Some(String(s)) => Some(s)
    Some(_) => raise unsupported(r.path_of("format"))
  }
  let format_type = r.string("formatType")
  let value = r.value("value")
  let datum = r.value("datum")
  let stack = match r.take("stack") {
    None => None
    Some(Null) | Some(False) => Some("none")
    Some(True) => Some("zero")
    Some(String(s)) => Some(s)
    Some(_) =>
      raise invalid(r.path_of("stack"), "应为字符串、布尔值或 null")
  }
  let band = r.number("band")
  let band_position = r.number("bandPosition")
  r.finish(unsupported_keys=[
    "impute", "header", "labelExpr", "repeat", "bandPosition_", "legendExpr",
  ])
  {
    field,
    typ,
    aggregate,
    bin,
    binned,
    time_unit,
    sort,
    scale,
    scale_off,
    axis,
    legend,
    title,
    format,
    format_type,
    value,
    datum,
    stack,
    band,
    band_position,
  }
}

///|
fn parse_mark(path : String, j : Json) -> Mark raise SpecError {
  match j {
    String(s) => {
      if known_unsupported_marks.contains(s) {
        raise unsupported(path)
      }
      if !supported_marks.contains(s) {
        raise invalid(path, "未知的 mark 类型 " + s)
      }
      { typ: s, props: Map([]), }
    }
    Object(_) => {
      let props = obj_props(path, j)
      let typ = match props.get("type") {
        Some(Str(s)) => s
        _ => raise invalid(join_path(path, "type"), "缺少 mark 类型")
      }
      if known_unsupported_marks.contains(typ) {
        raise unsupported(join_path(path, "type"))
      }
      if !supported_marks.contains(typ) {
        raise invalid(join_path(path, "type"), "未知的 mark 类型 " + typ)
      }
      props.remove("type")
      for k, _ in props {
        if unsupported_mark_props.contains(k) ||
          !supported_mark_props.contains(k) {
          if unsupported_mark_props.contains(k) {
            raise unsupported(join_path(path, k))
          } else {
            raise SpecError(join_path(path, k) + " 未知属性")
          }
        }
      }
      { typ, props, }
    }
    _ => raise invalid(path, "应为字符串或对象")
  }
}

///|
fn string_list(path : String, j : Json) -> Array[String] raise SpecError {
  match j {
    Array(items) => {
      let out : Array[String] = []
      for i in 0.. out.push(s)
          _ => raise invalid(index_path(path, i), "应为字符串")
        }
      }
      out
    }
    _ => raise invalid(path, "应为字符串数组")
  }
}

///|
fn parse_predicate(path : String, j : Json) -> Predicate raise SpecError {
  match j {
    String(s) => Expr(s)
    Object(m) => {
      let r = Reader::new(path, j)
      let tu = r.string("timeUnit")
      if tu is Some(_) {
        raise unsupported(r.path_of("timeUnit"))
      }
      if m.contains("and") || m.contains("or") {
        let key = if m.contains("and") { "and" } else { "or" }
        let items = match r.take(key) {
          Some(Array(xs)) => xs
          _ => raise invalid(r.path_of(key), "应为数组")
        }
        r.finish()
        let parts : Array[Predicate] = []
        for i in 0.. parse_predicate(r.path_of("not"), x)
          None => raise invalid(path, "缺少 not")
        }
        r.finish()
        return Not(inner)
      }
      if m.contains("param") {
        raise unsupported(join_path(path, "param"))
      }
      let field = match r.string("field") {
        Some(f) => f
        None => raise invalid(path, "缺少 field")
      }
      let tu2 : String? = None
      if m.contains("equal") {
        let v = r.value("equal").unwrap_or(Null)
        r.finish()
        return Equal(field, v, tu2)
      }
      if m.contains("lt") {
        let v = r.value("lt").unwrap_or(Null)
        r.finish()
        return Lt(field, v, tu2)
      }
      if m.contains("lte") {
        let v = r.value("lte").unwrap_or(Null)
        r.finish()
        return Lte(field, v, tu2)
      }
      if m.contains("gt") {
        let v = r.value("gt").unwrap_or(Null)
        r.finish()
        return Gt(field, v, tu2)
      }
      if m.contains("gte") {
        let v = r.value("gte").unwrap_or(Null)
        r.finish()
        return Gte(field, v, tu2)
      }
      if m.contains("range") {
        match r.value("range") {
          Some(Arr(items)) if items.length() == 2 => {
            r.finish()
            return Range(field, items[0], items[1], tu2)
          }
          _ =>
            raise invalid(r.path_of("range"), "应为含两个元素的数组")
        }
      }
      if m.contains("oneOf") {
        match r.value("oneOf") {
          Some(Arr(items)) => {
            r.finish()
            return OneOf(field, items, tu2)
          }
          _ => raise invalid(r.path_of("oneOf"), "应为数组")
        }
      }
      if m.contains("valid") {
        let v = r.boolean("valid").unwrap_or(true)
        r.finish()
        if !v {
          raise unsupported(join_path(path, "valid"))
        }
        return Valid(field)
      }
      raise invalid(path, "无法识别的过滤谓词")
    }
    _ => raise invalid(path, "过滤条件应为字符串或对象")
  }
}

///|
fn parse_transform(path : String, j : Json) -> Transform raise SpecError {
  let r = Reader::new(path, j)
  if r.has("filter") {
    let f = match r.take("filter") {
      Some(String(s)) => FilterSpec::Expr(s)
      Some(x) => FilterSpec::Predicate(parse_predicate(r.path_of("filter"), x))
      None => raise invalid(path, "缺少 filter")
    }
    r.finish()
    return Filter(f)
  }
  if r.has("calculate") {
    let expr = match r.string("calculate") {
      Some(e) => e
      None => raise invalid(r.path_of("calculate"), "应为字符串")
    }
    let as_name = match r.string("as") {
      Some(a) => a
      None => raise invalid(r.path_of("as"), "缺少 as")
    }
    r.finish()
    return Calculate(expr, as_name)
  }
  if r.has("aggregate") {
    let ops : Array[AggregateOp] = []
    let agg_path = r.path_of("aggregate")
    match r.take("aggregate") {
      Some(Array(items)) =>
        for i in 0.. o
            None => raise invalid(ir.path_of("op"), "缺少 op")
          }
          let field = ir.string("field")
          let out = match ir.string("as") {
            Some(a) => a
            None => @transform.measure_name(op, field, None)
          }
          ir.finish()
          if !@transform.is_aggregate_op(op) {
            raise unsupported(ir.path_of("op"))
          }
          ops.push({ op, field, out, })
        }
      _ => raise invalid(agg_path, "应为数组")
    }
    let groupby = match r.take("groupby") {
      Some(g) => string_list(r.path_of("groupby"), g)
      None => []
    }
    r.finish()
    return Aggregate(ops, groupby)
  }
  if r.has("bin") {
    let bin_props : Map[String, @value.Value] = match r.take("bin") {
      Some(True) => Map([])
      Some(Object(_) as o) => obj_props(r.path_of("bin"), o)
      _ => raise invalid(r.path_of("bin"), "应为 true 或对象")
    }
    let field = match r.string("field") {
      Some(f) => f
      None => raise invalid(r.path_of("field"), "缺少 field")
    }
    let as_names = match r.take("as") {
      Some(String(s)) => [s, s + "_end"]
      Some(Array(_) as a) => string_list(r.path_of("as"), a)
      _ => raise invalid(r.path_of("as"), "缺少 as")
    }
    r.finish()
    return Bin(field, bin_props, as_names)
  }
  if r.has("timeUnit") {
    let unit = match r.string("timeUnit") {
      Some(u) => u
      None => raise unsupported(r.path_of("timeUnit"))
    }
    let field = match r.string("field") {
      Some(f) => f
      None => raise invalid(r.path_of("field"), "缺少 field")
    }
    let as_name = match r.string("as") {
      Some(a) => a
      None => raise invalid(r.path_of("as"), "缺少 as")
    }
    r.finish()
    return TimeUnit(unit, field, as_name)
  }
  if r.has("fold") {
    let fields = match r.take("fold") {
      Some(f) => string_list(r.path_of("fold"), f)
      None => []
    }
    let as_names = match r.take("as") {
      Some(a) => string_list(r.path_of("as"), a)
      None => ["key", "value"]
    }
    r.finish()
    return Fold(fields, as_names)
  }
  for k, _ in r.obj {
    raise unsupported(join_path(path, k))
  }
  raise invalid(path, "空的 transform")
}

///|
fn parse_data(
  path : String,
  j : Json,
) -> Array[Map[String, @value.Value]]? raise SpecError {
  let r = Reader::new(path, j)
  if r.has("url") {
    raise unsupported(r.path_of("url"))
  }
  if r.has("name") {
    raise unsupported(r.path_of("name"))
  }
  if r.has("sequence") || r.has("sphere") || r.has("graticule") {
    raise unsupported(path)
  }
  let rows : Array[Map[String, @value.Value]] = []
  match r.take("values") {
    Some(Array(items)) =>
      for i in 0.. rows.push(m)
          _ => raise unsupported(index_path(r.path_of("values"), i))
        }
      }
    Some(_) => raise unsupported(r.path_of("values"))
    None => raise invalid(path, "缺少 values")
  }
  match r.take("format") {
    None => ()
    Some(Object(m)) => {
      // only the default JSON reader is supported
      let fr = Reader::new(r.path_of("format"), Json::object(m))
      match fr.string("type") {
        Some("json") | None => ()
        Some(_) => raise unsupported(fr.path_of("type"))
      }
      fr.finish()
    }
    Some(_) => raise invalid(r.path_of("format"), "应为对象")
  }
  r.finish()
  Some(rows)
}

///|
/// Parse a Vega-Lite specification from parsed JSON.
pub fn parse_spec(j : Json) -> Spec raise SpecError {
  let r = Reader::new("", j)
  r.ignore(["$schema", "description", "name", "usermeta"])
  for
    k in [
      "layer", "facet", "concat", "hconcat", "vconcat", "repeat", "spec", "params",
      "selection", "projection", "resolve", "datasets", "view",
    ] {
    if r.has(k) {
      raise unsupported(k)
    }
  }
  let mark = match r.take("mark") {
    Some(m) => parse_mark("mark", m)
    None => raise SpecError("mark: 缺少 mark(仅支持单视图规格)")
  }
  let encoding : Array[(String, ChannelDef)] = []
  match r.take("encoding") {
    None => ()
    Some(Object(m)) =>
      for k, v in m {
        if unsupported_channels.contains(k) {
          raise unsupported(join_path("encoding", k))
        }
        if !supported_channels.contains(k) {
          raise SpecError(join_path("encoding", k) + " 未知属性")
        }
        encoding.push((k, parse_channel(join_path("encoding", k), v)))
      }
    Some(_) => raise invalid("encoding", "应为对象")
  }
  let transform : Array[Transform] = []
  match r.take("transform") {
    None => ()
    Some(Array(items)) =>
      for i in 0.. raise invalid("transform", "应为数组")
  }
  let data = match r.take("data") {
    None => None
    Some(Null) => None
    Some(d) => parse_data("data", d)
  }
  let width = r.value("width")
  let height = r.value("height")
  match width {
    Some(Str(_)) => raise unsupported("width")
    _ => ()
  }
  match height {
    Some(Str(_)) => raise unsupported("height")
    _ => ()
  }
  let padding = r.value("padding")
  let background = r.string("background")
  let title = match r.take("title") {
    None => None
    Some(String(s)) => Some(s)
    Some(_) => raise unsupported("title")
  }
  let description = None
  let config : Map[String, @value.Value] = Map([])
  match r.take("config") {
    None => ()
    Some(c) => {
      let props = obj_props("config", c)
      for k, v in props {
        if supported_config_keys.contains(k) {
          config[k] = v
        } else {
          raise unsupported(join_path("config", k))
        }
      }
    }
  }
  r.finish(unsupported_keys=["autosize", "center", "bounds", "spacing", "align"])
  {
    mark,
    encoding,
    transform,
    data,
    width,
    height,
    padding,
    background,
    title,
    description,
    config,
  }
}

///|
/// Parse a specification from JSON text.
pub fn parse_spec_text(text : String) -> Spec raise SpecError {
  let j = @json.parse(text) catch {
    e => raise SpecError("JSON 解析失败: " + e.to_string())
  }
  parse_spec(j)
}