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

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.Validate;

/** Evaluates that an node matches the selector. */
public abstract class Evaluator<T> {
  protected Evaluator() {}

  /**
   * Test if the node meets the evaluator's requirements.
   *
   * @param root Root of the matching subtree
   * @param node tested node
   * @return Returns <tt>true</tt> if the requirements are met or <tt>false</tt> otherwise
   */
  public abstract boolean matches(Node<T> root, Node<T> node);

  /** Evaluator for type name */
  public static final class TypeName<T> extends Evaluator<T> {
    private final String typeName;

    /**
     * Construct evaluator for given typeName
     *
     * @param typeName the typeName to look for
     */
    public TypeName(String typeName) {
      this.typeName = typeName;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.getTypeName().equalsIgnoreCase(typeName);
    }

    @Override
    public String toString() {
      return String.format("%s", typeName);
    }
  }

  /** Evaluator for node id. */
  public static final class Id<T> extends Evaluator<T> {
    private final String id;

    /**
     * Construct an evaluator for the given id.
     *
     * @param id the id to look for
     */
    public Id(String id) {
      this.id = id;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return id.equals(node.id());
    }

    @Override
    public String toString() {
      return String.format("#%s", id);
    }
  }

  /** Evaluator for node class */
  public static final class Class<T> extends Evaluator<T> {
    private final String className;

    /**
     * Construct an evaluator for the given className.
     *
     * @param className the className to look for
     */
    public Class(String className) {
      this.className = className;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.hasClass(className);
    }

    @Override
    public String toString() {
      return String.format(".%s", className);
    }
  }

  /** Evaluator for attribute name matching (case insensitive) */
  public static final class Attribute<T> extends Evaluator<T> {
    private final String key;

    /**
     * Construct an evaluator for the given attribute.
     *
     * @param key the attribute name to look for
     */
    public Attribute(String key) {
      this.key = key;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.hasAttr(key);
    }

    @Override
    public String toString() {
      return String.format("[%s]", key);
    }
  }

  /** Evaluator for attribute name prefix matching */
  public static final class AttributeStarting<T> extends Evaluator<T> {
    private final String keyPrefix;

    /**
     * Construct an evaluator for the given attribute prefix.
     *
     * @param keyPrefix the prefix of the attribute name to look for
     */
    public AttributeStarting(String keyPrefix) {
      Validate.notEmpty(keyPrefix);
      this.keyPrefix = keyPrefix.toLowerCase();
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {

      Map<String, String> attributes = node.attributes();
      for (String attribute : attributes.keySet()) {
        if (attribute.toLowerCase().startsWith(keyPrefix)) {
          return true;
        }
      }
      return false;
    }

    @Override
    public String toString() {
      return String.format("[^%s]", keyPrefix);
    }
  }

  /** Evaluator for attribute name/value matching */
  public static final class AttributeWithValue<T> extends AttributeKeyPair<T> {

    /**
     * Construct an evaluator for the given attribute value.
     *
     * @param key the attribute name to look for
     * @param value the attribute value to look for
     */
    public AttributeWithValue(String key, String value) {
      super(key, value);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.hasAttr(key) && value.equalsIgnoreCase(node.attr(key).trim());
    }

    @Override
    public String toString() {
      return String.format("[%s=%s]", key, value);
    }
  }

  /** Evaluator for attribute name != value matching */
  public static final class AttributeWithValueNot<T> extends AttributeKeyPair<T> {

    /**
     * Construct an evaluator for an attribute without the given value.
     *
     * @param key the attribute name to look for
     * @param value the attribute value to lnot match
     */
    public AttributeWithValueNot(String key, String value) {
      super(key, value);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return !value.equalsIgnoreCase(node.attr(key));
    }

    @Override
    public String toString() {
      return String.format("[%s!=%s]", key, value);
    }
  }

  /** Evaluator for attribute name/value matching (value prefix) */
  public static final class AttributeWithValueStarting<T> extends AttributeKeyPair<T> {

    /**
     * Construct a starts with evaluator for the given attribute value.
     *
     * @param key the attribute name to look for
     * @param value the attribute value should start with
     */
    public AttributeWithValueStarting(String key, String value) {
      super(key, value);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      // value is lower case already
      return node.hasAttr(key) && node.attr(key).toLowerCase().startsWith(value);
    }

