/*  Identity management and indexing for RDF
 *
 * This file provides  IndexedFormula a formula (set of triples) which
 * indexed by predicate, subject and object.
 *
 * It "smushes"  (merges into a single node) things which are identical
 * according to owl:sameAs or an owl:InverseFunctionalProperty
 * or an owl:FunctionalProperty
 *
 *
 *  2005-10 Written Tim Berners-Lee
 *  2007    Changed so as not to munge statements from documents when smushing
 *  2019    Converted to typescript
 *
 *
*/

/** @module store */

import ClassOrder from './class-order'
import { defaultGraphURI } from './factories/canonical-data-factory'
import Formula, { FormulaOpts } from './formula'
import { ArrayIndexOf } from './utils'
import { RDFArrayRemove } from './utils-js'
import {
  isRDFlibSubject,
  isRDFlibPredicate,
  isRDFlibObject,
  isStore,
  isGraph,
  // isPredicate,
  isQuad,
  // isSubject
} from './utils/terms'
import Node from './node'
import Variable from './variable'
import { Query, indexedFormulaQuery } from './query'
import UpdateManager from './update-manager'
import {
  Bindings, BlankNodeTermType, CollectionTermType, DefaultGraphTermType, EmptyTermType, GraphTermType, LiteralTermType, NamedNodeTermType, VariableTermType
} from './types'
import Statement from './statement'
import { Indexable } from './factories/factory-types'
import NamedNode from './named-node'
import Fetcher from './fetcher'
import {
  Quad_Graph,
  Literal as TFLiteral,
  NamedNode as TFNamedNode,
  Quad_Object,
  Quad_Predicate,
  Quad,
  Quad_Subject,
  Term,
} from './tf-types'
import { namedNode } from './index'
import serialize from "./serialize";
import BlankNode from './blank-node'
import DefaultGraph from './default-graph'
import Empty from './empty'
import Literal from './literal'
import Collection from './collection'

const owlNamespaceURI = 'http://www.w3.org/2002/07/owl#'

type FeaturesType = Array<('sameAs' | 'InverseFunctionalProperty' | 'FunctionalProperty')> | undefined

export { defaultGraphURI }
// var link_ns = 'http://www.w3.org/2007/ont/link#'

// Handle Functional Property
function handleFP (
  formula: IndexedFormula,
  subj: Quad_Subject,
  pred: Quad_Predicate,
  obj: Quad_Object
): boolean {
  var o1 = formula.any(subj, pred, undefined)
  if (!o1) {
    return false // First time with this value
  }
  // log.warn("Equating "+o1.uri+" and "+obj.uri + " because FP "+pred.uri);  //@@
  formula.equate(o1, obj)
  return true
} // handleFP

// Handle Inverse Functional Property
function handleIFP (
  formula: IndexedFormula,
  subj: Quad_Subject,
  pred: Quad_Predicate,
  obj: Quad_Object
): boolean {
  var s1 = formula.any(undefined, pred, obj)
  if (!s1) {
    return false // First time with this value
  }
  // log.warn("Equating "+s1.uri+" and "+subj.uri + " because IFP "+pred.uri);  //@@
  formula.equate(s1, subj)
  return true
} // handleIFP

function handleRDFType (
  formula: IndexedFormula,
  subj: Quad_Subject,
  pred: Quad_Predicate,
  obj: Quad_Object,
  why: Quad_Graph
) {
  //@ts-ignore this method does not seem to exist in this library
  if (formula.typeCallback) {
    (formula as any).typeCallback(formula, obj, why)
  }

  var x = formula.classActions[formula.id(obj)]
  var done = false
  if (x) {
    for (var i = 0; i < x.length; i++) {
      done = done || x[i](formula, subj, pred, obj, why)
    }
  }
  return done // statement given is not needed if true
}

/**
 * Indexed Formula aka Store
 */
export default class IndexedFormula extends Formula { // IN future - allow pass array of statements to constructor
  /**
   * An UpdateManager initialised to this store
   */
  updater?: UpdateManager

  /**
   * Dictionary of namespace prefixes
   */
  namespaces: {[key: string]: string}

  /** Map of iri predicates to functions to call when adding { s type X } */
  classActions: { [k: string]: Function[] }
  /** Map of iri predicates to functions to call when getting statement with {s X o} */
  propertyActions: { [k: string]: Function[] }
  /** Redirect to lexically smaller equivalent symbol */
  redirections: any[]
  /** Reverse mapping to redirection: aliases for this */
  aliases: any[]
  /** Redirections we got from HTTP */
  HTTPRedirects: Statement[]
  /** Array of statements with this X as subject */
  subjectIndex: Statement[]
  /** Array of statements with this X as predicate */
  predicateIndex: Statement[]
  /** Array of statements with this X as object */
  objectIndex: Statement[]
  /** Array of statements with X as provenance */
  whyIndex: Statement[]
  index: [
    Statement[],
    Statement[],
    Statement[],
    Statement[]
  ]
  features: FeaturesType
  static handleRDFType: Function
  _universalVariables?: NamedNode[]
  _existentialVariables?: BlankNode[]

