// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.

package com.google.collide.client.code.autocomplete;

import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;

/**
 * Trie-based implementation of the prefix index.
 *
 * @param <T> value object type
 */
public class AbstractTrie<T> implements PrefixIndex<T> {

  // TODO: This member should be static and unmodifiable.
  private final JsonArray<T> emptyList = JsonCollections.createArray();

  private TrieNode<T> root;

  public AbstractTrie() {
    clear();
  }

  public TrieNode<T> getRoot() {
    return root;
  }

  public void clear() {
    this.root = TrieNode.<T>makeNode("");
  }

  /**
   * @return {@code true} is this trie is empty
   */
  public boolean isEmpty() {
    return this.root.getChildren().isEmpty();
  }

  public TrieNode<T> put(String key, T value) {
    return insertIntoTrie(key, root, value);
  }

  @Override
  public JsonArray<T> search(String prefix) {
    TrieNode<T> searchRoot = findNode(prefix, root);
    return (searchRoot == null) ? emptyList : collectSubtree(searchRoot);
  }

  /**
   * Returns all leaf nodes from a subtree rooted by {@code searchRoot} node
   * and restricted with a {@code stopFunction}
   *
   * @param searchRoot node to start from
   * @return all leaf nodes in the matching subtree
   */
  public static <T> JsonArray<T> collectSubtree(TrieNode<T> searchRoot) {
    JsonArray<TrieNode<T>> leaves = JsonCollections.createArray();
    getAllLeavesInSubtree(searchRoot, leaves);
    JsonArray<T> result = JsonCollections.createArray();
    for (int i = 0; i < leaves.size(); i++) {
      result.add(leaves.get(i).getValue());
    }
    return result;
  }

  /**
   * Traverses the subtree rooted at {@code root} and collects all nodes in the
   * subtree that are leaves (i.e., valid elements, not only prefixes).
   *
   * @param root a node in the trie from which to start collecting
   * @param leaves output array
   */
  private static <T> void getAllLeavesInSubtree(TrieNode<T> root, JsonArray<TrieNode<T>> leaves) {
    if (root.getIsLeaf()) {
      leaves.add(root);
    }

    for (int i = 0, size = root.getChildren().size(); i < size; i++) {
      TrieNode<T> child = root.getChildren().get(i);
      getAllLeavesInSubtree(child, leaves);
    }
  }

  private TrieNode<T> insertIntoTrie(String prefix, TrieNode<T> node, T value) {
    String nodePrefix = node.getPrefix();
    if (nodePrefix.equals(prefix)) {
      node.setValue(value);
      return node;
    } else {
      TrieNode<T> branch = node.findInsertionBranch(prefix);
      if (branch != null) {
        return insertIntoTrie(prefix, branch, value);
      } else {
        // create new trie nodes
        JsonArray<TrieNode<T>> suffixChain =
            makeSuffixChain(
            node, prefix.substring(nodePrefix.length()), value);
        return suffixChain.peek();
      }
    }
  }

  /**
   * Inserts a chain of children into the given node.
   *
   * @param root node to insert into
   * @param suffix suffix of the last node in the chain
   * @param value value of the last node in the chain
   * @return the inserted chain in direct order (from the root to the leaf)
   */
  JsonArray<TrieNode<T>> makeSuffixChain(TrieNode<T> root, String suffix, T value) {
    JsonArray<TrieNode<T>> result = JsonCollections.createArray();
    String rootPrefix = root.getPrefix();
    for (int i = 1, suffixSize = suffix.length(); i <= suffixSize; i++) {
      String newPrefix = rootPrefix + suffix.substring(0, i);
      TrieNode<T> newNode = TrieNode.makeNode(newPrefix);
      result.add(newNode);
      root.addChild(newNode);
      root = newNode;
    }
    root.setValue(value);
    return result;
  }

  /**
   * Searches the subtree rooted at {@code searchRoot} for a node
   * corresponding to the prefix.
   *
   * There can only ever be one such node, or zero. If no node is found, returns
   * null.
   *
   * Note that the {@code prefix} is relative to the whole trie root, not
   * to the {@code searchRoot}.
   *
   * @param prefix the prefix to be found
   * @param searchRoot the root of the subtree that is searched
   * @return the node in the tree corresponding to prefix, or null if no such
   *         node exists
   */
  public static <T> TrieNode<T> findNode(String prefix, TrieNode<T> searchRoot) {
    Preconditions.checkNotNull(prefix);
    if (prefix.equals(searchRoot.getPrefix())) {
      return searchRoot;
    }
    TrieNode<T> closestAncestor = findClosestAncestor(prefix, searchRoot);
    return (closestAncestor.getPrefix().equals(prefix)) ? closestAncestor : null;
  }

  /**
   * Finds the closest ancestor of a search key. Formally it returns
   * a node x, such that: {@code prefix.startsWith(x.prefix)}
   * and there is no other node y, such that
   * {@code (prefix.startsWith(y.prefix) and y.prefix.length > x.prefix)}
   *
   * @param key search key
   * @param searchRoot node to start from
   * @return closest ancestor
   */
  static <T> TrieNode<T> findClosestAncestor(String key, TrieNode<T> searchRoot) {
    Preconditions.checkNotNull(key);
    Preconditions.checkArgument(key.startsWith(searchRoot.getPrefix()), "key=%s root prefix=%s",
        key, searchRoot.getPrefix());

    TrieNode<T> result = searchRoot;
    for (TrieNode<T> child = searchRoot; child != null; child = child.findInsertionBranch(key)) {
      result = child;
    }
    return result;
  }
}