    @Override
    public String toString() {
      return String.format("[%s^=%s]", key, value);
    }
  }

  /** Evaluator for attribute name/value matching (value ending) */
  public static final class AttributeWithValueEnding<T> extends AttributeKeyPair<T> {

    /**
     * Construct a ends with evaluator for the given attribute value.
     *
     * @param key the attribute name to look for
     * @param value the attribute value should end with
     */
    public AttributeWithValueEnding(String key, String value) {
      super(key, value);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      // value is lower case
      return node.hasAttr(key) && node.attr(key).toLowerCase().endsWith(value);
    }

    @Override
    public String toString() {
      return String.format("[%s$=%s]", key, value);
    }
  }

  /** Evaluator for attribute name/value matching (value containing) */
  public static final class AttributeWithValueContaining<T> extends AttributeKeyPair<T> {

    /**
     * Construct a contains with evaluator for the given attribute value.
     *
     * @param key the attribute name to look for
     * @param value the attribute value should contain
     */
    public AttributeWithValueContaining(String key, String value) {
      super(key, value);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      // value is lower case
      return node.hasAttr(key) && node.attr(key).toLowerCase().contains(value);
    }

    @Override
    public String toString() {
      return String.format("[%s*=%s]", key, value);
    }
  }

  /** Evaluator for attribute name/value matching (value regex matching) */
  public static final class AttributeWithValueMatching<T> extends Evaluator<T> {
    final String key;
    final Pattern pattern;

    /**
     * Construct an matches evaluator for the given attribute value.
     *
     * @param key the attribute name to look for
     * @param pattern the attribute value should match
     */
    public AttributeWithValueMatching(String key, Pattern pattern) {
      this.key = key.trim().toLowerCase();
      this.pattern = pattern;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.hasAttr(key) && pattern.matcher(node.attr(key)).find();
    }

    @Override
    public String toString() {
      return String.format("[%s~=%s]", key, pattern.toString());
    }
  }

  /** Abstract evaluator for attribute name/value matching */
  public abstract static class AttributeKeyPair<T> extends Evaluator<T> {
    final String key;
    final String value;

    /**
     * Construct the abstract attribute evaluator.
     *
     * @param key the attribute name to look for
     * @param value the attribute value should use in the evaluation
     */
    public AttributeKeyPair(String key, String value) {
      Validate.notEmpty(key);
      Validate.notEmpty(value);

      this.key = key.trim().toLowerCase();
      if (value.startsWith("\"") && value.endsWith("\"")
          || value.startsWith("'") && value.endsWith("'")) {
        value = value.substring(1, value.length() - 1);
      }
      this.value = value.trim().toLowerCase();
    }
  }

  /** Evaluator for any / all node matching */
  public static final class AllNodes<T> extends Evaluator<T> {

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return true;
    }

    @Override
    public String toString() {
      return "*";
    }
  }

  /** Evaluator for matching by sibling index number (e {@literal <} idx) */
  public static final class IndexLessThan<T> extends IndexEvaluator<T> {

    /**
     * Constructor for the less than index evaluator
     *
     * @param index the index to be less than
     */
    public IndexLessThan(int index) {
      super(index);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.getSiblingIndex() < index;
    }

    @Override
    public String toString() {
      return String.format(":lt(%d)", index);
    }
  }

  /** Evaluator for matching by sibling index number (e {@literal >} idx) */
  public static final class IndexGreaterThan<T> extends IndexEvaluator<T> {

    /**
     * Constructor for the greater than index evaluator
     *
     * @param index the index to be greater than
     */
    public IndexGreaterThan(int index) {
      super(index);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.getSiblingIndex() > index;
    }

    @Override
    public String toString() {
      return String.format(":gt(%d)", index);
    }
  }

  /** Evaluator for matching by sibling index number (e = idx) */
  public static final class IndexEquals<T> extends IndexEvaluator<T> {

    /**
     * Constructor for the equals index evaluator
     *
     * @param index the index to be equal to
     */
    public IndexEquals(int index) {
      super(index);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.getSiblingIndex() == index;
    }

    @Override
    public String toString() {
      return String.format(":eq(%d)", index);
    }
  }

