// Features that are accepted by the specification reader but not implemented
// by the compiler must fail loudly instead of being ignored: every check here
// names the JSON path of the property.

///|
fn nope(path : String) -> CompileError {
  CompileError(path + " 暂不支持")
}

///|
let honoured_mark_props : Array[String] = [
  "color", "filled", "interpolate", "opacity",
]

///|
/// Mark properties that only mean something for `arc`.
let honoured_arc_props : Array[String] = [
  "innerRadius", "outerRadius", "padAngle", "cornerRadius", "stroke", "strokeWidth",
]

///|
/// Mark properties that only mean something for `text`.
let honoured_text_props : Array[String] = [
  "align", "baseline", "dx", "dy", "fontSize", "text", "x", "y",
]

///|
/// Mark properties that only mean something for `rule` (`size` also for `line`).
let honoured_rule_props : Array[String] = ["strokeDash", "size"]

///|
let honoured_axis_props : Array[String] = [
  "domain", "domainColor", "domainWidth", "format", "formatType", "grid", "gridColor",
  "gridWidth", "labelAlign", "labelAngle", "labelBaseline", "labelColor", "labelFlush",
  "labelFontSize", "labelLimit", "labelOffset", "labelOverlap", "labelPadding", "labelSeparation",
  "labels", "maxExtent", "minExtent", "offset", "position", "tickColor", "tickMinStep",
  "tickOffset", "tickRound", "tickSize", "tickWidth", "ticks", "titlePadding",
]

///|
let honoured_scale_props : Array[String] = [
  "type", "base", "exponent", "domain", "domainMin", "domainMax", "zero", "range",
  "reverse", "clamp", "nice",
]

///|
/// The field type a channel resolves to (vega-lite's inference).
fn inferred_type(d : @spec.ChannelDef) -> String {
  match d.typ {
    Some(t) => t
    None =>
      if d.datum is Some(Num(_)) {
        "quantitative"
      } else if d.aggregate is Some(_) {
        "quantitative"
      } else if d.time_unit is Some(_) {
        "temporal"
      } else if d.bin is Some(_) {
        "quantitative"
      } else {
        match d.scale {
          Some(m) =>
            match m.get("type") {
              Some(Str("log"))
              | Some(Str("linear"))
              | Some(Str("sqrt"))
              | Some(Str("pow")) => "quantitative"
              Some(Str("time")) | Some(Str("utc")) => "temporal"
              _ => "nominal"
            }
          None => "nominal"
        }
      }
  }
}

///|
fn validate_config(
  config : Map[String, @value.Value],
) -> Unit raise CompileError {
  for k, v in config {
    if k == "scale" {
      match v {
        Obj(m) =>
          for sk, sv in m {
            if sk != "zero" || !(sv is Bool(_)) {
              raise nope("config.scale." + sk)
            }
          }
        _ => raise nope("config.scale")
      }
    } else {
      raise nope("config." + k)
    }
  }
}

