/*
 * 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 Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xsl;

import com.caucho.java.LineMap;
import com.caucho.util.CharBuffer;
import com.caucho.util.IntArray;
import com.caucho.vfs.Path;
import com.caucho.xml.CauchoNode;
import com.caucho.xml.QAbstractNode;
import com.caucho.xml.QElement;
import com.caucho.xml.XMLWriter;
import com.caucho.xml.XmlChar;
import com.caucho.xml.XmlUtil;
import com.caucho.xpath.Env;
import com.caucho.xpath.Expr;
import com.caucho.xpath.StylesheetEnv;
import com.caucho.xpath.XPath;
import com.caucho.xpath.XPathException;
import com.caucho.xpath.XPathFun;
import com.caucho.xpath.pattern.AbstractPattern;
import com.caucho.xpath.pattern.NodeIterator;
import com.caucho.xsl.fun.DocumentFun;
import com.caucho.xsl.fun.ExtensionElementFun;
import com.caucho.xsl.fun.ExtensionFunctionFun;
import com.caucho.xsl.fun.SystemPropertyFun;
import com.caucho.xsl.fun.UnparsedEntityFun;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Logger;

/**
 * Implementation base class for stylesheets.  It is made public only
 * because generated Java and JavaScript classes need to access these
 * routines.
 */
public class StylesheetImpl extends AbstractStylesheet  {
  private static final Logger log
    = Logger.getLogger(StylesheetImpl.class.getName());

  public char []text; // static buffer of the text nodes

  protected HashMap templates;
  HashMap<String,XPathFun> _funs = new HashMap<String,XPathFun>();

  private HashMap<String,String> _preserve;
  private HashMap<String,String> _strip;
  private HashMap<String,String> _preservePrefix;
  private HashMap<String,String> _stripPrefix;
  private HashMap<String,Object> _properties = new HashMap<String,Object>();

  boolean isCacheable = true;
  protected boolean _defaultDisableEscaping;
  Path cachePath;
  long lastModified;

  boolean _generateLocation;
  
  LineMap lineMap;

  protected void copy(AbstractStylesheet stylesheet)
  {
    super.copy(stylesheet);

    StylesheetImpl stylesheetImpl = (StylesheetImpl) stylesheet;
    
    stylesheetImpl.text = text;
    stylesheetImpl.templates = templates;
    stylesheetImpl._preserve = _preserve;
    stylesheetImpl._strip = _strip;
    stylesheetImpl._preservePrefix = _preservePrefix;
    stylesheetImpl._stripPrefix = _stripPrefix;
    stylesheetImpl.lineMap = lineMap;
    stylesheetImpl._properties = _properties;
    stylesheetImpl._defaultDisableEscaping = _defaultDisableEscaping;
  }

  public OutputFormat getOutputFormat()
  {
    return new OutputFormat();
  }

  public void setOutputFormat(OutputFormat output)
  {
  }

  protected void setSpaces(HashMap<String,String> preserve,
                           HashMap<String,String> preservePrefix,
                           HashMap<String,String> strip,
                           HashMap<String,String> stripPrefix)
  {
    _preserve = preserve;
    _strip = strip;
    _preservePrefix = preservePrefix;
    _stripPrefix = stripPrefix;
  }

  public void setProperty(String name, Object value)
  {
    _properties.put(name, value);
  }

  public void setGenerateLocation(boolean generateLocation)
  {
    _generateLocation = generateLocation;
  }

  public boolean getGenerateLocation()
  {
    return _generateLocation;
  }

  public Object getProperty(String name)
  {
    Object value = _properties.get(name);
    if (value != null)
      return value;

    return super.getProperty(name);
  }

  protected void addFunction(String name, XPathFun fun)
  {
    _funs.put(name, fun);
  }

  public void init(Path path)
    throws Exception
  {
    super.init(path);

    addFunction("system-property",  new SystemPropertyFun());
    addFunction("element-available", new ExtensionElementFun());
    addFunction("function-available", new ExtensionFunctionFun());
    addFunction("unparsed-entity-uri", new UnparsedEntityFun());
  }

