// strip_spans: the same tree with every span set to zero, so that the
// derived Eq compares structure only (spec M3a section 3).

///|
let no_span : Span = { start: 0, end: 0, }

///|
fn strip_ident(i : Identifier) -> Identifier {
  { ..i, span: no_span, }
}

///|
fn strip_ident_opt(i : Identifier?) -> Identifier? {
  i.map(strip_ident)
}

///|
fn strip_opt(e : Expr?) -> Expr? {
  e.map(strip_expr)
}

///|
fn strip_exprs(xs : Array[Expr]) -> Array[Expr] {
  xs.map(strip_expr)
}

///|
fn strip_fields(fs : Array[(Expr, Expr)]) -> Array[(Expr, Expr)] {
  fs.map(f => (strip_expr(f.0), strip_expr(f.1)))
}

///|
fn strip_type(t : DataType) -> DataType {
  match t {
    Map(k, v) => Map(strip_type(k), strip_type(v))
    Array(e) => Array(e.map(strip_type))
    Custom(i) => Custom(strip_ident(i))
    other => other
  }
}

///|
fn strip_step(s : PathStep) -> PathStep {
  match s {
    Field(i) => Field(strip_ident(i))
    FieldString(t, ..) => FieldString(t, span=no_span)
    Index(e) => Index(strip_expr(e))
    AllElements(_) => AllElements(no_span)
    AllFields(_) => AllFields(no_span)
  }
}

///|
fn strip_expr(e : Expr) -> Expr {
  let kind : ExprKind = match e.kind {
    Lit(_) | Parameter | Error => e.kind
    VarRef(i, qualified~) => VarRef(strip_ident(i), qualified~)
    Path(b, steps) => Path(strip_expr(b), steps.map(strip_step))
    Unary(op, x) => Unary(op, strip_expr(x))
    Binary(op, a, b) => Binary(op, strip_expr(a), strip_expr(b))
    And(a, b) => And(strip_expr(a), strip_expr(b))
    Or(a, b) => Or(strip_expr(a), strip_expr(b))
    Not(a) => Not(strip_expr(a))
    BoolTest(x, v, negated~) => BoolTest(strip_expr(x), v, negated~)
    Like(x, pattern~, escape~, negated~) =>
      Like(
        strip_expr(x),
        pattern=strip_expr(pattern),
        escape=strip_opt(escape),
        negated~,
      )
    Between(x, low~, high~, negated~) =>
      Between(
        strip_expr(x),
        low=strip_expr(low),
        high=strip_expr(high),
        negated~,
      )
    In(a, b, negated~) => In(strip_expr(a), strip_expr(b), negated~)
    IsNull(x, negated~) => IsNull(strip_expr(x), negated~)
    IsMissing(x, negated~) => IsMissing(strip_expr(x), negated~)
    IsType(x, t, negated~) => IsType(strip_expr(x), strip_type(t), negated~)
    Overlaps(a, b) => Overlaps(strip_expr(a), strip_expr(b))
    Case(operand~, branches~, default~) =>
      Case(
        operand=strip_opt(operand),
        branches=branches.map(b => (strip_expr(b.0), strip_expr(b.1))),
        default=strip_opt(default),
      )
    Cast(x, t) => Cast(strip_expr(x), strip_type(t))
    Extract(f, x) => Extract(f, strip_expr(x))
    Trim(spec~, chars~, x) => Trim(spec~, chars=strip_opt(chars), strip_expr(x))
    Substring(x, start~, length~) =>
      Substring(
        strip_expr(x),
        start=strip_expr(start),
        length=strip_opt(length),
      )
    Position(a, b) => Position(strip_expr(a), strip_expr(b))
    Overlay(x, placing~, from~, for_~) =>
      Overlay(
        strip_expr(x),
        placing=strip_expr(placing),
        from=strip_expr(from),
        for_=strip_opt(for_),
      )
    Coalesce(xs) => Coalesce(strip_exprs(xs))
    NullIf(a, b) => NullIf(strip_expr(a), strip_expr(b))
    DateAdd(f, a, b) => DateAdd(f, strip_expr(a), strip_expr(b))
    DateDiff(f, a, b) => DateDiff(f, strip_expr(a), strip_expr(b))
    Call(i, args, quantifier~) =>
      Call(strip_ident(i), strip_exprs(args), quantifier~)
    CallStar(i) => CallStar(strip_ident(i))
    List(xs) => List(strip_exprs(xs))
    Bag(xs) => Bag(strip_exprs(xs))
    Sexp(xs) => Sexp(strip_exprs(xs))
    Struct(fs) => Struct(strip_fields(fs))
    Map(fs) => Map(strip_fields(fs))
    Row(xs) => Row(strip_exprs(xs))
    Values(xs) => Values(strip_exprs(xs))
    Query(q) => Query(strip_query(q))
    GraphMatch(m) => GraphMatch(strip_match(m))
  }
  { kind, span: no_span, }
}

///|
fn strip_query(q : Query) -> Query {
  let body : QueryBody = match q.body {
    Select(s) => Select(strip_select(s))
    SetOp(op) =>
      SetOp({
        ..op,
        corresponding: op.corresponding.map(c => {
          match c {
            By(names) => By(names.map(strip_ident))
            other => other
          }
        }),
        left: strip_query(op.left),
        right: strip_query(op.right),
        span: no_span,
      })
    Expr(e) => Expr(strip_expr(e))
    Table(names) => Table(names.map(strip_ident))
  }
  {
    with_: q.with_.map(w => WithBinding::{
      name: strip_ident(w.name),
      query: strip_query(w.query),
      span: no_span,
    }),
    body,
    order_by: q.order_by.map(s => {
      ..s,
      expr: strip_expr(s.expr),
      span: no_span,
    }),
    limit: strip_opt(q.limit),
    offset: strip_opt(q.offset),
    span: no_span,
  }
}

