/**
 * @license
 * Copyright 2017 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { SortedMap } from '../util/sorted_map';

import { documentMap } from './collections';
import { Document } from './document';
import { DocumentComparator } from './document_comparator';
import { DocumentKey } from './document_key';

/**
 * DocumentSet is an immutable (copy-on-write) collection that holds documents
 * in order specified by the provided comparator. We always add a document key
 * comparator on top of what is provided to guarantee document equality based on
 * the key.
 */

export class DocumentSet {
  /**
   * Returns an empty copy of the existing DocumentSet, using the same
   * comparator.
   */
  static emptySet(oldSet: DocumentSet): DocumentSet {
    return new DocumentSet(oldSet.comparator);
  }

  private comparator: DocumentComparator;
  private keyedMap: SortedMap<DocumentKey, Document>;
  private sortedSet: SortedMap<Document, null>;

  /** The default ordering is by key if the comparator is omitted */
  constructor(comp?: DocumentComparator) {
    // We are adding document key comparator to the end as it's the only
    // guaranteed unique property of a document.
    if (comp) {
      this.comparator = (d1: Document, d2: Document) =>
        comp(d1, d2) || DocumentKey.comparator(d1.key, d2.key);
    } else {
      this.comparator = (d1: Document, d2: Document) =>
        DocumentKey.comparator(d1.key, d2.key);
    }

    this.keyedMap = documentMap();
    this.sortedSet = new SortedMap<Document, null>(this.comparator);
  }

  has(key: DocumentKey): boolean {
    return this.keyedMap.get(key) != null;
  }

  get(key: DocumentKey): Document | null {
    return this.keyedMap.get(key);
  }

  first(): Document | null {
    return this.sortedSet.minKey();
  }

  last(): Document | null {
    return this.sortedSet.maxKey();
  }

  isEmpty(): boolean {
    return this.sortedSet.isEmpty();
  }

  /**
   * Returns the index of the provided key in the document set, or -1 if the
   * document key is not present in the set;
   */
  indexOf(key: DocumentKey): number {
    const doc = this.keyedMap.get(key);
    return doc ? this.sortedSet.indexOf(doc) : -1;
  }

  get size(): number {
    return this.sortedSet.size;
  }

  /** Iterates documents in order defined by "comparator" */
  forEach(cb: (doc: Document) => void): void {
    this.sortedSet.inorderTraversal((k, v) => {
      cb(k);
      return false;
    });
  }

  /** Inserts or updates a document with the same key */
  add(doc: Document): DocumentSet {
    // First remove the element if we have it.
    const set = this.delete(doc.key);
    return set.copy(
      set.keyedMap.insert(doc.key, doc),
      set.sortedSet.insert(doc, null)
    );
  }

  /** Deletes a document with a given key */
  delete(key: DocumentKey): DocumentSet {
    const doc = this.get(key);
    if (!doc) {
      return this;
    }

    return this.copy(this.keyedMap.remove(key), this.sortedSet.remove(doc));
  }

  isEqual(other: DocumentSet | null | undefined): boolean {
    if (!(other instanceof DocumentSet)) {
      return false;
    }
    if (this.size !== other.size) {
      return false;
    }

    const thisIt = this.sortedSet.getIterator();
    const otherIt = other.sortedSet.getIterator();
    while (thisIt.hasNext()) {
      const thisDoc = thisIt.getNext().key;
      const otherDoc = otherIt.getNext().key;
      if (!thisDoc.isEqual(otherDoc)) {
        return false;
      }
    }
    return true;
  }

  toString(): string {
    const docStrings: string[] = [];
    this.forEach(doc => {
      docStrings.push(doc.toString());
    });
    if (docStrings.length === 0) {
      return 'DocumentSet ()';
    } else {
      return 'DocumentSet (\n  ' + docStrings.join('  \n') + '\n)';
    }
  }

  private copy(
    keyedMap: SortedMap<DocumentKey, Document>,
    sortedSet: SortedMap<Document, null>
  ): DocumentSet {
    const newSet = new DocumentSet();
    newSet.comparator = this.comparator;
    newSet.keyedMap = keyedMap;
    newSet.sortedSet = sortedSet;
    return newSet;
  }
}