  /**
   * Transforms the input node to the output writer
   *
   * @param xml the input node to be transformed
   * @param writer output writer receiving the output
   * @param transformer the transformer to be used
   */
  public void transform(Node xml,
                        XMLWriter writer,
                        TransformerImpl transformer)
    throws SAXException, IOException, TransformerException
  {
    if (xml == null)
      throw new NullPointerException("can't transform null node");
    
    XslWriter out = new XslWriter(null, this, transformer);
    out.init(writer);

    if (_funs == null)
      _funs = (HashMap) ((StylesheetImpl) _stylesheet)._funs.clone();
    else
      _funs.putAll((HashMap) ((StylesheetImpl) _stylesheet)._funs);

    addFunction("document", new DocumentFun(transformer));
    DocumentFun docFun = new DocumentFun(transformer);
    docFun.setHtml(true);
    addFunction("html_document", docFun);

    Env env = XPath.createEnv();
    env.setFunctions(_funs);
    StylesheetEnv ssEnv = new StylesheetEnv();
    ssEnv.setPath(getPath());
    env.setStylesheetEnv(ssEnv);

    out.disableEscaping(_defaultDisableEscaping);

    if (_strip != null && ! _strip.isEmpty()) {
      stripSpaces(xml);
    }
    
    try {
      _xsl_init(out, xml, env);

      applyNode(out, xml, env, 0, Integer.MAX_VALUE);
    } catch (TransformerException e) {
      throw e;
    } catch (IOException e) {
      throw e;
    } catch (SAXException e) {
      throw e;
    } catch (Exception e) {
      throw new XslException(e, lineMap);
    }

    out.close();

    XPath.freeEnv(env);

    // funs = null;
  }

  protected void _xsl_init(XslWriter out, Node context, Env env)
    throws Exception
  {
  }

  protected Document ownerDocument(Node node)
  {
    Document owner = node.getOwnerDocument();
    
    if (owner != null)
      return owner;
    else
      return (Document) node;
  }

  public void applyNode(XslWriter out, Node node, Env env)
    throws Exception
  {
    applyNode(out, node, env, Integer.MIN_VALUE, Integer.MAX_VALUE);
  }
  
  protected void applyNode(XslWriter out, Node node, Env env, int min, int max)
    throws Exception
  {
    if (node == null)
      return;

    switch (node.getNodeType()) {
    case Node.DOCUMENT_NODE:
    case Node.DOCUMENT_FRAGMENT_NODE:
      for (Node child = node.getFirstChild();
           child != null;
           child = child.getNextSibling()) {
        applyNode(out, child, env, 0, 2147483647);
      }
      break;
      
    case Node.ELEMENT_NODE:
      out.pushCopy(node);
      if (node instanceof QElement) {
        for (Node child = ((QElement) node).getFirstAttribute();
             child != null;
             child = child.getNextSibling()) {
          applyNode(out, child, env, 0, 2147483647);
        }
      } else {
        NamedNodeMap attributeMap = ((Element) node).getAttributes();
        int size = attributeMap.getLength();
        for (int i = 0; i < size; i++) {
          Node child = attributeMap.item(i);
          
          applyNode(out, child, env, 0, 2147483647);
        }
      }
      
      for (Node child = node.getFirstChild();
           child != null;
           child = child.getNextSibling()) {
        applyNode(out, child, env, 0, 2147483647);
      }
      out.popCopy(node);
      break;

    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
      String value = node.getNodeValue();
      out.print(value);
      return;
      
    case Node.ATTRIBUTE_NODE:
      out.pushCopy(node);
      out.popCopy(node);
      break;
      
    case Node.ENTITY_REFERENCE_NODE:
      out.pushCopy(node);
      out.popCopy(node);
      break;
    }
  }
  
