/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xpath;

import com.caucho.loader.EnvironmentLocal;
import com.caucho.util.LruCache;
import com.caucho.xpath.pattern.AbstractPattern;
import com.caucho.xpath.pattern.FromContext;

import org.w3c.dom.Node;

import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Public facade for selecting nodes and creating match patterns.
 *
 * <p>Applications can select nodes directly from the XPath facade.  
 * For example,
 * <code><pre>
 * Node verse = XPath.find("chapter/verse", node);
 * </pre></code>
 *
 * <p>For greater efficiency, applications can also precompile the 
 * match patterns.
 * <code><pre>
 * Pattern pattern = XPath.parseSelect("chapter/verse");
 * Node verse = pattern.find(node);
 * </pre></code>
 *
 * <p>XPath can also return values based on XPath expressions, following
 * the XPath expression syntax.  Applications can use the expressions for
 * the equivalent of xsl:value-of
 * <code><pre>
 * Expr expr = XPath.parseExpr("chapter/verse/@id + 1");
 * double value = expr.evalNumber(node);
 * </pre></code>
 *
 * <p>To support the XPath pattern variables, XPath uses an environment
 * object.  Most applications will not need to use it. 
 */
public class XPath {
  private static final Logger log
    = Logger.getLogger(XPath.class.getName());

  private static EnvironmentLocal<LruCache<String,Pattern>> _matchCache
    = new EnvironmentLocal<LruCache<String,Pattern>>();
  
  private static EnvironmentLocal<LruCache<String,Pattern>> _selectCache
    = new EnvironmentLocal<LruCache<String,Pattern>>();
  
  private static EnvironmentLocal<LruCache<String,Expr>> _exprCache
    = new EnvironmentLocal<LruCache<String,Expr>>();

  private XPath()
  {
  }

  /**
   * Finds a node based on an XPath pattern.  The pattern is relative
   * to the node so <code>XPath.find("child", node)</code> will find children,
   * not grandchildren.
   *
   * @param query XPath select pattern.
   * @param node XML node to start searching from.
   * @return The first matching node in document order.
   */
  public static Node find(String query, Node node)
    throws XPathException
  {
    Pattern pattern = parseSelect(query);

    return (Node) pattern.find(node);
  }

  /**
   * Selects all node matching an XPath pattern
   *
   * @param query XPath select pattern.
   * @param node XML node to start searching from.
   * @return An iterator of nodes matching the pattern.
   */
  public static Iterator select(String query, Node node)
    throws XPathException
  {
    Pattern pattern = parseSelect(query);

    return pattern.select(node);
  }

  /**
   * Create a node selection pattern.  The pattern matches relative
   * to the current node.
   *
   * @param query XPath select pattern.
   * @return a pattern that can later select nodes.
   */
  public static Pattern parseSelect(String query)
    throws XPathParseException
  {
    LruCache<String,Pattern> cache = _selectCache.get();
    if (cache == null)
      cache = new LruCache<String,Pattern>(128);
    
    Pattern pattern = cache.get(query);

    if (pattern == null) {
      pattern = parseSelect(query, null);
      cache.put(query, pattern);
    }

    return pattern;
  }

  /**
   * Create a node selection pattern.  The pattern matches relative
   * to the current node.
   *
   * <p>XSLT uses this version of parseSelect for proper namespace
   * matching.
   *
   * @param query XPath select pattern.
   * @param namespace the appropriate namespace mappings
   *
   * @return a pattern that can later select nodes.
   */
  public static Pattern parseSelect(String query, NamespaceContext namespace)
    throws XPathParseException
  {
    XPathParser parser = new XPathParser(query, namespace);

    AbstractPattern pattern = parser.parseSelect();

    if (log.isLoggable(Level.FINER))
      log.finest("select: " + pattern);

    return new Pattern(pattern);
  }

  /**
   * Create a node match pattern.  Match patterns are intended to test
   * if a node matches the pattern.  They do not work well for finding or
   * selecting patterns.  Essentially, a match pattern of 'foo[@bar]' is
   * equivalent to a select pattern of '//foo[@bar]', but with less overhead.
   *
   * @param query XPath match pattern.
   * @return a pattern that can later be used for isMatch.
   */
  public static Pattern parseMatch(String query)
    throws XPathParseException
  {
    LruCache<String,Pattern> cache = _matchCache.get();
    if (cache == null)
      cache = new LruCache<String,Pattern>(128);
    
    Pattern pattern = cache.get(query);

    if (pattern == null) {
      pattern = parseMatch(query, null);
      cache.put(query, pattern);
    }

    return pattern;
  }