  /** Function to remove quads from the store arrays with */
  private rdfArrayRemove: (arr: Quad[], q: Quad) => void
  /** Callbacks which are triggered after a statement has been added to the store */
  private dataCallbacks?: Array<(q: Quad) => void>

  /**
   * Creates a new formula
   * @param features - What sort of automatic processing to do? Array of string
   * @param features.sameAs - Smush together A and B nodes whenever { A sameAs B }
   * @param opts
   * @param [opts.rdfFactory] - The data factory that should be used by the store
   * @param [opts.rdfArrayRemove] - Function which removes statements from the store
   * @param [opts.dataCallback] - Callback when a statement is added to the store, will not trigger when adding duplicates
   */
  constructor (features?: FeaturesType, opts: FormulaOpts = {}) {
    super(undefined, undefined, undefined, undefined, opts)

    this.propertyActions = {}
    this.classActions = {}
    this.redirections = []
    this.aliases = []
    this.HTTPRedirects = []
    this.subjectIndex = []
    this.predicateIndex = []
    this.objectIndex = []
    this.whyIndex = []
    this.index = [
      this.subjectIndex,
      this.predicateIndex,
      this.objectIndex,
      this.whyIndex
    ]
    this.namespaces = {} // Dictionary of namespace prefixes
    this.features = features || [ // By default, devs do not expect these features.
// See https://github.com/linkeddata/rdflib.js/issues/458
//      'sameAs',
//      'InverseFunctionalProperty',
//      'FunctionalProperty',
    ]
    this.rdfArrayRemove = opts.rdfArrayRemove || RDFArrayRemove
    if (opts.dataCallback) {
      this.dataCallbacks = [opts.dataCallback]
    }

    this.initPropertyActions(this.features)
  }

  /**
   * Gets the URI of the default graph
   */
  static get defaultGraphURI(): string {
    return defaultGraphURI
  }

  /**
   * Gets this graph with the bindings substituted
   * @param bindings The bindings
   */
  substitute <T extends Node = IndexedFormula>(bindings: Bindings): T {
    var statementsCopy = this.statements.map(function (ea: Quad) {
      return (ea as Statement).substitute(bindings)
    })
    var y = new IndexedFormula()
    y.add(statementsCopy)
    return y as unknown as T
  }

  /**
   * Add a callback which will be triggered after a statement has been added to the store.
   * @param cb
   */
  addDataCallback(cb: (q: Quad) => void): void {
    if (!this.dataCallbacks) {
      this.dataCallbacks = []
    }
    this.dataCallbacks.push(cb)
  }

  /**
   * Apply a set of statements to be deleted and to be inserted
   *
   * @param patch - The set of statements to be deleted and to be inserted
   * @param target - The name of the document to patch
   * @param patchCallback - Callback to be called when patching is complete
   */
  applyPatch(
    patch: {
        delete?: ReadonlyArray<Statement>,
        patch?: ReadonlyArray<Statement>,
        where?: any
    },
    target: TFNamedNode,
    patchCallback: (errorString?: string) => void
  ): void {
    var targetKB = this
    var ds
    var binding: Bindings | null = null

    function doPatch (onDonePatch: (errorString?: string) => void) {
      if (patch['delete']) {
        ds = patch['delete']
        // console.log(bindingDebug(binding))
        // console.log('ds before substitute: ' + ds)
        if (binding) ds = ds.substitute(binding)
        // console.log('applyPatch: delete: ' + ds)
        ds = ds.statements as Statement[]
        var bad: Quad[] = []
        var ds2 = ds.map(function (st: Quad) { // Find the actual statements in the store
          var sts = targetKB.statementsMatching(st.subject, st.predicate, st.object, target)
          if (sts.length === 0) {
            // log.info("NOT FOUND deletable " + st)
            bad.push(st)
            return null
          } else {
            // log.info("Found deletable " + st)
            return sts[0]
          }
        })
        if (bad.length) {
          // console.log('Could not find to delete ' + bad.length + 'statements')
          // console.log('despite ' + targetKB.statementsMatching(bad[0].subject, bad[0].predicate)[0])
          return patchCallback('Could not find to delete: ' + bad.join('\n or '))
        }
        ds2.map(function (st: Quad) {
          targetKB.remove(st)
        })
      }
      if (patch['insert']) {
        // log.info("doPatch insert "+patch['insert'])
        ds = patch['insert']
        if (binding) ds = ds.substitute(binding)
        ds = ds.statements
        ds.map(function (st: Quad) {
          st.graph = target
          targetKB.add(st.subject, st.predicate, st.object, st.graph)
        })
      }
      onDonePatch()
    }
    if (patch.where) {
      // log.info("Processing WHERE: " + patch.where + '\n')
      var query = new Query('patch')
      query.pat = patch.where
      query.pat.statements.map(function (st) {
        st.graph = namedNode(target.value)
      })
      //@ts-ignore TODO: add sync property to Query when converting Query to typescript
      query.sync = true

      var bindingsFound: Bindings[] = []

      targetKB.query(
        query,
        function onBinding (binding) {
        bindingsFound.push(binding)
        // console.log('   got a binding: ' + bindingDebug(binding))
      },
        targetKB.fetcher,
        function onDone () {
          if (bindingsFound.length === 0) {
            return patchCallback('No match found to be patched:' + patch.where)
          }
          if (bindingsFound.length > 1) {
            return patchCallback('Patch ambiguous. No patch done.')
          }
          binding = bindingsFound[0]
          doPatch(patchCallback)
        })
    } else {
      doPatch(patchCallback)
    }
  }