  /**
   * Gets a template.
   *
   * Only those templates with importance between min and max are considered.
   * For apply-templates, min = 0, and max = Infinity, 
   *
   * @param min minimum allowed importance
   * @param max maximum allowed importance
   */
  protected Template getTemplate(HashMap templates,
                                 Node node, Env env, int min, int max)
    throws XPathException
  {
    Template template = null;

    Template []templateList = (Template []) templates.get(node.getNodeName());
    if (templateList == null)
      templateList = (Template []) templates.get("*");
    
    for (int i = 0; templateList != null && i < templateList.length; i++) {
      Template subtemplate = templateList[i];

      if (min <= subtemplate.maxImportance &&
          subtemplate.maxImportance <= max &&
          subtemplate.pattern.match(node, env)) {
        return subtemplate;
      }
    }

    return null;
  }

  /**
   * The default rule when no templates match.  By default, it
   * calls apply-template on element children and copies text.  All
   * other nodes are stripped.
   *
   * @param out the current writer.
   * @param node the current node.
   * @param env the xpath environment.
   */
  protected void applyNodeDefault(XslWriter out, Node node, Env env)
    throws Exception
  {
    switch (node.getNodeType()) {
    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
      if (_generateLocation && node instanceof QAbstractNode)
        out.setLocation(((QAbstractNode) node).getBaseURI(),
                        ((QAbstractNode) node).getFilename(),
                        ((QAbstractNode) node).getLine());
      String value = node.getNodeValue();
      out.print(value);
      return;
      
    case Node.ATTRIBUTE_NODE:
    case Node.ENTITY_REFERENCE_NODE:
      out.print(node.getNodeValue());
      break;

    case Node.ELEMENT_NODE:
    case Node.DOCUMENT_NODE:
      throw new RuntimeException();
    }
  }

  public void printValue(XslWriter out, Node node) throws IOException
  {
    if (node != null)
      out.print(getNodeValue(node));
  }

  public String getNodeValue(Node node)
  {
    CharBuffer cb = new CharBuffer();

    nodeValue(cb, node);

    return cb.toString();
  }

  private void nodeValue(CharBuffer cb, Node node)
  {
    if (node == null)
      return;

    switch (node.getNodeType()) {
    case Node.ELEMENT_NODE:
      for (Node child = node.getFirstChild();
           child != null;
           child = child.getNextSibling()) {
        switch (child.getNodeType()) {
        case Node.ELEMENT_NODE:
        case Node.TEXT_NODE:
        case Node.CDATA_SECTION_NODE:
        case Node.ENTITY_REFERENCE_NODE:
          nodeValue(cb, child);
          break;
        }
      }
      break;

    case Node.ENTITY_REFERENCE_NODE:
      cb.append('&');
      cb.append(node.getNodeName());
      cb.append(';');
      break;

    case Node.DOCUMENT_NODE:
      Document doc = (Document) node;
      nodeValue(cb, doc.getDocumentElement());
      break;

    case Node.TEXT_NODE:
    case Node.CDATA_SECTION_NODE:
      String value = node.getNodeValue();
      cb.append(value);
      break;

    default:
      cb.append(node.getNodeValue());
      break;
    }
  }

