Skip to main content

lib/terminus_db/woql/encoder.ex

defmodule TerminusDB.WOQL.Encoder do
  @moduledoc false

  # JSON-LD encoder for WOQL queries.
  #
  # Uses four value-wrapper types matching the Python/JS clients:
  #
  #   NodeValue      — nodes/IRIs (subjects, predicates, identifiers)
  #   Value          — generic values (triple objects, comparison operands)
  #   DataValue      — literal data (string-op operands, ID-gen keys)
  #   ArithmeticValue — arithmetic operands
  #
  # Variables use the "v:Name" convention. Each wrapper has variable/node/data
  # fields as appropriate:
  #
  #   {"@type": "<Wrapper>", "variable": "Name"}          — variable
  #   {"@type": "NodeValue", "node": "iri"}               — IRI constant
  #   {"@type": "<Wrapper>", "data": {"@type": "xsd:…", "@value": …}} — literal

  def encode(%TerminusDB.WOQL{op: :triple, args: [s, p, o]}) do
    %{
      "@type" => "Triple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o)
    }
  end

  def encode(%TerminusDB.WOQL{op: :and, args: queries}) do
    %{
      "@type" => "And",
      "and" => Enum.map(queries, &encode/1)
    }
  end

  def encode(%TerminusDB.WOQL{op: :or, args: queries}) do
    %{
      "@type" => "Or",
      "or" => Enum.map(queries, &encode/1)
    }
  end

  def encode(%TerminusDB.WOQL{op: :eq, args: [left, right]}) do
    %{
      "@type" => "Equals",
      "left" => encode_value(left),
      "right" => encode_value(right)
    }
  end

  def encode(%TerminusDB.WOQL{op: :select, args: [vars, query]}) do
    %{
      "@type" => "Select",
      "variables" => Enum.map(vars, &encode_select_var/1),
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :read_document, args: [id, var]}) do
    %{
      "@type" => "ReadDocument",
      "identifier" => encode_node(id),
      "document" => encode_value(var)
    }
  end

  def encode(%TerminusDB.WOQL{op: :type_of, args: [node, var]}) do
    %{
      "@type" => "TypeOf",
      "value" => encode_value(node),
      "type" => encode_value(var)
    }
  end

  def encode(%TerminusDB.WOQL{op: true, args: []}) do
    %{"@type" => "True"}
  end

  def encode(%TerminusDB.WOQL{op: :not, args: [query]}) do
    %{"@type" => "Not", "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :opt, args: [query]}) do
    %{"@type" => "Optional", "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :once, args: [query]}) do
    %{"@type" => "Once", "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :immediately, args: [query]}) do
    %{"@type" => "Immediately", "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :distinct, args: [vars, query]}) do
    %{
      "@type" => "Distinct",
      "variables" => Enum.map(vars, &encode_select_var/1),
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :limit, args: [n, query]}) do
    %{"@type" => "Limit", "limit" => n, "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :start, args: [n, query]}) do
    %{"@type" => "Start", "start" => n, "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :order_by, args: [specs, query]}) do
    %{
      "@type" => "OrderBy",
      "ordering" =>
        Enum.map(specs, fn {var, order} ->
          %{"@type" => "OrderTemplate", "variable" => var, "order" => order}
        end),
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :group_by, args: [vars, template, grouped, query]}) do
    %{
      "@type" => "GroupBy",
      "group_by" => Enum.map(vars, &encode_select_var/1),
      "template" => encode_value(template),
      "grouped" => encode_value(grouped),
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :count, args: [countvar, query]}) do
    %{"@type" => "Count", "count" => encode_value(countvar), "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :collect, args: [template, into, query]}) do
    %{
      "@type" => "Collect",
      "template" => encode_value(template),
      "into" => encode_value(into),
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :quad, args: [s, p, o, graph]}) do
    %{
      "@type" => "Triple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :added_triple, args: [s, p, o]}) do
    %{
      "@type" => "AddedTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o)
    }
  end

  def encode(%TerminusDB.WOQL{op: :removed_triple, args: [s, p, o]}) do
    %{
      "@type" => "DeletedTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o)
    }
  end

  def encode(%TerminusDB.WOQL{op: :added_quad, args: [s, p, o, graph]}) do
    %{
      "@type" => "AddedTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :removed_quad, args: [s, p, o, graph]}) do
    %{
      "@type" => "DeletedTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :add_triple, args: [s, p, o]}) do
    %{
      "@type" => "AddTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o)
    }
  end

  def encode(%TerminusDB.WOQL{op: :delete_triple, args: [s, p, o]}) do
    %{
      "@type" => "DeleteTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o)
    }
  end

  def encode(%TerminusDB.WOQL{op: :add_quad, args: [s, p, o, graph]}) do
    %{
      "@type" => "AddTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :delete_quad, args: [s, p, o, graph]}) do
    %{
      "@type" => "DeleteTriple",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :less, args: [left, right]}) do
    %{"@type" => "Less", "left" => encode_value(left), "right" => encode_value(right)}
  end

  def encode(%TerminusDB.WOQL{op: :greater, args: [left, right]}) do
    %{"@type" => "Greater", "left" => encode_value(left), "right" => encode_value(right)}
  end

  def encode(%TerminusDB.WOQL{op: :gte, args: [left, right]}) do
    %{"@type" => "Gte", "left" => encode_value(left), "right" => encode_value(right)}
  end

  def encode(%TerminusDB.WOQL{op: :lte, args: [left, right]}) do
    %{"@type" => "Lte", "left" => encode_value(left), "right" => encode_value(right)}
  end

  def encode(%TerminusDB.WOQL{op: :like, args: [left, right, dist]}) do
    %{
      "@type" => "Like",
      "left" => encode_value(left),
      "right" => encode_value(right),
      "distance" => encode_value(dist)
    }
  end

  def encode(%TerminusDB.WOQL{op: :isa, args: [element, type]}) do
    %{"@type" => "IsA", "element" => encode_node(element), "type" => encode_node(type)}
  end

  def encode(%TerminusDB.WOQL{op: :sub, args: [parent, child]}) do
    %{"@type" => "Subsumption", "parent" => encode_node(parent), "child" => encode_node(child)}
  end

  def encode(%TerminusDB.WOQL{op: :cast, args: [value, type, result]}) do
    %{
      "@type" => "Typecast",
      "value" => encode_value(value),
      "type" => encode_value(type),
      "result" => encode_value(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :eval, args: [expression, result]}) do
    %{
      "@type" => "Eval",
      "expression" => encode_arithmetic(expression),
      "result" => encode_value(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :plus, args: args}) do
    %{"@type" => "Plus", "arguments" => Enum.map(args, &encode_arithmetic/1)}
  end

  def encode(%TerminusDB.WOQL{op: :minus, args: args}) do
    %{"@type" => "Minus", "arguments" => Enum.map(args, &encode_arithmetic/1)}
  end

  def encode(%TerminusDB.WOQL{op: :times, args: args}) do
    %{"@type" => "Times", "arguments" => Enum.map(args, &encode_arithmetic/1)}
  end

  def encode(%TerminusDB.WOQL{op: :divide, args: args}) do
    %{"@type" => "Divide", "arguments" => Enum.map(args, &encode_arithmetic/1)}
  end

  def encode(%TerminusDB.WOQL{op: :div, args: args}) do
    %{"@type" => "Div", "arguments" => Enum.map(args, &encode_arithmetic/1)}
  end

  def encode(%TerminusDB.WOQL{op: :exp, args: [base, exponent]}) do
    %{
      "@type" => "Exp",
      "first" => encode_arithmetic(base),
      "second" => encode_arithmetic(exponent)
    }
  end

  def encode(%TerminusDB.WOQL{op: :floor, args: [value]}) do
    %{"@type" => "Floor", "data" => encode_arithmetic(value)}
  end

  def encode(%TerminusDB.WOQL{op: :sum, args: [list, result]}) do
    %{"@type" => "Sum", "list" => encode_value(list), "result" => encode_value(result)}
  end

  def encode(%TerminusDB.WOQL{op: :concat, args: [list, result]}) do
    %{
      "@type" => "Concatenate",
      "list" => Enum.map(list, &encode_data/1),
      "result" => encode_data(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :join, args: [list, glue, output]}) do
    %{
      "@type" => "Join",
      "list" => encode_data(list),
      "glue" => encode_data(glue),
      "result" => encode_data(output)
    }
  end

  def encode(%TerminusDB.WOQL{op: :substr, args: [string, length, substring, before, after_]}) do
    %{
      "@type" => "Substring",
      "string" => encode_data(string),
      "length" => encode_data(length),
      "substring" => encode_data(substring),
      "before" => encode_data(before),
      "after" => encode_data(after_)
    }
  end

  def encode(%TerminusDB.WOQL{op: :trim, args: [untrimmed, trimmed]}) do
    %{"@type" => "Trim", "untrimmed" => encode_data(untrimmed), "trimmed" => encode_data(trimmed)}
  end

  def encode(%TerminusDB.WOQL{op: :upper, args: [left, right]}) do
    %{"@type" => "Upper", "left" => encode_data(left), "right" => encode_data(right)}
  end

  def encode(%TerminusDB.WOQL{op: :lower, args: [left, right]}) do
    %{"@type" => "Lower", "left" => encode_data(left), "right" => encode_data(right)}
  end

  def encode(%TerminusDB.WOQL{op: :pad, args: [input, pad, length, output]}) do
    %{
      "@type" => "Pad",
      "string" => encode_data(input),
      "pad" => encode_data(pad),
      "length" => encode_data(length),
      "result" => encode_data(output)
    }
  end

  def encode(%TerminusDB.WOQL{op: :split, args: [input, glue, output]}) do
    %{
      "@type" => "Split",
      "string" => encode_data(input),
      "glue" => encode_data(glue),
      "result" => encode_data(output)
    }
  end

  def encode(%TerminusDB.WOQL{op: :length, args: [list, len]}) do
    %{"@type" => "Length", "list" => encode_value(list), "length" => encode_value(len)}
  end

  def encode(%TerminusDB.WOQL{op: :regexp, args: [pattern, string, result_list]}) do
    %{
      "@type" => "Regexp",
      "pattern" => encode_data(pattern),
      "string" => encode_data(string),
      "result" => encode_data(result_list)
    }
  end

  def encode(%TerminusDB.WOQL{op: :dot, args: [document, field, value]}) do
    %{
      "@type" => "Dot",
      "document" => encode_value(document),
      "field" => encode_data(field),
      "value" => encode_value(value)
    }
  end

  def encode(%TerminusDB.WOQL{op: :member, args: [item, list]}) do
    %{"@type" => "Member", "member" => encode_value(item), "list" => encode_value(list)}
  end

  def encode(%TerminusDB.WOQL{op: :slice, args: [input, result, start, end_val]}) do
    %{
      "@type" => "Slice",
      "list" => encode_value(input),
      "slice" => encode_value(result),
      "from" => encode_value(start),
      "to" => encode_value(end_val)
    }
  end

  def encode(%TerminusDB.WOQL{op: :set_difference, args: [list_a, list_b, result]}) do
    %{
      "@type" => "SetDifference",
      "left" => encode_value(list_a),
      "right" => encode_value(list_b),
      "result" => encode_value(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :set_intersection, args: [list_a, list_b, result]}) do
    %{
      "@type" => "SetIntersection",
      "left" => encode_value(list_a),
      "right" => encode_value(list_b),
      "result" => encode_value(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :set_union, args: [list_a, list_b, result]}) do
    %{
      "@type" => "SetUnion",
      "left" => encode_value(list_a),
      "right" => encode_value(list_b),
      "result" => encode_value(result)
    }
  end

  def encode(%TerminusDB.WOQL{op: :set_member, args: [element, set]}) do
    %{"@type" => "SetMember", "element" => encode_value(element), "set" => encode_value(set)}
  end

  def encode(%TerminusDB.WOQL{op: :list_to_set, args: [input, result]}) do
    %{"@type" => "ListToSet", "list" => encode_value(input), "result" => encode_value(result)}
  end

  def encode(%TerminusDB.WOQL{op: :path, args: [subject, pattern, object]}) do
    %{
      "@type" => "Path",
      "subject" => encode_node(subject),
      "pattern" => TerminusDB.WOQL.Path.to_jsonld(pattern),
      "object" => encode_value(object)
    }
  end

  def encode(%TerminusDB.WOQL{op: :path, args: [subject, pattern, object, path_var]}) do
    %{
      "@type" => "Path",
      "subject" => encode_node(subject),
      "pattern" => TerminusDB.WOQL.Path.to_jsonld(pattern),
      "object" => encode_value(object),
      "path" => encode_value(path_var)
    }
  end

  def encode(%TerminusDB.WOQL{op: :unique, args: [prefix, key_list, uri]}) do
    %{
      "@type" => "HashKey",
      "base" => encode_data(prefix),
      "key_list" => Enum.map(key_list, &encode_data/1),
      "uri" => encode_node(uri)
    }
  end

  def encode(%TerminusDB.WOQL{op: :idgen, args: [prefix, key_list, uri]}) do
    %{
      "@type" => "LexicalKey",
      "base" => encode_data(prefix),
      "key_list" => Enum.map(key_list, &encode_data/1),
      "uri" => encode_node(uri)
    }
  end

  def encode(%TerminusDB.WOQL{op: :idgen_random, args: [prefix, uri]}) do
    %{"@type" => "RandomKey", "base" => encode_data(prefix), "uri" => encode_node(uri)}
  end

  def encode(%TerminusDB.WOQL{op: :insert_document, args: [doc, identifier]}) do
    base = %{"@type" => "InsertDocument", "document" => encode_document(doc)}

    if identifier do
      Map.put(base, "identifier", encode_node(identifier))
    else
      base
    end
  end

  def encode(%TerminusDB.WOQL{op: :update_document, args: [doc, identifier]}) do
    base = %{"@type" => "UpdateDocument", "document" => encode_document(doc)}

    if identifier do
      Map.put(base, "identifier", encode_node(identifier))
    else
      base
    end
  end

  def encode(%TerminusDB.WOQL{op: :delete_document, args: [iri]}) do
    %{"@type" => "DeleteDocument", "identifier" => encode_node(iri)}
  end

  def encode(%TerminusDB.WOQL{op: :using, args: [collection, query]}) do
    %{"@type" => "Using", "collection" => collection, "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :from, args: [graph, query]}) do
    %{"@type" => "From", "graph" => graph, "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :into, args: [graph, query]}) do
    %{"@type" => "Into", "graph" => graph, "query" => encode(query)}
  end

  def encode(%TerminusDB.WOQL{op: :comment, args: [text, query]}) do
    %{
      "@type" => "Comment",
      "comment" => %{"@type" => "xsd:string", "@value" => text},
      "query" => encode(query)
    }
  end

  def encode(%TerminusDB.WOQL{op: :size, args: [graph, size_var]}) do
    %{"@type" => "Size", "graph" => graph, "size" => encode_value(size_var)}
  end

  def encode(%TerminusDB.WOQL{op: :triple_count, args: [graph, count_var]}) do
    %{"@type" => "TripleCount", "graph" => graph, "triple_count" => encode_value(count_var)}
  end

  def encode(%TerminusDB.WOQL{op: :triple_slice, args: [s, p, o, low, high]}) do
    %{
      "@type" => "TripleSlice",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "low" => encode_value(low),
      "high" => encode_value(high)
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_slice, args: [s, p, o, low, high, graph]}) do
    %{
      "@type" => "TripleSlice",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "low" => encode_value(low),
      "high" => encode_value(high),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_slice_rev, args: [s, p, o, low, high]}) do
    %{
      "@type" => "TripleSliceRev",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "low" => encode_value(low),
      "high" => encode_value(high)
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_slice_rev, args: [s, p, o, low, high, graph]}) do
    %{
      "@type" => "TripleSliceRev",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "low" => encode_value(low),
      "high" => encode_value(high),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_next, args: [s, p, o, next_val]}) do
    %{
      "@type" => "TripleNext",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "next" => encode_value(next_val)
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_next, args: [s, p, o, next_val, graph]}) do
    %{
      "@type" => "TripleNext",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "next" => encode_value(next_val),
      "graph" => graph
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_previous, args: [s, p, o, prev_val]}) do
    %{
      "@type" => "TriplePrevious",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "previous" => encode_value(prev_val)
    }
  end

  def encode(%TerminusDB.WOQL{op: :triple_previous, args: [s, p, o, prev_val, graph]}) do
    %{
      "@type" => "TriplePrevious",
      "subject" => encode_node(s),
      "predicate" => encode_node(p),
      "object" => encode_value(o),
      "previous" => encode_value(prev_val),
      "graph" => graph
    }
  end

  # --------------------------------------------------------------------------
  # Temporal / Allen interval algebra
  # --------------------------------------------------------------------------

  def encode(%TerminusDB.WOQL{op: :interval, args: [s, e, i]}) do
    %{
      "@type" => "Interval",
      "start" => encode_value(s),
      "end" => encode_value(e),
      "interval" => encode_value(i)
    }
  end

  def encode(%TerminusDB.WOQL{op: :interval_start_duration, args: [s, d, i]}) do
    %{
      "@type" => "IntervalStartDuration",
      "start" => encode_value(s),
      "duration" => encode_value(d),
      "interval" => encode_value(i)
    }
  end

  def encode(%TerminusDB.WOQL{op: :interval_duration_end, args: [d, e, i]}) do
    %{
      "@type" => "IntervalDurationEnd",
      "duration" => encode_value(d),
      "end" => encode_value(e),
      "interval" => encode_value(i)
    }
  end

  def encode(%TerminusDB.WOQL{op: :interval_relation, args: [r, xs, xe, ys, ye]}) do
    %{
      "@type" => "IntervalRelation",
      "relation" => encode_value(r),
      "start" => encode_value(xs),
      "end" => encode_value(xe),
      "start2" => encode_value(ys),
      "end2" => encode_value(ye)
    }
  end

  def encode(%TerminusDB.WOQL{op: :interval_relation_typed, args: [r, x, y]}) do
    %{
      "@type" => "IntervalRelationTyped",
      "relation" => encode_value(r),
      "left" => encode_value(x),
      "right" => encode_value(y)
    }
  end

  def encode(%TerminusDB.WOQL{op: :date_duration, args: [s, e, d]}) do
    %{
      "@type" => "DateDuration",
      "start" => encode_value(s),
      "end" => encode_value(e),
      "duration" => encode_value(d)
    }
  end

  def encode(%TerminusDB.WOQL{op: :day_after, args: [d, n]}) do
    %{
      "@type" => "DayAfter",
      "date" => encode_value(d),
      "next" => encode_value(n)
    }
  end

  def encode(%TerminusDB.WOQL{op: :day_before, args: [d, p]}) do
    %{
      "@type" => "DayBefore",
      "date" => encode_value(d),
      "previous" => encode_value(p)
    }
  end

  def encode(%TerminusDB.WOQL{op: :weekday, args: [d, w]}) do
    %{
      "@type" => "Weekday",
      "date" => encode_value(d),
      "weekday" => encode_value(w)
    }
  end

  def encode(%TerminusDB.WOQL{op: :weekday_sunday_start, args: [d, w]}) do
    %{
      "@type" => "WeekdaySundayStart",
      "date" => encode_value(d),
      "weekday" => encode_value(w)
    }
  end

  def encode(%TerminusDB.WOQL{op: :iso_week, args: [d, y, w]}) do
    %{
      "@type" => "IsoWeek",
      "date" => encode_value(d),
      "year" => encode_value(y),
      "week" => encode_value(w)
    }
  end

  def encode(%TerminusDB.WOQL{op: :month_start_date, args: [ym, d]}) do
    %{
      "@type" => "MonthStartDate",
      "year_month" => encode_value(ym),
      "date" => encode_value(d)
    }
  end

  def encode(%TerminusDB.WOQL{op: :month_end_date, args: [ym, d]}) do
    %{
      "@type" => "MonthEndDate",
      "year_month" => encode_value(ym),
      "date" => encode_value(d)
    }
  end

  def encode(%TerminusDB.WOQL{op: :month_start_dates, args: [d, s, e]}) do
    %{
      "@type" => "MonthStartDates",
      "date" => encode_value(d),
      "start" => encode_value(s),
      "end" => encode_value(e)
    }
  end

  def encode(%TerminusDB.WOQL{op: :month_end_dates, args: [d, s, e]}) do
    %{
      "@type" => "MonthEndDates",
      "date" => encode_value(d),
      "start" => encode_value(s),
      "end" => encode_value(e)
    }
  end

  def encode(%TerminusDB.WOQL{op: :in_range, args: [v, s, e]}) do
    %{
      "@type" => "InRange",
      "value" => encode_value(v),
      "start" => encode_value(s),
      "end" => encode_value(e)
    }
  end

  def encode(%TerminusDB.WOQL{op: :sequence, args: [v, s, e, step, count]}) do
    base = %{
      "@type" => "Sequence",
      "value" => encode_value(v),
      "start" => encode_value(s),
      "end" => encode_value(e)
    }

    base
    |> maybe_put_encoded("step", step)
    |> maybe_put_encoded("count", count)
  end

  def encode(%TerminusDB.WOQL{op: :range_min, args: [lst, r]}) do
    %{
      "@type" => "RangeMin",
      "list" => encode_data_list(lst),
      "min" => encode_value(r)
    }
  end

  def encode(%TerminusDB.WOQL{op: :range_max, args: [lst, r]}) do
    %{
      "@type" => "RangeMax",
      "list" => encode_data_list(lst),
      "max" => encode_value(r)
    }
  end

  # --------------------------------------------------------------------------
  # CSV / IO
  # --------------------------------------------------------------------------

  def encode(%TerminusDB.WOQL{op: :get, args: [as_vars, resource]}) do
    %{
      "@type" => "Get",
      "columns" => as_vars,
      "resource" => encode(resource)
    }
  end

  def encode(%TerminusDB.WOQL{op: :put, args: [as_vars, query, resource]}) do
    %{
      "@type" => "Put",
      "columns" => as_vars,
      "query" => encode(query),
      "resource" => encode(resource)
    }
  end

  def encode(%TerminusDB.WOQL{op: :file, args: [fpath, format]}) do
    %{
      "@type" => "QueryResource",
      "source" => %{"@type" => "FileResource", "file_name" => fpath},
      "format" => encode_format(format)
    }
  end

  def encode(%TerminusDB.WOQL{op: :remote, args: [uri, format]}) do
    %{
      "@type" => "QueryResource",
      "source" => %{"@type" => "RemoteResource", "url" => uri},
      "format" => encode_format(format)
    }
  end

  def encode(%TerminusDB.WOQL{op: :post, args: [fpath, format]}) do
    %{
      "@type" => "QueryResource",
      "source" => %{"@type" => "PostResource", "file_name" => fpath},
      "format" => encode_format(format)
    }
  end

  defp encode_format(format) do
    %{
      "@type" => "Format",
      "format_type" => %{"@type" => "xsd:string", "@value" => format}
    }
  end

  defp maybe_put_encoded(map, _key, nil), do: map
  defp maybe_put_encoded(map, key, value), do: Map.put(map, key, encode_value(value))

  # --------------------------------------------------------------------------
  # NodeValue — nodes/IRIs (subjects, predicates, identifiers)
  # --------------------------------------------------------------------------

  def encode_node(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "NodeValue", "variable" => String.slice(var, 2..-1//1)}
  end

  def encode_node(value) when is_binary(value) do
    %{"@type" => "NodeValue", "node" => value}
  end

  def encode_node(value) when is_map(value), do: value

  # --------------------------------------------------------------------------
  # Value — generic values (triple objects, comparison operands, type_of)
  # --------------------------------------------------------------------------

  def encode_value(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "Value", "variable" => String.slice(var, 2..-1//1)}
  end

  def encode_value(value) when is_binary(value) do
    %{"@type" => "Value", "data" => %{"@type" => "xsd:string", "@value" => value}}
  end

  def encode_value(value) when is_integer(value) do
    %{"@type" => "Value", "data" => %{"@type" => "xsd:integer", "@value" => value}}
  end

  def encode_value(value) when is_float(value) do
    %{"@type" => "Value", "data" => %{"@type" => "xsd:decimal", "@value" => value}}
  end

  def encode_value(value) when is_boolean(value) do
    %{"@type" => "Value", "data" => %{"@type" => "xsd:boolean", "@value" => value}}
  end

  def encode_value(%{"@value" => _} = literal) do
    %{"@type" => "Value", "data" => literal}
  end

  def encode_value(value) when is_map(value), do: value

  def encode_value(values) when is_list(values) do
    %{"@type" => "Value", "list" => Enum.map(values, &encode_value/1)}
  end

  # --------------------------------------------------------------------------
  # Document — encode raw document maps as DictionaryTemplate (InsertDocument
  # / UpdateDocument). Variables pass through as Value.
  # --------------------------------------------------------------------------

  def encode_document(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "Value", "variable" => String.slice(var, 2..-1//1)}
  end

  def encode_document(doc) when is_map(doc) do
    pairs =
      Enum.map(doc, fn {key, value} ->
        %{
          "@type" => "FieldValuePair",
          "field" => key,
          "value" => encode_value(value)
        }
      end)

    %{
      "@type" => "Value",
      "dictionary" => %{
        "@type" => "DictionaryTemplate",
        "data" => pairs
      }
    }
  end

  def encode_document(value), do: encode_value(value)

  # --------------------------------------------------------------------------
  # DataValue — literal data (string-op operands, ID-gen keys)
  # --------------------------------------------------------------------------

  def encode_data(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "DataValue", "variable" => String.slice(var, 2..-1//1)}
  end

  def encode_data(value) when is_binary(value) do
    %{"@type" => "DataValue", "data" => %{"@type" => "xsd:string", "@value" => value}}
  end

  def encode_data(value) when is_integer(value) do
    %{"@type" => "DataValue", "data" => %{"@type" => "xsd:integer", "@value" => value}}
  end

  def encode_data(value) when is_float(value) do
    %{"@type" => "DataValue", "data" => %{"@type" => "xsd:decimal", "@value" => value}}
  end

  def encode_data(value) when is_boolean(value) do
    %{"@type" => "DataValue", "data" => %{"@type" => "xsd:boolean", "@value" => value}}
  end

  def encode_data(%{"@value" => _} = literal) do
    %{"@type" => "DataValue", "data" => literal}
  end

  def encode_data(values) when is_list(values) do
    %{"@type" => "DataValue", "list" => Enum.map(values, &encode_data/1)}
  end

  def encode_data(value) when is_map(value), do: value

  defp encode_data_list(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "DataValue", "variable" => String.slice(var, 2..-1//1)}
  end

  defp encode_data_list(values) when is_list(values) do
    %{"@type" => "DataValue", "list" => Enum.map(values, &encode_data/1)}
  end

  defp encode_data_list(value), do: encode_data(value)

  # --------------------------------------------------------------------------
  # ArithmeticValue — arithmetic operands
  # --------------------------------------------------------------------------

  def encode_arithmetic(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    %{"@type" => "ArithmeticValue", "variable" => String.slice(var, 2..-1//1)}
  end

  def encode_arithmetic(value) when is_integer(value) do
    %{"@type" => "ArithmeticValue", "data" => %{"@type" => "xsd:integer", "@value" => value}}
  end

  def encode_arithmetic(value) when is_float(value) do
    %{"@type" => "ArithmeticValue", "data" => %{"@type" => "xsd:decimal", "@value" => value}}
  end

  def encode_arithmetic(%{"@value" => _} = literal) do
    %{"@type" => "ArithmeticValue", "data" => literal}
  end

  def encode_arithmetic(%TerminusDB.WOQL{} = q), do: encode(q)

  def encode_arithmetic(value) when is_binary(value) do
    %{"@type" => "ArithmeticValue", "data" => %{"@type" => "xsd:string", "@value" => value}}
  end

  def encode_arithmetic(value) when is_map(value), do: value

  # --------------------------------------------------------------------------
  # Select variables — bare variable name strings (without v: prefix)
  # --------------------------------------------------------------------------

  def encode_select_var(var) when is_binary(var) and binary_part(var, 0, 2) == "v:" do
    String.slice(var, 2..-1//1)
  end

  def encode_select_var(var) when is_binary(var), do: var
end