// Dstl (c) Crown Copyright 2017
package uk.gov.dstl.baleen.uima.utils.select;

import java.util.*;

import org.apache.commons.lang3.Validate;

import com.google.common.collect.ImmutableList;

import uk.gov.dstl.baleen.uima.utils.AnnotationNode;

/**
 * A list of {@link AnnotationNode}s, with methods that act on every node in the list.
 *
 * <p>To get an {@code nodes} object, use the {@link AnnotationNode#select(String)} method.
 */
public class Nodes<T> extends ArrayList<Node<T>> {

  /** Generated serial version Uid */
  private static final long serialVersionUID = 727081737507005840L;

  /** Construct empty node list */
  public Nodes() {
    super();
  }

  /**
   * Construct empty node list
   *
   * @param initialCapacity of the list
   */
  public Nodes(int initialCapacity) {
    super(initialCapacity);
  }

  /**
   * Construct a node list containing the given nodes in the order returned by the collections
   * iterator.
   *
   * @param nodes the collection whose elements are to be placed into this list
   */
  public Nodes(Collection<? extends Node<T>> nodes) {
    super(nodes);
  }

  // attribute methods
  /**
   * Get an attribute value from the first matched node that has the attribute.
   *
   * @param attributeKey The attribute key.
   * @return The attribute value from the first matched node that has the attribute. If no nodes
   *     were matched (isEmpty() == true), or if the no nodes have the attribute, returns empty
   *     string.
   * @see #hasAttr(String)
   */
  public String attr(String attributeKey) {
    for (Node<T> node : this) {
      if (node.hasAttr(attributeKey)) {
        return node.attr(attributeKey);
      }
    }
    return "";
  }