  protected ArrayList xslSort(Node node, Env env, AbstractPattern pattern,
                              Sort []sortList)
    throws Exception
  {
    ArrayList<Node> sortKeys = new ArrayList<Node>();

    Iterator<Node> sortIter;
    NodeIterator iter = pattern.select(node, env);

    while (iter.hasNext()) {
      Node child = iter.next();
      sortKeys.add(child);
    }

    int []map = new int[sortKeys.size()];
    for (int i = map.length - 1; i >= 0; i--)
      map[i] = i;

    int []workMap = new int[map.length];
    
    Object []values = new Object[map.length * sortList.length];
    
    int size = map.length;
    for (int i = 0; i < size; i++) {
      Node child = (Node) sortKeys.get(i);
      
      env.setPosition(i + 1);
      // XXX: set last() as well

      for (int j = 0; j < sortList.length; j++) {
        Sort sort = sortList[j];
        Object value = sort.sortValue(child, env);

        values[i * sortList.length + j] = value;
      }
    }
    
    boolean []ascendingList = new boolean[sortList.length];

    for (int i = 0; i < ascendingList.length; i++) {
      Expr isAscending = sortList[i].getAscending();
      if (isAscending == null || isAscending.evalBoolean(node, env))
        ascendingList[i] = true;
    }

    Comparator []comparatorList = new Comparator[sortList.length];

    for (int i = 0; i < comparatorList.length; i++) {
      Expr langExpr = sortList[i].getLang();
      String lang = null;

      if (langExpr != null) {
        lang = langExpr.evalString(node, env);
      }

      if (lang != null)
        comparatorList[i] = getComparator(lang);
    }

    int []caseOrderList = new int[sortList.length];

    for (int i = 0; i < caseOrderList.length; i++) {
      Expr caseOrder = sortList[i].getCaseOrder();
      if (caseOrder == null)
        caseOrderList[i] = Sort.NO_CASE_ORDER;
      else if (caseOrder.evalBoolean(node, env))
        caseOrderList[i] = Sort.UPPER_FIRST;
      else
        caseOrderList[i] = Sort.LOWER_FIRST;
    }

    sort(values, sortList, comparatorList, ascendingList, caseOrderList,
         0, map.length, map, workMap);

    ArrayList sortedKeys = new ArrayList();

    for (int i = 0; i < map.length; i++)
      sortedKeys.add(sortKeys.get(map[i]));

    return sortedKeys;
  }

  /**
   * Returns the comparator for the language.
   */
  private Comparator getComparator(String lang)
  {
    Locale locale = getLocale(lang);

    return java.text.Collator.getInstance(locale);
  }

  /**
   * Returns the locale for the language.
   */
  private Locale getLocale(String lang)
  {
    int p = lang.indexOf('-');
    Locale locale = null;

    if (p < 0) {
      locale = new Locale(lang, "");
    }
    else {
      String language = lang.substring(0, p);
          
      int q = lang.indexOf(p + 1, '-');

      if (q < 0) {
        String country = lang.substring(p + 1);

        locale = new Locale(language, country);
      }
      else {
        String country = lang.substring(p + 1, q);
        String variant = lang.substring(q);

        locale = new Locale(language, country, variant);
      }
    }

    return locale;
  }

  /**
   * Sorts a subsequence.
   *
   * @param head the start of the subsequence
   * @param tail the tail of the subsequence
   */
  private void sort(Object []values, Sort []sortList,
                    Comparator []comparatorList,
                    boolean []ascendingList,
                    int []caseOrder,
                    int head, int tail,
                    int map[], int []workMap)
  {
    int length = tail - head;
    if (length <= 1)
      return;

    // shortcut when only have two items
    if (length == 2) {
      int a = map[head];
      int b = map[head + 1];

      if (lessThan(values, sortList, comparatorList,
                   ascendingList, caseOrder, b, a)) {
        map[head] = b;
        map[head + 1] = a;
      }
      return;
    }
    // shortcut when only have three items
    else if (length == 3) {
      int a = map[head];
      int b = map[head + 1];
      int c = map[head + 2];
      
      if (lessThan(values, sortList, comparatorList,
                   ascendingList, caseOrder, b, a)) {
        map[head] = b;
        map[head + 1] = a;
        a = map[head];
        b = map[head + 1];
      }

      if (! lessThan(values, sortList, comparatorList,
                     ascendingList, caseOrder, c, b)) {
      }
      else if (lessThan(values, sortList, comparatorList,
                        ascendingList, caseOrder, c, a)) {
        map[head] = c;
        map[head + 1] = a;
        map[head + 2] = b;
      }
      else {
        map[head + 1] = c;
        map[head + 2] = b;
      }

      return;
    }

    int pivotIndex = (head + tail) / 2;
    int pivot = map[pivotIndex];
    int top = tail;

    // values greater than the pivot value are put in the work map
    for (int i = tail - 1; i >= head; i--) {
      if (lessThan(values, sortList, comparatorList,
                   ascendingList, caseOrder, pivot, map[i])) {
        workMap[--top] = map[i];
        map[i] = -1;
      }
    }

    // if the pivot is the max, need to shift equals
    if (top == tail) {
      // values greater than the pivot value are put in the work map
      for (int i = tail - 1; i >= head; i--) {
        if (! lessThan(values, sortList, comparatorList,
                       ascendingList, caseOrder, map[i], pivot)) {
          workMap[--top] = map[i];
          map[i] = -1;
        }
      }

      // If all entries are equal to the pivot, we're done
      if (top == head) {
        for (int i = head; i < tail; i++)
          map[i] = workMap[i];
        return;
      }
    }
    
    // shift down the values less than the pivot
    int center = head;
    for (int i = head; i < tail; i++) {
      if (map[i] >= 0)
        map[center++] = map[i];
    }

    for (int i = center; i < tail; i++)
      map[i] = workMap[i];

    sort(values, sortList, comparatorList, ascendingList, caseOrder,
         head, center, map, workMap);
    sort(values, sortList, comparatorList, ascendingList, caseOrder,
         center, tail, map, workMap);
  }