  /**
   * Create a node match pattern.  Match patterns are intended to test
   * if a node matches the pattern.  They do not work well for finding or
   * selecting patterns.  Essentially, a match pattern of 'foo[@bar]' is
   * equivalent to a select pattern of '//foo[@bar]', but with less overhead.
   *
   * @param query XPath match pattern.
   * @param namespace the appropriate namespace mappings.
   *
   * @return a pattern that can later be used for isMatch.
   */
  public static Pattern parseMatch(String query, NamespaceContext namespace)
    throws XPathParseException
  {
    XPathParser parser = new XPathParser(query, namespace);

    AbstractPattern pattern = parser.parseMatch();

    if (log.isLoggable(Level.FINER))
      log.finest("match: " + pattern);

    return new Pattern(pattern);
  }

  /**
   * Evaluates an XPath expression, returning a string.  evalString works
   * like the XSL <code>value-of</code> element.
   *
   * <p>For example, to get the value of an attribute use:
   *
   * <code><pre>
   * String value = XPath.evalString("@id", node);
   * </pre></code>
   *
   * @param query XPath expression
   * @param node the node context
   *
   * @return the string result of the expression.
   */
  public static String evalString(String query, Node node)
    throws XPathException
  {
    Expr expr = parseExpr(query);

    return expr.evalString(node);
  }

  /**
   * Evaluates an XPath expression, returning a double.
   *
   * @param query XPath expression
   * @param node the node context
   *
   * @return the number result of the expression.
   */
  public static double evalNumber(String query, Node node)
    throws XPathException
  {
    Expr expr = parseExpr(query);

    return expr.evalNumber(node);
  }

  /**
   * Evaluates an XPath expression, returning a boolean.
   *
   * @param query XPath expression
   * @param node the node context
   *
   * @return the boolean result of the expression.
   */
  public static boolean evalBoolean(String query, Node node)
    throws XPathException
  {
    Expr expr = parseExpr(query);

    return expr.evalBoolean(node);
  }

  /**
   * Evaluates an XPath expression, returning an object
   *
   * @param query XPath expression
   * @param node the node context
   *
   * @return the result of the expression.
   */
  public static Object evalObject(String query, Node node)
    throws XPathException
  {
    Expr expr = parseExpr(query);

    return expr.evalObject(node);
  }

  /**
   * Parses an XPath expression for later evaluation.
   *
   * @param query XPath expression
   * @return the result of the expression.
   */
  public static Expr parseExpr(String query)
    throws XPathParseException
  {
    LruCache<String,Expr> cache = _exprCache.get();
    if (cache == null) {
      cache = new LruCache<String,Expr>(128);
      _exprCache.set(cache);
    }
    
    Expr expr = cache.get(query);

    if (expr == null) {
      expr = parseExpr(query, null);
      cache.put(query, expr);
    }

    return expr;
  }

  /**
   * Parses an XPath expression for later evaluation.
   *
   * @param query XPath expression
   * @param namespace namespace context
   *
   * @return the compiled expression
   */
  public static Expr parseExpr(String query, NamespaceContext namespace)
    throws XPathParseException
  {
    XPathParser parser = new XPathParser(query, namespace);

    Expr expr = parser.parseExpr();

    if (log.isLoggable(Level.FINER))
      log.finest("expr: " + expr);

    return expr;
  }

  /**
   * Parses an XPath expression for later evaluation.
   *
   * @param query XPath expression
   * @param namespace namespace context
   * @param nodeList containing nodeList pattern
   *
   * @return the compiled expression
   */
  public static Expr parseExpr(String query, NamespaceContext namespace,
                               AbstractPattern nodeList)
    throws XPathParseException
  {
    XPathParser parser = new XPathParser(query, namespace);

    Expr expr = parser.parseExpr(new FromContext(), nodeList);

    if (expr != null)
      expr.setListContext(nodeList);

    if (log.isLoggable(Level.FINER))
      log.finest("expr: " + expr);

    return expr;
  }

  /**
   * Creates a new variable environment.
   */
  public static Env createEnv()
  {
    return Env.create();
  }

  /**
   * Creates a new variable environment based on an old environment.
   *
   * <p>This lets environments share globals even through function calls.
   */
  public static Env createEnv(Env global)
  {
    Env env = Env.create();

    env.init(global);

    return env;
  }

  /**
   * Creates a new variable environment based on an old environment.
   *
   * <p>This lets environments share globals even through function calls.
   */
  public static Env createCall(Env parent)
  {
    Env env = Env.create();

    env.initMacro(parent);

    return env;
  }

  /**
   * Free an environment.
   */
  public static void freeEnv(Env env)
  {
    // env.free();
  }
}