  /**
   * N3 allows for declaring blank nodes, this function enables that support
   *
   * @param x The blank node to be declared, supported in N3
   */
  declareExistential(x: BlankNode): BlankNode {
    if (!this._existentialVariables) this._existentialVariables = []
    this._existentialVariables.push(x)
    return x
  }

  /**
   * @param features
   */
  initPropertyActions(features: FeaturesType) {
    // If the predicate is #type, use handleRDFType to create a typeCallback on the object
    this.propertyActions[this.rdfFactory.id(this.rdfFactory.namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'))] =
      [ handleRDFType ]

    // Assumption: these terms are not redirected @@fixme
    if (ArrayIndexOf(features, 'sameAs') >= 0) {
      this.propertyActions[this.rdfFactory.id(this.rdfFactory.namedNode(`${owlNamespaceURI}sameAs`))] = [
        function (formula, subj, pred, obj, why) {
          // log.warn("Equating "+subj.uri+" sameAs "+obj.uri);  //@@
          formula.equate(subj, obj)
          return true // true if statement given is NOT needed in the store
        }
      ] // sameAs -> equate & don't add to index
    }
    if (ArrayIndexOf(features, 'InverseFunctionalProperty') >= 0) {
      this.classActions[this.rdfFactory.id(this.rdfFactory.namedNode(`${owlNamespaceURI}InverseFunctionalProperty`))] = [
        function (formula, subj, pred, obj, addFn) {
          // yes subj not pred!
          return formula.newPropertyAction(subj, handleIFP)
        }
      ] // IFP -> handleIFP, do add to index
    }
    if (ArrayIndexOf(features, 'FunctionalProperty') >= 0) {
      this.classActions[this.rdfFactory.id(this.rdfFactory.namedNode(`${owlNamespaceURI}FunctionalProperty`))] = [
        function (formula, subj, proj, obj, addFn) {
          return formula.newPropertyAction(subj, handleFP)
        }
      ] // FP => handleFP, do add to index
    }
  }

  /** @deprecated Use {add} instead */
  addStatement (st: Quad): number {
    this.add(st.subject, st.predicate, st.object, st.graph)
    return this.statements.length
  }

  /**
   * Adds a triple (quad) to the store.
   *
   * @param subj - The thing about which the fact a relationship is asserted.
   *        Also accepts a statement or an array of Statements.
   * @param pred - The relationship which is asserted
   * @param obj - The object of the relationship, e.g. another thing or a value. If passed a string, this will become a literal.
   * @param why - The document in which the triple (S,P,O) was or will be stored on the web
   * @returns The statement added to the store, or the store
   */
  add (
    subj: Quad_Subject | Quad | Quad[] | Statement | Statement[],
    pred?: Quad_Predicate,
    obj?: Term | string,
    why?: Quad_Graph
  ): Statement | null | this | number {
    var i: number
    if (arguments.length === 1) {
      if (subj instanceof Array) {
        for (i = 0; i < subj.length; i++) {
          this.add(subj[i])
        }
      } else if (isQuad(subj)) {
        this.add(subj.subject, subj.predicate, subj.object, subj.graph)
      } else if (isStore(subj)) {
        this.add((subj as any).statements)
      }
      return this
    }
    var actions: Function[]
    var st: Statement
    if (!why) {
      // system generated
      why = this.fetcher ? this.fetcher.appNode : this.rdfFactory.defaultGraph()
    }
    if (typeof subj == 'string') {
      subj = this.rdfFactory.namedNode(subj)
    }
    pred = Node.fromValue(pred)
    const objNode = Node.fromValue(obj) as Term
    why = Node.fromValue(why)
    if (!isRDFlibSubject(subj)) {
      throw new Error('Subject is not a subject type')
    }
    if (!isRDFlibPredicate(pred)) {
      throw new Error(`Predicate ${pred} is not a predicate type`)
    }
    if (!isRDFlibObject(objNode)) {
      throw new Error(`Object ${objNode} is not an object type`)
    }
    if (!isGraph(why)) {
      throw new Error("Why is not a graph type")
    }
    //@ts-ignore This is not used internally
    if (this.predicateCallback) {
      //@ts-ignore This is not used internally
      this.predicateCallback(this, pred, why)
    }
    // Action return true if the statement does not need to be added
    var predHash = this.id(this.canon(pred!))
    actions = this.propertyActions[predHash] // Predicate hash
    var done = false
    if (actions) {
      // alert('type: '+typeof actions +' @@ actions='+actions)
      for (i = 0; i < actions.length; i++) {
        done = done || actions[i](this, subj, pred, objNode, why)
      }
    }
    if (this.holds(subj, pred, objNode, why)) { // Takes time but saves duplicates
      // console.log('rdflib: Ignoring dup! {' + subj + ' ' + pred + ' ' + obj + ' ' + why + '}')
      return null // @@better to return self in all cases?
    }
    // If we are tracking provenance, every thing should be loaded into the store
    // if (done) return this.rdfFactory.quad(subj, pred, obj, why)
    // Don't put it in the store
    // still return this statement for owl:sameAs input
    var hash = [
      this.id(this.canon(subj)),
      predHash,
      this.id(this.canon(objNode)),
      this.id(this.canon(why))
    ]
    // @ts-ignore this will fail if you pass a collection and the factory does not allow Collections
    st = this.rdfFactory.quad(subj, pred, objNode, why)
    for (i = 0; i < 4; i++) {
      var ix = this.index[i]
      var h = hash[i]
      if (!ix[h]) {
        ix[h] = []
      }
      ix[h].push(st) // Set of things with this as subject, etc
    }

    // log.debug("ADDING    {"+subj+" "+pred+" "+objNode+"} "+why)
    this.statements.push(st)

    if (this.dataCallbacks) {
      for (const callback of this.dataCallbacks) {
        callback(st)
      }
    }

    return st
  }

  /**
   * Returns the symbol with canonical URI as smushed
   * @param term - An RDF node
   */
  canon(term?: Term): Node {
    if (!term) {
      // @@ TODO Should improve this to return proper value - doing this to keep it backward compatible
      return term as unknown as Node
    }
    const y = this.redirections[this.id(term)]
    if (y) {
      return y
    }
    switch (term.termType) {
      case BlankNodeTermType:
        return new BlankNode(term.value)
      case CollectionTermType:
        return term as Collection // non-RDF/JS type, should just need to cast
      case DefaultGraphTermType:
        return new DefaultGraph()
      case EmptyTermType: // non-RDF/JS type, should just need to cast
        return term as Empty
      case GraphTermType: // non-RDF/JS type, should just need to cast
        return term as IndexedFormula
      case LiteralTermType:
        return new Literal(term.value, (term as TFLiteral).language, (term as TFLiteral).datatype)
      case NamedNodeTermType:
        return new NamedNode(term.value)
      case VariableTermType:
        return new Variable(term.value)
      default:
        throw new Error(`Term Type not recognized for canonization: ${term.termType}`)
    }
  }


  /**
   * Checks this formula for consistency
   */
  check(): void {
    this.checkStatementList(this.statements)
    for (var p = 0; p < 4; p++) {
      var ix = this.index[p]
      for (var key in ix) {
        if (ix.hasOwnProperty(key)) {
          // @ts-ignore should this pass an array or a single statement? checkStateMentsList expects an array.
          this.checkStatementList(ix[key], p)
        }
      }
    }
  }

  /**
   * Checks a list of statements for consistency
   * @param sts - The list of statements to check
   * @param from - An index with the array ['subject', 'predicate', 'object', 'why']
   */
  checkStatementList(
    sts: ReadonlyArray<Quad>,
    from?: number
  ): boolean | void {
    if (from === undefined) {
      from = 0
    }
    var names = ['subject', 'predicate', 'object', 'why']
    var origin = ' found in ' + names[from] + ' index.'
    var st: Quad
    for (var j = 0; j < sts.length; j++) {
      st = sts[j]
      var term = [ st.subject, st.predicate, st.object, st.graph ]
      var arrayContains = function (a: Array<any>, x: Quad) {
        for (var i = 0; i < a.length; i++) {
          if (a[i].subject.equals(x.subject) &&
            a[i].predicate.equals(x.predicate) &&
            a[i].object.equals(x.object) &&
            a[i].why.equals(x.graph)) {
            return true
          }
        }
      }
      for (var p = 0; p < 4; p++) {
        var c = this.canon(term[p])
        var h = this.id(c)
        if (!this.index[p][h]) {
          // throw new Error('No ' + name[p] + ' index for statement ' + st + '@' + st.why + origin)
        } else {
          if (!arrayContains(this.index[p][h], st)) {
            // throw new Error('Index for ' + name[p] + ' does not have statement ' + st + '@' + st.why + origin)
          }
        }
      }
      if (!arrayContains(this.statements, st)) {
        throw new Error('Statement list does not statement ' + st + '@' + st.graph + origin)
      }
    }
  }

  /**
   * Closes this formula (and return it)
   */
  close(): IndexedFormula {
    return this
  }

  compareTerms(u1: Term, u2: Term): number {
    // Keep compatibility with downstream classOrder changes
    if (Object.prototype.hasOwnProperty.call(u1, "compareTerm")) {
      return (u1 as Node).compareTerm(u2 as Node)
    }
    if (ClassOrder[u1.termType] < ClassOrder[u2.termType]) {
      return -1
    }
    if (ClassOrder[u1.termType] > ClassOrder[u2.termType]) {
      return +1
    }
    if (u1.value < u2.value) {
      return -1
    }
    if (u1.value > u2.value) {
      return +1
    }
    return 0
  }


  /**
   * replaces @template with @target and add appropriate triples
   * removes no triples by default and is a one-direction replication
   * @param template node to copy
   * @param target node to copy to
   * @param flags Whether or not to do a two-directional copy and/or delete triples
   */
  copyTo(
    template: Quad_Subject,
    target: Quad_Subject,
    flags?: Array<('two-direction' | 'delete')>
  ): void {
    if (!flags) flags = []
    var statList = this.statementsMatching(template)
    if (ArrayIndexOf(flags, 'two-direction') !== -1) {
      statList.concat(this.statementsMatching(undefined, undefined, template))
    }
    for (var i = 0; i < statList.length; i++) {
      var st = statList[i]
      switch (st.object.termType) {
        case 'NamedNode':
          this.add(target, st.predicate, st.object)
          break
        case 'Literal':
        case 'BlankNode':
        // @ts-ignore Collections can appear here
        case 'Collection':
          // @ts-ignore Possible bug: copy is not available on Collections
          this.add(target, st.predicate, st.object.copy(this))
      }
      if (ArrayIndexOf(flags, 'delete') !== -1) {
        this.remove(st)
      }
    }
  }

  /**
   * Simplify graph in store when we realize two identifiers are equivalent
   * We replace the bigger with the smaller.
   * @param u1in The first node
   * @param u2in The second node
   */
  equate(u1in: Term, u2in : Term): boolean {
    // log.warn("Equating "+u1+" and "+u2); // @@
    // @@JAMBO Must canonicalize the uris to prevent errors from a=b=c
    // 03-21-2010
    const u1 = this.canon(u1in) as Quad_Subject
    const u2 = this.canon(u2in) as Quad_Subject
    var d = this.compareTerms(u1, u2)
    if (!d) {
      return true // No information in {a = a}
    }
    // var big
    // var small
    if (d < 0) { // u1 less than u2
      return this.replaceWith(u2, u1)
    } else {
      return this.replaceWith(u1, u2)
    }
  }

  /**
   * Creates a new empty indexed formula
   * Only applicable for IndexedFormula, but TypeScript won't allow a subclass to override a property
   * @param features The list of features
   */
  formula(features: FeaturesType): IndexedFormula {
    return new IndexedFormula(features)
  }

  /**
   * Returns the number of statements contained in this IndexedFormula.
   * (Getter proxy to this.statements).
   * Usage:
   *    ```
   *    var kb = rdf.graph()
   *    kb.length  // -> 0
   *    ```
   * @returns {Number}
   */
  get length (): number {
    return this.statements.length
  }

  /**
   * Returns any quads matching the given arguments.
   * Standard RDFJS spec method for Source objects, implemented as an
   * alias to `statementsMatching()`
   * @param subject The subject
   * @param predicate The predicate
   * @param object The object
   * @param graph The graph that contains the statement
   */
  match(
    subject?: Quad_Subject | null,
    predicate?: Quad_Predicate | null,
    object?: Quad_Object | null,
    graph?: Quad_Graph | null
  ): Quad[] {
    return this.statementsMatching(
      Node.fromValue(subject),
      Node.fromValue(predicate),
      Node.fromValue(object),
      Node.fromValue(graph)
    )
  }

  /**
   * Find out whether a given URI is used as symbol in the formula
   * @param uri The URI to look for
   */
  mentionsURI(uri: string): boolean {
    var hash = '<' + uri + '>'
    return (!!this.subjectIndex[hash] ||
    !!this.objectIndex[hash] ||
    !!this.predicateIndex[hash])
  }

  /**
   * Existentials are BNodes - something exists without naming
   * @param uri An URI
   */
  newExistential(uri: string): Term {
    if (!uri) return this.bnode()
    var x = this.sym(uri)
    // @ts-ignore x should be blanknode, but is namedNode.
    return this.declareExistential(x)
  }

  /**
   * Adds a new property action
   * @param pred the predicate that the function should be triggered on
   * @param action the function that should trigger
   */
  newPropertyAction(
    pred: Quad_Predicate,
    action: (
      store: IndexedFormula,
      subject: Quad_Subject,
      predicate: Quad_Predicate,
      object: Quad_Object
    ) => boolean
  ): boolean {
    // log.debug("newPropertyAction:  "+pred)
    var hash = this.id(pred)
    if (!this.propertyActions[hash]) {
      this.propertyActions[hash] = []
    }
    this.propertyActions[hash].push(action)
    // Now apply the function to to statements already in the store
    var toBeFixed = this.statementsMatching(undefined, pred, undefined)
    var done = false
    for (var i = 0; i < toBeFixed.length; i++) { // NOT optimized - sort toBeFixed etc
      done = done || action(this, toBeFixed[i].subject, pred, toBeFixed[i].object)
    }
    return done
  }

  /**
   * Creates a new universal node
   * Universals are Variables
   * @param uri An URI
   */
  newUniversal(uri: string): TFNamedNode {
    var x = this.sym(uri)
    if (!this._universalVariables) this._universalVariables = []
    this._universalVariables.push(x)
    return x
  }

  // convenience function used by N3 parser
  variable (name: string) {
    return new Variable(name)
  }

  /**
   * Find an unused id for a file being edited: return a symbol
   * (Note: Slow iff a lot of them -- could be O(log(k)) )
   * @param doc A document named node
   */
  nextSymbol(doc: TFNamedNode): TFNamedNode {
    for (var i = 0; ;i++) {
      var uri = doc.value + '#n' + i
      if (!this.mentionsURI(uri)) return this.sym(uri)
    }
  }

  /**
   * Query this store asynchronously, return bindings in callback
   *
   * @param myQuery The query to be run
   * @param callback Function to call when bindings
   * @param Fetcher | null  If you want the query to do link following
   * @param onDone OBSOLETE - do not use this // @@ Why not ?? Called when query complete
   */
  query(
    myQuery: Query,
    callback: (bindings: Bindings) => void,
    fetcher?: Fetcher | null,
    onDone?: () => void
  ): void {
    return indexedFormulaQuery.call(this, myQuery, callback, fetcher, onDone)
  }

  /**
   * Query this store synchronously and return bindings
   *
   * @param myQuery The query to be run
   */
  querySync(myQuery: Query): any[] {
    var results: Bindings[] = []
    function saveBinginds (bindings: Bindings) {
      results.push(bindings)
    }
    function onDone () {
      done = true
    }
    var done = false
    // @ts-ignore TODO: Add .sync to Query
    myQuery.sync = true

    indexedFormulaQuery.call(this, myQuery, saveBinginds, null, onDone)
    if (!done) {
      throw new Error('Sync query should have called done function')
    }
    return results
  }

  /**
   * Removes one or multiple statement(s) from this formula
   * @param st - A Statement or array of Statements to remove
   */
  remove(st: Quad | Quad[]): IndexedFormula {
    if (st instanceof Array) {
      for (var i = 0; i < st.length; i++) {
        this.remove(st[i])
      }
      return this
    }
    if (isStore(st)) {
      return this.remove(st.statements)
    }
    var sts = this.statementsMatching(st.subject, st.predicate, st.object, st.graph)
    if (!sts.length) {
      throw new Error('Statement to be removed is not on store: ' + st)
    }
    this.removeStatement(sts[0])
    return this
  }

  /**
   * Removes all statements in a doc, along with the related metadata including request/response/status
   * @param doc - The document / graph
   */
  removeDocument(doc: Quad_Graph): IndexedFormula {
    this.removeMetadata(doc)
    // remove document
    var sts: Quad[] = this.statementsMatching(undefined, undefined, undefined, doc).slice() // Take a copy as this is the actual index
    for (var i = 0; i < sts.length; i++) {
      this.removeStatement(sts[i])
    }
    this.removeMatches(doc as Quad_Subject, null, null)
    return this
  }

  removeMetadata(doc: Quad_Graph): IndexedFormula {
    const meta = this.sym('chrome://TheCurrentSession') // or this.rdfFactory.namedNode('chrome://TheCurrentSession')
    const linkNamespaceURI = 'http://www.w3.org/2007/ont/link#'
    // remove status/response/request metadata
    const requests = this.statementsMatching(undefined, this.sym(`${linkNamespaceURI}requestedURI`), this.rdfFactory.literal(doc.value), meta).map(st => st.subject)
    for (var r = 0; r < requests.length; r++) {
      const request = requests[r]
      if (request != undefined) {
        // removeMatches unresolved issue with collection https://github.com/linkeddata/rdflib.js/issues/631
        let sts: Quad[]
        // status collection
        const status = this.any(request, this.sym(`${linkNamespaceURI}status`), null, meta) as Quad_Subject
        if (status != undefined) {
          sts = this.statementsMatching(status, this.sym(`${linkNamespaceURI}status`), null, meta).slice()
          for (var i = 0; i < sts.length; i++) {
            this.removeStatement(sts[i])
          }
        }
        // response items list
        const response = this.any(request, this.sym(`${linkNamespaceURI}response`), null, meta) as Quad_Subject
        if (response != undefined) {
          sts = this.statementsMatching(response, null, null, meta).slice()
          for (var i = 0; i < sts.length; i++) {
            this.removeStatement(sts[i])
          }
        }
        // request triples
        sts = this.statementsMatching(request, null, null, meta).slice()
        for (var i = 0; i < sts.length; i++) {
          this.removeStatement(sts[i])
        }

      }
    }
    this.removeMatches(this.sym(doc.value), null, null, meta) // content-type
    return this
  }

  /**
   * Remove all statements matching args (within limit) *
   * @param subj The subject
   * @param pred The predicate
   * @param obj The object
   * @param why The graph that contains the statement
   * @param limit The number of statements to remove
   */
  removeMany(
    subj?: Quad_Subject | null,
    pred?: Quad_Predicate | null,
    obj?: Quad_Object | null,
    why?: Quad_Graph | null,
    limit?: number
  ): void {
    // log.debug("entering removeMany w/ subj,pred,obj,why,limit = " + subj +", "+ pred+", " + obj+", " + why+", " + limit)
    var sts = this.statementsMatching(subj, pred, obj, why, false)
    // This is a subtle bug that occurred in updateCenter.js too.
    // The fact is, this.statementsMatching returns this.whyIndex instead of a copy of it
    // but for perfromance consideration, it's better to just do that
    // so make a copy here.
    var statements: Quad[] = []
    for (var i = 0; i < sts.length; i++) statements.push(sts[i])
    if (limit) statements = statements.slice(0, limit)
    for (i = 0; i < statements.length; i++) this.remove(statements[i])
  }

  /**
   * Remove all matching statements
   * @param subject The subject
   * @param predicate The predicate
   * @param object The object
   * @param graph The graph that contains the statement
   */
  removeMatches(
    subject?: Quad_Subject | null,
    predicate?: Quad_Predicate | null,
    object?: Quad_Object | null,
    graph?: Quad_Graph | null
  ): IndexedFormula {
    this.removeMany(subject, predicate, object, graph)
    return this
  }

  /**
   * Remove a particular statement object from the store
   *
   * @param st - a statement which is already in the store and indexed.
   *        Make sure you only use this for these.
   *        Otherwise, you should use remove() above.
   */
  removeStatement(st: Quad): IndexedFormula {
    // log.debug("entering remove w/ st=" + st)
    var term = [ st.subject, st.predicate, st.object, st.graph ]
    for (var p = 0; p < 4; p++) {
      var c = this.canon(term[p])
      var h = this.id(c)
      if (!this.index[p][h]) {
        // log.warn ("Statement removal: no index '+p+': "+st)
      } else {
        this.rdfArrayRemove(this.index[p][h], st)
      }
    }
    this.rdfArrayRemove(this.statements, st)
    return this
  }

  /**
   * Removes statements
   * @param sts The statements to remove
   */
  removeStatements(sts: ReadonlyArray<Quad>): IndexedFormula {
    for (var i = 0; i < sts.length; i++) {
      this.remove(sts[i])
    }
    return this
  }

  /**
   * Replace big with small, obsoleted with obsoleting.
   */
  replaceWith (big: Quad_Subject, small: Quad_Subject): boolean {
    // log.debug("Replacing "+big+" with "+small) // this.id(@@
    var oldhash = this.id(big)
    var newhash = this.id(small)
    var moveIndex = function (ix) {
      var oldlist = ix[oldhash]
      if (!oldlist) {
        return // none to move
      }
      var newlist = ix[newhash]
      if (!newlist) {
        ix[newhash] = oldlist
      } else {
        ix[newhash] = oldlist.concat(newlist)
      }
      delete ix[oldhash]
    }
    // the canonical one carries all the indexes
    for (var i = 0; i < 4; i++) {
      moveIndex(this.index[i])
    }
    this.redirections[oldhash] = small
    if (big.value) {
      // @@JAMBO: must update redirections,aliases from sub-items, too.
      if (!this.aliases[newhash]) {
        this.aliases[newhash] = []
      }
      this.aliases[newhash].push(big) // Back link
      if (this.aliases[oldhash]) {
        for (i = 0; i < this.aliases[oldhash].length; i++) {
          this.redirections[this.id(this.aliases[oldhash][i])] = small
          this.aliases[newhash].push(this.aliases[oldhash][i])
        }
      }
      this.add(small, this.sym('http://www.w3.org/2007/ont/link#uri'), big)
      // If two things are equal, and one is requested, we should request the other.
      if (this.fetcher) {
        this.fetcher.nowKnownAs(big, small)
      }
    }
    moveIndex(this.classActions)
    moveIndex(this.propertyActions)
    // log.debug("Equate done. "+big+" to be known as "+small)
    return true // true means the statement does not need to be put in
  }

  /**
   * Return all equivalent URIs by which this is known
   * @param x A named node
   */
  allAliases(x: NamedNode): NamedNode[] {
    var a = this.aliases[this.id(this.canon(x))] || []
    a.push(this.canon(x))
    return a
  }

  /**
   * Compare by canonical URI as smushed
   * @param x A named node
   * @param y Another named node
   */
  sameThings(x: NamedNode, y: NamedNode): boolean {
    if (x.equals(y)) {
      return true
    }
    var x1 = this.canon(x)
    //    alert('x1='+x1)
    if (!x1) return false
    var y1 = this.canon(y)
    //    alert('y1='+y1); //@@
    if (!y1) return false
    return (x1.value === y1.value)
  }

  setPrefixForURI (prefix: string, nsuri: string): void {
    // TODO: This is a hack for our own issues, which ought to be fixed
    // post-release
    // See http://dig.csail.mit.edu/cgi-bin/roundup.cgi/$rdf/issue227
    if (prefix === 'tab' && this.namespaces['tab']) {
      return
    } // There are files around with long badly generated prefixes like this
    if (prefix.slice(0, 2) === 'ns' || prefix.slice(0, 7) === 'default') {
      return
    }

    // remove any prefix that currently targets nsuri
    for (let existingPrefix in this.namespaces) {
      if (this.namespaces[existingPrefix] == nsuri)
        delete this.namespaces[existingPrefix];
    }

    this.namespaces[prefix] = nsuri
  }

  /** Search the Store
   *
   * ALL CONVENIENCE LOOKUP FUNCTIONS RELY ON THIS!
   * @param subj - A node to search for as subject, or if null, a wildcard
   * @param pred - A node to search for as predicate, or if null, a wildcard
   * @param obj - A node to search for as object, or if null, a wildcard
   * @param why - A node to search for as graph, or if null, a wildcard
   * @param justOne - flag - stop when found one rather than get all of them?
   * @returns An array of nodes which match the wildcard position
   */
  statementsMatching (
    subj?: Quad_Subject | null,
    pred?: Quad_Predicate | null,
    obj?: Quad_Object | null,
    why?: Quad_Graph | null,
    justOne?: boolean
  ): Statement[] {
    // log.debug("Matching {"+subj+" "+pred+" "+obj+"}")
    var pat = [ subj, pred, obj, why ]
    var pattern: Term[] = []
    var hash: Indexable[] = []
    var wild: number[] = [] // wildcards
    var given: number[] = [] // Not wild
    var p: number
    var list
    for (p = 0; p < 4; p++) {
      pattern[p] = this.canon(Node.fromValue(pat[p]))
      if (!pattern[p]) {
        wild.push(p)
      } else {
        given.push(p)
        hash[p] = this.id(pattern[p])
      }
    }
    if (given.length === 0) {
      return this.statements
    }
    if (given.length === 1) { // Easy too, we have an index for that
      p = given[0]
      list = this.index[p][hash[p]]
      if (list && justOne) {
        if (list.length > 1) {
          list = list.slice(0, 1)
        }
      }
      list = list || []
      return list
    }
    // Now given.length is 2, 3 or 4.
    // We hope that the scale-free nature of the data will mean we tend to get
    // a short index in there somewhere!
    var best = 1e10 // really bad
    var iBest
    var i
    for (i = 0; i < given.length; i++) {
      p = given[i] // Which part we are dealing with
      list = this.index[p][hash[p]]
      if (!list) {
        return [] // No occurrences
      }
      if (list.length < best) {
        best = list.length
        iBest = i // (not p!)
      }
    }
    // Ok, we have picked the shortest index but now we have to filter it
    var pBest = given[iBest]
    var possibles: Statement[] = this.index[pBest][hash[pBest]]
    var check = given.slice(0, iBest).concat(given.slice(iBest + 1)) // remove iBest
    var results: Statement[] = []
    var parts = [ 'subject', 'predicate', 'object', 'why' ]
    for (var j = 0; j < possibles.length; j++) {
      var st: Statement | null = possibles[j]

      for (i = 0; i < check.length; i++) { // for each position to be checked
        p = check[i]
        if (!this.canon(st[parts[p]]).equals(pattern[p])) {
          st = null
          break
        }
      }
      if (st != null) {
        results.push(st)
        if (justOne) break
      }
    }
    return results
  }

  /**
   * A list of all the URIs by which this thing is known
   * @param term
   */
  uris(term: Quad_Subject): string[] {
    var cterm = this.canon(term)
    var terms = this.aliases[this.id(cterm)]
    if (!cterm.value) return []
    var res = [ cterm.value ]
    if (terms) {
      for (var i = 0; i < terms.length; i++) {
        res.push(terms[i].uri)
      }
    }
    return res
  }

  serialize (base, contentType, provenance, options?) {

    // override Formula.serialize to force the serializer namespace prefixes
    // to those of this IndexedFormula

    // if namespaces are explicitly passed in options, let them override the existing namespaces in this formula
    const namespaces = options?.namespaces ? {...this.namespaces, ...options.namespaces} : {...this.namespaces};

    options = {
      ...(options || {}),
      namespaces
    }
    return serialize(provenance, this, base, contentType, undefined, options);
  }
}
IndexedFormula.handleRDFType = handleRDFType