///|
pub fn validate(spec : @spec.Spec) -> Unit raise CompileError {
  for k, _ in spec.mark.props {
    if !honoured_mark_props.contains(k) &&
      !(spec.mark.typ == "text" && honoured_text_props.contains(k)) &&
      !(spec.mark.typ == "rule" && honoured_rule_props.contains(k)) &&
      !(spec.mark.typ == "arc" && honoured_arc_props.contains(k)) &&
      !(spec.mark.typ == "line" && k == "size") {
      raise nope("mark." + k)
    }
  }
  match spec.mark.props.get("size") {
    Some(Num(_)) | None => ()
    Some(_) => raise nope("mark.size")
  }
  if spec.mark.typ == "text" {
    // constant position ("width" / "height" or pixels) and constant text
    match spec.mark.props.get("x") {
      Some(Num(_)) | Some(Str("width")) | None => ()
      Some(_) => raise nope("mark.x")
    }
    match spec.mark.props.get("y") {
      Some(Num(_)) | Some(Str("height")) | None => ()
      Some(_) => raise nope("mark.y")
    }
    match spec.mark.props.get("text") {
      Some(Str(_)) | None => ()
      Some(_) => raise nope("mark.text")
    }
  }
  if spec.mark.typ == "arc" {
    for
      k in [
        "innerRadius", "outerRadius", "padAngle", "cornerRadius", "strokeWidth",
      ] {
      match spec.mark.props.get(k) {
        Some(Num(_)) | None => ()
        Some(_) => raise nope("mark." + k)
      }
    }
    match spec.mark.props.get("stroke") {
      Some(Str(_)) | None => ()
      Some(_) => raise nope("mark.stroke")
    }
  }
  match spec.mark.props.get("strokeDash") {
    Some(Arr(a)) if a.all(fn(v) { v is Num(_) }) => ()
    None => ()
    Some(_) => raise nope("mark.strokeDash")
  }
  validate_config(spec.config)
  let mark = spec.mark.typ
  // a continuous colour sharing its field with another legend channel makes
  // vega-lite merge them into one symbol legend, which is not implemented
  for e in spec.encoding {
    let (name, d) = e
    if name == "color" &&
      inferred_type(d) == "quantitative" &&
      d.field is Some(_) {
      for o in spec.encoding {
        if o.0 != "color" &&
          (o.0 == "size" || o.0 == "opacity" || o.0 == "shape") &&
          o.1.field == d.field {
          raise nope("encoding." + o.0 + " (与连续颜色共用字段)")
        }
      }
    }
  }
  // an arc is drawn around the middle of the view, by its angle
  if mark == "arc" {
    let has_theta = spec.encoding.any(fn(e) { e.0 == "theta" })
    if !has_theta {
      raise CompileError("encoding.theta 缺失 (mark arc 需要 theta 通道)")
    }
  }
  for e in spec.encoding {
    let (name, d) = e
    let path = "encoding." + name
    let positional = name == "x" || name == "y"
    let typ = inferred_type(d)
    if mark == "arc" && (positional || name == "x2" || name == "y2") {
      raise nope(path + " (mark arc)")
    }
    if d.sort is Some(_) &&
      !(positional && (typ == "nominal" || typ == "ordinal")) {
      raise nope(path + ".sort")
    }
    match d.stack {
      Some("none") => ()
      Some(o) =>
        if name == "theta" {
          if !(o == "zero" || o == "normalize") {
            raise nope(path + ".stack")
          }
        } else if !(positional && (mark == "bar" || mark == "area")) ||
          !(o == "zero" || o == "normalize" || o == "center") {
          raise nope(path + ".stack")
        }
      None => ()
    }
    if d.band is Some(_) {
      raise nope(path + ".band")
    }
    if d.band_position is Some(_) {
      raise nope(path + ".bandPosition")
    }
    // only these marks know how to draw a binned field (bars as a range,
    // points and text at the middle of the bin)
    if d.bin is Some(_) &&
      !(mark == "bar" ||
      mark == "point" ||
      mark == "circle" ||
      mark == "square" ||
      mark == "text") {
      raise nope(path + ".bin (mark " + mark + ")")
    }
    match d.datum {
      Some(Num(_)) =>
        if !positional || (d.typ is Some(_) && typ != "quantitative") {
          raise nope(path + ".datum")
        }
      Some(_) => raise nope(path + ".datum")
      None => ()
    }
    if d.format_type is Some(_) {
      raise nope(path + ".formatType")
    }
    if d.format is Some(_) && name != "text" {
      raise nope(path + ".format")
    }
    if d.scale_off {
      raise nope(path + ".scale")
    }
    match d.legend {
      Props(m) =>
        for k, v in m {
          // only the side of the plot a symbol legend sits on
          let side = match v {
            Str("left") | Str("right") => true
            _ => false
          }
          let ok = k == "orient" &&
            side &&
            !(name == "color" && typ == "quantitative")
          if !ok {
            raise nope(
              path + ".legend" + (if k == "orient" { "" } else { "." + k }),
            )
          }
        }
      _ => ()
    }
    if (d.bin is Some(_) || d.time_unit is Some(_)) && !positional {
      raise nope(path + (if d.bin is Some(_) { ".bin" } else { ".timeUnit" }))
    }
    match d.axis {
      Props(m) => {
        if !positional {
          raise nope(path + ".axis")
        }
        for k, _ in m {
          if !honoured_axis_props.contains(k) {
            raise nope(path + ".axis." + k)
          }
        }
      }
      _ => ()
    }
    match d.scale {
      Some(m) => {
        let allowed = positional &&
          typ != "nominal" &&
          typ != "ordinal" &&
          d.bin is None
        if name == "theta" {
          // the angles the value range maps to, in radians
          for k, v in m {
            let two_numbers = match v {
              Arr([Num(_), Num(_)]) => true
              _ => false
            }
            if k != "range" || !two_numbers {
              raise nope(path + ".scale." + k)
            }
          }
        } else {
          for k, _ in m {
            if !allowed || !honoured_scale_props.contains(k) {
              raise nope(path + ".scale." + k)
            }
            if typ == "temporal" && k != "type" {
              raise nope(path + ".scale." + k)
            }
          }
        }
      }
      None => ()
    }
    match name {
      "x2" | "y2" =>
        if mark != "rule" && mark != "bar" && mark != "rect" {
          raise nope(path + " (mark " + mark + ")")
        }
      "text" => if mark != "text" { raise nope(path + " (mark " + mark + ")") }
      "theta" => {
        if mark != "arc" {
          raise nope(path + " (mark " + mark + ")")
        }
        if typ != "quantitative" {
          raise nope(path + ".type " + typ)
        }
      }
      "color" =>
        if d.field is Some(_) &&
          typ != "nominal" &&
          typ != "ordinal" &&
          typ != "quantitative" {
          raise nope(path + ".type " + typ)
        }
      "size" | "opacity" =>
        if d.field is Some(_) && typ != "quantitative" {
          raise nope(path + ".type " + typ)
        }
      "shape" =>
        if d.field is Some(_) && typ != "nominal" {
          raise nope(path + ".type " + typ)
        }
      _ => ()
    }
  }
}