// 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"]

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

///|
pub fn validate(spec : @spec.Spec) -> Unit raise CompileError {
  for k, _ in spec.mark.props {
    if !honoured_mark_props.contains(k) {
      raise nope("mark." + k)
    }
  }
  for k, v in spec.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)
    }
  }
  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 + " (与连续颜色共用字段)")
        }
      }
    }
  }
  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 d.sort is Some(_) &&
      !(positional && (typ == "nominal" || typ == "ordinal")) {
      raise nope(path + ".sort")
    }
    match d.stack {
      Some("none") => ()
      Some(o) =>
        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")
    }
    if d.datum is Some(_) {
      raise nope(path + ".datum")
    }
    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")
    }
    if d.legend is Props(_) {
      raise nope(path + ".legend")
    }
    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
        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" {
          raise nope(path + " (mark " + mark + ")")
        }
      "text" => if mark != "text" { raise nope(path + " (mark " + mark + ")") }
      "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)
        }
      _ => ()
    }
  }
}