  /** Evaluator for matching the last sibling (css :last-child) */
  public static final class IsLastChild<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      final Node<T> p = node.getParent();
      return p != null && node.getSiblingIndex() == p.getChildren().size() - 1;
    }

    @Override
    public String toString() {
      return ":last-child";
    }
  }

  /** Evaluator for matching the first sibling of type (:first-of-type) */
  public static final class IsFirstOfType<T> extends IsNthOfType<T> {

    /** Constructor fo first of type evaluator. */
    public IsFirstOfType() {
      super(0, 1);
    }

    @Override
    public String toString() {
      return ":first-of-type";
    }
  }

  /** Evaluator for matching the last sibling of type (:last-of-type) */
  public static final class IsLastOfType<T> extends IsNthLastOfType<T> {

    /** Constructor fo first of type evaluator. */
    public IsLastOfType() {
      super(0, 1);
    }

    @Override
    public String toString() {
      return ":last-of-type";
    }
  }

  /** Base evaluator for 'nth of' matches. Using (an + b) */
  public abstract static class NthEvaluator<T> extends Evaluator<T> {

    final int a;
    final int b;

    /**
     * Constructor for the base nth evaluator defining both a and b.
     *
     * @param a the a in (an+b)
     * @param b the b in (an+b)
     */
    public NthEvaluator(int a, int b) {
      this.a = a;
      this.b = b;
    }

    /**
     * Constructor for the base nth evaluator defining b and a=0.
     *
     * @param b the b in (an+b)
     */
    public NthEvaluator(int b) {
      this(0, b);
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      final Node<T> p = node.getParent();
      if (p == null) {
        return false;
      }

      final int pos = calculatePosition(root, node);
      if (a == 0) {
        return pos == b;
      }

      return (pos - b) * a >= 0 && (pos - b) % a == 0;
    }

    @Override
    public String toString() {
      if (a == 0) {
        return String.format(":%s(%d)", getPseudoClass(), b);
      }
      if (b == 0) {
        return String.format(":%s(%dn)", getPseudoClass(), a);
      }
      return String.format(":%s(%dn%+d)", getPseudoClass(), a, b);
    }

    protected abstract String getPseudoClass();

    protected abstract int calculatePosition(Node<T> root, Node<T> node);
  }

  /**
   * Evaluator for (:nth-child)
   *
   * @see IndexEquals
   */
  public static final class IsNthChild<T> extends NthEvaluator<T> {

    /**
     * Constructor for is nth child evaluator defining both a and b.
     *
     * @param a the a in (an+b)
     * @param b the b in (an+b)
     */
    public IsNthChild(int a, int b) {
      super(a, b);
    }

    @Override
    protected int calculatePosition(Node<T> root, Node<T> node) {
      return node.getSiblingIndex() + 1;
    }

    @Override
    protected String getPseudoClass() {
      return "nth-child";
    }
  }

  /**
   * Evaluator for pseudo class :nth-last-child
   *
   * @see IndexEquals
   */
  public static final class IsNthLastChild<T> extends NthEvaluator<T> {

    /**
     * Constructor for is nth last child evaluator defining both a and b.
     *
     * @param a the a in (an+b)
     * @param b the b in (an+b)
     */
    public IsNthLastChild(int a, int b) {
      super(a, b);
    }

    @Override
    protected int calculatePosition(Node<T> root, Node<T> node) {
      return node.getParent().getChildren().size() - node.getSiblingIndex();
    }

    @Override
    protected String getPseudoClass() {
      return "nth-last-child";
    }
  }

  /** Evaluator for pseudo class nth-of-type */
  public static class IsNthOfType<T> extends NthEvaluator<T> {

    /**
     * Constructor for is nth of type evaluator defining both a and b.
     *
     * @param a the a in (an+b)
     * @param b the b in (an+b)
     */
    public IsNthOfType(int a, int b) {
      super(a, b);
    }

    @Override
    protected int calculatePosition(Node<T> root, Node<T> node) {
      int pos = 0;

      Nodes<T> family = new Nodes<>(node.getParent().getChildren());
      for (Node<T> el : family) {
        if (el.getTypeName().equals(node.getTypeName())) {
          pos++;
        }
        if (el == node) {
          break;
        }
      }
      return pos;
    }

    @Override
    protected String getPseudoClass() {
      return "nth-of-type";
    }
  }

  /** Evaluator for matching nth last of type */
  public static class IsNthLastOfType<T> extends NthEvaluator<T> {

    /**
     * Constructor for is nth list child of type evaluator defining both a and b.
     *
     * @param a the a in (an+b)
     * @param b the b in (an+b)
     */
    public IsNthLastOfType(int a, int b) {
      super(a, b);
    }

    @Override
    protected int calculatePosition(Node<T> root, Node<T> node) {
      Nodes<?> family = new Nodes<>(node.getParent().getChildren(node.getType()));
      return family.size() - node.getTypeIndex();
    }

    @Override
    protected String getPseudoClass() {
      return "nth-last-of-type";
    }
  }

  /** Evaluator for matching the first sibling (css :first-child) */
  public static final class IsFirstChild<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      final Node<T> p = node.getParent();
      return p != null && node.getSiblingIndex() == 0;
    }

    @Override
    public String toString() {
      return ":first-child";
    }
  }

  /**
   * Evaluator to match if node is the root
   *
   * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root selector</a>
   */
  public static final class IsRoot<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node == root;
    }

    @Override
    public String toString() {
      return ":root";
    }
  }

  /** Evaluator to match if only child */
  public static final class IsOnlyChild<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      final Node<T> p = node.getParent();
      return p != null && p.getChildren().size() == 1;
    }

    @Override
    public String toString() {
      return ":only-child";
    }
  }

  /** Evaluator to match if only of type */
  public static final class IsOnlyOfType<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      final Node<T> p = node.getParent();
      if (p == null) {
        return false;
      }
      return p.getChildren(node.getType()).size() == 1;
    }

    @Override
    public String toString() {
      return ":only-of-type";
    }
  }

  /** Evaluator to match if the node is empty (of text and other nodes) */
  public static final class IsEmpty<T> extends Evaluator<T> {
    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      Nodes<T> family = node.getChildren();
      return family.isEmpty() && node.text().isEmpty();
    }

    @Override
    public String toString() {
      return ":empty";
    }
  }

  /** Abstract evaluator for sibling index matching */
  public abstract static class IndexEvaluator<T> extends Evaluator<T> {
    final int index;

    /**
     * Construcotr for base index evaluator.
     *
     * @param index the index
     */
    public IndexEvaluator(int index) {
      this.index = index;
    }
  }

  /** Evaluator for matching Node<T> (and its descendants) text */
  public static final class ContainsText<T> extends Evaluator<T> {
    private String searchText;

    /**
     * Constructor for the contains text evaluator.
     *
     * @param searchText the text to search for
     */
    public ContainsText(String searchText) {
      this.searchText = searchText.toLowerCase();
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.text().toLowerCase().contains(searchText);
    }

    @Override
    public String toString() {
      return String.format(":contains(%s)", searchText);
    }
  }

  /** Evaluator for matching Node's own text (this excludes the text contained in any children). */
  public static final class ContainsOwnText<T> extends Evaluator<T> {
    private final String searchText;

    /**
     * Constructor for the contains own text evaluator.
     *
     * @param searchText the text to search for
     */
    public ContainsOwnText(String searchText) {
      this.searchText = searchText.toLowerCase();
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      return node.ownText().toLowerCase().contains(searchText);
    }

    @Override
    public String toString() {
      return String.format(":containsOwn(%s)", searchText);
    }
  }

  /** Evaluator for matching Node<T> (and its descendants) text with regex */
  public static final class Matches<T> extends Evaluator<T> {
    private final Pattern pattern;

    /**
     * Constructor for the matches text evaluator.
     *
     * @param pattern the regular expression to match
     */
    public Matches(Pattern pattern) {
      this.pattern = pattern;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      Matcher m = pattern.matcher(node.text());
      return m.find();
    }

    @Override
    public String toString() {
      return String.format(":matches(%s)", pattern);
    }
  }

  /** Evaluator for matching Node's own text with regex (excluding text in children). */
  public static final class MatchesOwn<T> extends Evaluator<T> {
    private final Pattern pattern;

    /**
     * Constructor for the matches own text evaluator.
     *
     * @param pattern the regular expression to match
     */
    public MatchesOwn(Pattern pattern) {
      this.pattern = pattern;
    }

    @Override
    public boolean matches(Node<T> root, Node<T> node) {
      Matcher m = pattern.matcher(node.ownText());
      return m.find();
    }

    @Override
    public String toString() {
      return String.format(":matchesOwn(%s)", pattern);
    }
  }
}