  /**
   * Swaps two items in the map.
   */
  private void swap(int []map, int a, int b)
  {
    int ka = map[a];
    int kb = map[b];
    
    map[b] = ka;
    map[a] = kb;
  }

  /**
   * Returns true if the first value is strictly less than the second.
   */
  private boolean lessThan(Object []values,
                           Sort []sortList,
                           Comparator []comparatorList,
                           boolean []ascendingList,
                           int []caseOrder,
                           int ai, int bi)
  {
    int len = sortList.length;

    for (int i = 0; i < len; i++) {
      Object a = values[len * ai + i];
      Object b = values[len * bi + i];

      int cmp = sortList[i].cmp(a, b, comparatorList[i],
                                ascendingList[i], caseOrder[i]);
      if (cmp < 0)
        return true;
      else if (cmp > 0)
        return false;
    }

    return false;
  }

  public void singleNumber(XslWriter out, Node node, Env env,
                           AbstractPattern countPattern,
                           AbstractPattern fromPattern,
                           XslNumberFormat format)
    throws Exception
  {
    if (countPattern == null)
      countPattern = XPath.parseMatch(node.getNodeName()).getPattern();

    IntArray numbers = new IntArray();
    for (; node != null; node = node.getParentNode()) {
      if (countPattern.match(node, env)) {
        numbers.add(countPreviousSiblings(node, env, countPattern));
        break;
      }
      if (fromPattern != null && fromPattern.match(node, env))
        break;
    }
    if (fromPattern != null && ! findFromAncestor(node, env, fromPattern))  
      numbers.clear();

    format.format(out, numbers);
  }

  public void multiNumber(XslWriter out, Node node, Env env,
                          AbstractPattern countPattern,
                          AbstractPattern fromPattern,
                          XslNumberFormat format)
    throws Exception
  {
    if (countPattern == null)
      countPattern = XPath.parseMatch(node.getNodeName()).getPattern();

    IntArray numbers = new IntArray();
    for (; node != null; node = node.getParentNode()) {
      if (countPattern.match(node, env))
        numbers.add(countPreviousSiblings(node, env, countPattern));

      if (fromPattern != null && fromPattern.match(node, env))
        break;
    }
    if (fromPattern != null && ! findFromAncestor(node, env, fromPattern))  
      numbers.clear();

    format.format(out, numbers);
  }