  /**
   * Checks if any of the matched nodes have this attribute defined.
   *
   * @param attributeKey attribute key
   * @return true if any of the nodes have the attribute; false if none do.
   */
  public boolean hasAttr(String attributeKey) {
    for (Node<T> node : this) {
      if (node.hasAttr(attributeKey)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Get the attribute value for each of the matched nodes. If an node does not have this attribute,
   * no value is included in the result set for that node.
   *
   * @param attributeKey the attribute name to return values for.
   * @return a list of each node's attribute value for the attribute
   */
  public List<String> eachAttr(String attributeKey) {
    List<String> attrs = new ArrayList<>(size());
    for (Node<T> node : this) {
      if (node.hasAttr(attributeKey)) {
        attrs.add(node.attr(attributeKey));
      }
    }
    return attrs;
  }

  /**
   * Get the combined text of all the matched nodes.
   *
   * <p>Note that it is possible to get repeats if the matched nodes contain both parent nodes and
   * their own children, as the {@link AnnotationNode#text} method returns the combined text of a
   * parent and all its children.
   *
   * @return string of all text
   * @see AnnotationNode#text()
   * @see #eachText()
   */
  public String text() {
    StringBuilder sb = new StringBuilder();
    for (Node<T> node : this) {
      if (sb.length() != 0) {
        sb.append(" ");
      }
      sb.append(node.text());
    }
    return sb.toString();
  }

  /**
   * Test if any matched Node has any text content, that is not just whitespace.
   *
   * @return true if any node has non-blank text content.
   * @see AnnotationNode#hasText()
   */
  public boolean hasText() {
    for (Node<T> node : this) {
      if (node.hasText()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Get the text content of each of the matched nodes. If an node has no text, then it is not
   * included in the result.
   *
   * @return A list of each matched node's text content.
   * @see AnnotationNode#text()
   * @see AnnotationNode#hasText()
   * @see #text()
   */
  public List<String> eachText() {
    ArrayList<String> texts = new ArrayList<>(size());
    for (Node<T> el : this) {
      if (el.hasText()) {
        texts.add(el.text());
      }
    }
    return texts;
  }

  /**
   * Get the combined text of all matched nodes.
   *
   * @return string of all node's text.
   * @see #text()
   */
  @Override
  public String toString() {
    return text();
  }

  // filters

  /**
   * Find matching nodes within this node list.
   *
   * @param query A {@link Selector} query
   * @return the filtered list of nodes, or an empty list if none match.
   */
  public Nodes<T> select(String query) {
    return Selector.select(query, this);
  }

  /**
   * Remove nodes from this list that match the {@link Selector} query.
   *
   * <p>E.g. {@code <Section class=logo>One</Section> <Section>Two</Section>}<br>
   * <code>nodes divs = nodes.select("Section").not(".logo");</code><br>
   * Result: {@code [<Section>Two</Section>]}
   *
   * <p>
   *
   * @param query the selector query whose results should be removed from these nodes
   * @return a new nodes list that contains only the filtered results
   */
  public Nodes<T> not(String query) {
    Nodes<T> out = Selector.select(query, this);
    return Selector.filterOut(this, out);
  }

  /**
   * Get the <i>nth</i> matched node as an nodes object.
   *
   * <p>See also {@link #get(int)} to retrieve an Node.
   *
   * @param index the (zero-based) index of the node in the list to retain
   * @return nodes containing only the specified node, or, if that node did not exist, an empty
   *     list.
   */
  public Nodes<T> eq(int index) {
    return size() > index ? new Nodes<>(ImmutableList.of(get(index))) : new Nodes<>();
  }

  /**
   * Test if any of the matched nodes match the supplied query.
   *
   * @param query A selector
   * @return true if at least one node in the list matches the query.
   */
  public boolean is(String query) {
    Nodes<T> children = select(query);
    return !children.isEmpty();
  }

  /**
   * Get the immediate next node sibling of each node in this list.
   *
   * @return next node siblings.
   */
  public Nodes<T> next() {
    return siblings(null, true, false);
  }

  /**
   * Get the immediate next node sibling of each node in this list, filtered by the query.
   *
   * @param query query to match siblings against
   * @return next node siblings.
   */
  public Nodes<T> next(String query) {
    return siblings(query, true, false);
  }

  /**
   * Get all of the following node siblings of each node in this list.
   *
   * @return all following node siblings.
   */
  public Nodes<T> nextAll() {
    return siblings(null, true, true);
  }

  /**
   * Get all of the following node siblings of each node in this list, filtered by the query.
   *
   * @param query CSS query to match siblings against
   * @return all following node siblings.
   */
  public Nodes<T> nextAll(String query) {
    return siblings(query, true, true);
  }

  /**
   * Get the immediate previous node sibling of each node in this list.
   *
   * @return previous node siblings.
   */
  public Nodes<T> prev() {
    return siblings(null, false, false);
  }

  /**
   * Get the immediate previous node sibling of each node in this list, filtered by the query.
   *
   * @param query query to match siblings against
   * @return previous node siblings.
   */
  public Nodes<T> prev(String query) {
    return siblings(query, false, false);
  }

  /**
   * Get all of the previous node siblings of each node in this list.
   *
   * @return all previous node siblings.
   */
  public Nodes<T> prevAll() {
    return siblings(null, false, true);
  }

  /**
   * Get all of the previous node siblings of each node in this list, filtered by the query.
   *
   * @param query query to match siblings against
   * @return all previous node siblings.
   */
  public Nodes<T> prevAll(String query) {
    return siblings(query, false, true);
  }

  /**
   * Internal handler for sibling methods
   *
   * @param query query to match siblings against, or null to match all
   * @param next true for next, false for previous
   * @param all true to get all that match, false for the first match
   * @return siblings matching the criteria
   */
  private Nodes<T> siblings(String query, boolean next, boolean all) {
    Nodes<T> els = new Nodes<>();
    Evaluator<T> eval = query != null ? QueryParser.parse(query) : null;
    for (Node<T> e : this) {
      do {
        Node<T> sib = next ? e.nextSibling() : e.previousSibling();
        if (sib == null) {
          break;
        }
        if (eval == null || sib.is(eval)) {
          els.add(sib);
        }
        e = sib;
      } while (all);
    }
    return els;
  }

  /**
   * Get all of the parents and ancestor nodes of the matched nodes.
   *
   * @return all of the parents and ancestor nodes of the matched nodes
   */
  public Nodes<T> parents() {
    HashSet<Node<T>> combo = new LinkedHashSet<>();
    for (Node<T> e : this) {
      combo.addAll(e.getParents());
    }
    return new Nodes<>(combo);
  }

  /**
   * Get the first matched node.
   *
   * @return The first matched node, or <code>null</code> if contents is empty.
   */
  public Node<T> first() {
    return isEmpty() ? null : get(0);
  }

  /**
   * Get the last matched node.
   *
   * @return The last matched node, or <code>null</code> if contents is empty.
   */
  public Node<T> last() {
    return isEmpty() ? null : get(size() - 1);
  }

  /**
   * Perform a depth-first traversal on each of the selected nodes.
   *
   * @param nodeVisitor the visitor callbacks to perform on each node
   * @return this, for chaining
   */
  public Nodes<T> traverse(NodeVisitor<T> nodeVisitor) {
    Validate.notNull(nodeVisitor);
    NodeTraversor<T> traversor = new NodeTraversor<>(nodeVisitor);
    for (Node<T> el : this) {
      traversor.traverse(el);
    }
    return this;
  }
}
