// 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, fold : Bool) -> Identifier {
  {
    ..i,
    text: if fold && !i.delimited {
      i.text.to_lower()
    } else {
      i.text
    },
    span: no_span,
  }
}

///|
fn strip_ident_opt(i : Identifier?, fold : Bool) -> Identifier? {
  i.map(x => strip_ident(x, fold))
}

///|
fn strip_opt(e : Expr?, fold : Bool) -> Expr? {
  e.map(x => strip_expr(x, fold))
}

///|
fn strip_exprs(xs : Array[Expr], fold : Bool) -> Array[Expr] {
  xs.map(x => strip_expr(x, fold))
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

///|
fn strip_match(m : GraphMatch, fold : Bool) -> GraphMatch {
  {
    graph: strip_expr(m.graph, fold),
    pattern: GraphPattern::{
      selector: m.pattern.selector.map(s => { ..s, span: no_span, }),
      paths: m.pattern.paths.map(x => strip_path(x, fold)),
      where_: strip_opt(m.pattern.where_, fold),
      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, false)
}

///|
/// The same expression with every span zero and every regular identifier in
/// lower case: regular identifiers compare without case, so two expressions
/// with equal canonical forms mean the same thing (GROUP BY key matching,
/// spec M4c ยง3.5.2).
pub fn Expr::canonical(self : Expr) -> Expr {
  strip_expr(self, true)
}

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

///|
/// 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, false))
    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,
  }
}