  public void anyNumber(XslWriter out, Node node, Env env,
                        AbstractPattern countPattern,
                        AbstractPattern fromPattern,
                        XslNumberFormat format)
    throws Exception
  {
    if (countPattern == null)
      countPattern = XPath.parseMatch(node.getNodeName()).getPattern();

    IntArray numbers = new IntArray();
    int count = 0;
    for (; node != null; node = XmlUtil.getPrevious(node)) {
      if (countPattern.match(node, env))
        count++;

      if (fromPattern != null && fromPattern.match(node, env))
        break;
    }
    numbers.add(count);
    if (fromPattern != null && ! findFromAncestor(node, env, fromPattern))
      numbers.clear();

    format.format(out, numbers);
  }

  public void exprNumber(XslWriter out, Node node, Env env, Expr expr,
                         XslNumberFormat format)
    throws Exception
  {
    IntArray numbers = new IntArray();
    numbers.add((int) expr.evalNumber(node, env));

    format.format(out, numbers);
  }

  private int countPreviousSiblings(Node node, Env env, String name)
  {
    int count = 1;
    for (node = node.getPreviousSibling();
         node != null;
         node = node.getPreviousSibling()) {
      if (node.getNodeType() == node.ELEMENT_NODE &&
          node.getNodeName().equals(name))
        count++;
    }

    return count;
  }

  private int countPreviousSiblings(Node node, Env env, AbstractPattern pattern)
    throws XPathException
  {
    int count = 1;
    for (node = node.getPreviousSibling();
         node != null;
         node = node.getPreviousSibling()) {
      if (pattern.match(node, env))
        count++;
    }

    return count;
  }

  private boolean findFromAncestor(Node node, Env env, AbstractPattern pattern)
    throws XPathException
  {
    for (; node != null; node = node.getParentNode())
      if (pattern.match(node, env))
        return true;

    return false;
  }

  /**
   * Strips the spaces from a tree.
   */
  void stripSpaces(Node node)
  {
    Node child = node.getFirstChild();

    while (child != null) {
      Node next = child.getNextSibling();
      
      if (child instanceof Element) {
        stripSpaces(child);
      }
      else if (child instanceof Text) {
        String data = ((Text) child).getData();

        boolean hasContent = false;
        for (int i = data.length() - 1; i >= 0; i--) {
          char ch = data.charAt(i);

          if (! XmlChar.isWhitespace(ch)) {
            hasContent = true;
            break;
          }
        }

        if (! hasContent && isStripSpaces(node)) {
          node.removeChild(child);
        }
      }

      child = next;
    }
  }
  /**
   * Returns true if the node is a pure whitespace text node.
   */
  boolean isStripSpaces(Node node)
  {
    if (_strip == null)
      return false;
    
    for (Node ptr = node; ptr != null; ptr = ptr.getParentNode()) {
      if (ptr instanceof Element) {
        Element elt = (Element) ptr;
        String space = elt.getAttribute("xml:space");
        if (space != null && space.equals("preserve"))
          return false;
        else if (space != null)
          break;
      }
    }
    String name = node.getNodeName();
    if (_preserve.get(node.getNodeName()) != null)
      return false;
    else if (_strip.get(node.getNodeName()) != null)
      return true;

    CauchoNode cnode = (CauchoNode) node;
    String nsStar = cnode.getPrefix();
    if (_preservePrefix.get(nsStar) != null)
      return false;
    else if (_stripPrefix.get(nsStar) != null)
      return true;

    return _strip.get("*") != null;
  }

  /**
   * Merges two template arrays into the final one.
   */
  protected static Template []mergeTemplates(Template []star,
                                             Template []templates)
  {
    Template []merged = new Template[star.length + templates.length];

    int i = 0;
    int j = 0;
    int k = 0;

    while (i < star.length && j < templates.length) {
      if (star[i].compareTo(templates[j]) > 0)
        merged[k++] = star[i++];
      else
        merged[k++] = templates[j++];
    }

    for (; i < star.length; i++)
      merged[k++] = star[i];

    for (; j < templates.length; j++)
      merged[k++] = templates[j];

    return merged;
  }
}