///|
fn strip_item(i : SelectItem) -> SelectItem {
  match i {
    Expr(e, as_~, ..) =>
      Expr(strip_expr(e), as_=strip_ident_opt(as_), span=no_span)
    AllFields(e, ..) => AllFields(strip_expr(e), span=no_span)
  }
}

///|
fn strip_select(s : Select) -> Select {
  let projection : Projection = match s.projection {
    Star(_) => Star(no_span)
    Items(items) => Items(items.map(strip_item))
    Value(e) => Value(strip_expr(e))
    Pivot(value~, key~) => Pivot(value=strip_expr(value), key=strip_expr(key))
  }
  {
    ..s,
    projection,
    from: s.from.map(strip_from),
    let_: s.let_.map(l => LetBinding::{
      expr: strip_expr(l.expr),
      name: strip_ident(l.name),
      span: no_span,
    }),
    where_: strip_opt(s.where_),
    group_by: s.group_by.map(strip_group),
    having: strip_opt(s.having),
    span: no_span,
  }
}

///|
fn strip_group(g : GroupBy) -> GroupBy {
  let keys : GroupKeys = match g.keys {
    By(ks, partial~) =>
      By(
        ks.map(k => GroupKey::{
          expr: strip_expr(k.expr),
          as_: strip_ident_opt(k.as_),
          span: no_span,
        }),
        partial~,
      )
    other => other
  }
  { keys, group_as: strip_ident_opt(g.group_as), span: no_span, }
}

///|
fn strip_source(s : FromSource) -> FromSource {
  {
    ..s,
    expr: strip_expr(s.expr),
    as_: strip_ident_opt(s.as_),
    at: strip_ident_opt(s.at),
    by: strip_ident_opt(s.by),
    span: no_span,
  }
}

///|
fn strip_from(f : FromItem) -> FromItem {
  match f {
    Scan(s) => Scan(strip_source(s))
    Unpivot(s) => Unpivot(strip_source(s))
    Join(j) => {
      let form : JoinForm = match j.form {
        On(c) => On(strip_expr(c))
        other => other
      }
      Join({
        ..j,
        form,
        left: strip_from(j.left),
        right: strip_from(j.right),
        span: no_span,
      })
    }
  }
}

///|
fn strip_label(l : Label) -> Label {
  let kind : LabelKind = match l.kind {
    Name(i) => Name(strip_ident(i))
    Wildcard => Wildcard
    Not(x) => Not(strip_label(x))
    And(a, b) => And(strip_label(a), strip_label(b))
    Or(a, b) => Or(strip_label(a), strip_label(b))
  }
  { kind, span: no_span, }
}

///|
fn strip_quantifier(q : Quantifier?) -> Quantifier? {
  q.map(x => { ..x, span: no_span, })
}

///|
fn strip_path(p : PathPattern) -> PathPattern {
  {
    ..p,
    variable: strip_ident_opt(p.variable),
    parts: p.parts.map(strip_part),
    span: no_span,
  }
}

///|
fn strip_part(p : PathPart) -> PathPart {
  match p {
    Node(n) =>
      Node(NodePattern::{
        variable: strip_ident_opt(n.variable),
        label: n.label.map(strip_label),
        where_: strip_opt(n.where_),
        span: no_span,
      })
    Edge(e) =>
      Edge({
        ..e,
        filler: e.filler.map(f => EdgeFiller::{
          variable: strip_ident_opt(f.variable),
          label: f.label.map(strip_label),
          where_: strip_opt(f.where_),
          span: no_span,
        }),
        quantifier: strip_quantifier(e.quantifier),
        span: no_span,
      })
    Group(g) =>
      Group({
        ..g,
        path: strip_path(g.path),
        where_: strip_opt(g.where_),
        quantifier: strip_quantifier(g.quantifier),
        span: no_span,
      })
  }
}

///|
fn strip_match(m : GraphMatch) -> GraphMatch {
  {
    graph: strip_expr(m.graph),
    pattern: GraphPattern::{
      selector: m.pattern.selector.map(s => { ..s, span: no_span, }),
      paths: m.pattern.paths.map(strip_path),
      where_: strip_opt(m.pattern.where_),
      span: no_span,
    },
    span: no_span,
  }
}

///|
/// The same expression with every span set to `{ start: 0, end: 0 }`.
pub fn Expr::strip_spans(self : Expr) -> Expr {
  strip_expr(self)
}

///|
/// The same query with every span set to `{ start: 0, end: 0 }`.
pub fn Query::strip_spans(self : Query) -> Query {
  strip_query(self)
}

///|
/// The same statement with every span set to `{ start: 0, end: 0 }`, so that
/// `==` compares structure only.
pub fn Statement::strip_spans(self : Statement) -> Statement {
  match self {
    Query(q) => Query(strip_query(q))
    Error(_) => Error(no_span)
  }
}

///|
/// The same script with every span set to `{ start: 0, end: 0 }`.
pub fn Script::strip_spans(self : Script) -> Script {
  {
    statements: self.statements.map(s => s.strip_spans()),
    comments: self.comments.map(c => { ..c, span: no_span, }),
    span: no_span,
  }
}