/*
 * 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.L10N;
import com.caucho.vfs.Encoding;
import com.caucho.vfs.IOExceptionWrapper;
import com.caucho.vfs.Path;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;
import com.caucho.xml.*;
import com.caucho.xpath.XPathFun;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Properties;

public class TransformerImpl extends javax.xml.transform.Transformer {
  protected static L10N L = new L10N(TransformerImpl.class);
  
  public final static String LINE_MAP = "caucho.line-map";
  public final static String CACHE_DEPENDS = "caucho.cache.depends";
  public final static String GENERATE_LOCATION = "caucho.generate.location";

  protected StylesheetImpl _stylesheet;
  protected HashMap<String,Object> _properties = new HashMap<String,Object>();
  protected HashMap<String,Object> _parameters;
  
  private URIResolver _uriResolver;
  private ErrorListener _errorListener;

  private Properties _output;
  
  protected LineMap _lineMap;

  protected ArrayList<Path> _cacheDepends = new ArrayList<Path>();

  protected TransformerImpl(StylesheetImpl stylesheet)
  {
    _stylesheet = stylesheet;
    _uriResolver = stylesheet.getURIResolver();
  }

  /**
   * Returns the URI to filename resolver.
   */
  public URIResolver getURIResolver()
  {
    return _uriResolver;
  }

  /**
   * Sets the URI to filename resolver.
   */
  public void setURIResolver(URIResolver uriResolver)
  {
    _uriResolver = uriResolver;
  }

  /**
   * Returns the error listener.
   */
  public ErrorListener getErrorListener()
  {
    return _errorListener;
  }

  /**
   * Sets the error listener.
   */
  public void setErrorListener(ErrorListener errorListener)
  {
    _errorListener = errorListener;
  }
  
  public boolean getFeature(String name)
  {
    if (name.equals(DOMResult.FEATURE))
      return true;
    else if (name.equals(DOMSource.FEATURE))
      return true;
    else if (name.equals(StreamSource.FEATURE))
      return true;
    else if (name.equals(StreamResult.FEATURE))
      return true;
    else if (name.equals(SAXSource.FEATURE))
      return true;
    else if (name.equals(SAXResult.FEATURE))
      return true;
    else
      return false;
  }

  public void setFeature(String name, boolean enable)
  {
    if (name.equals(GENERATE_LOCATION))
      _stylesheet.setGenerateLocation(enable);
  }

  public StylesheetImpl getStylesheet()
  {
    return _stylesheet;
  }

  public Object getProperty(String name)
  {
    Object property = _properties.get(name);

    if (property != null)
      return property;
    
    if (name.equals(CACHE_DEPENDS))
      return _cacheDepends;
    else if (name.equals(LINE_MAP))
      return _lineMap;
    else
      return _stylesheet.getProperty(name);
  }

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

  /**
   * Sets a parameter that XPath expressions in the stylesheet can
   * use as $name.
   *
   * @param name the name of the XPath variable.
   * @param value the value for the variable.
   */
  public void setParameter(String name, Object value)
  {
    if (_parameters == null)
      _parameters = new HashMap<String,Object>();
    
    _parameters.put(name, value);
  }

  /**
   * Returns a copy of the xsl:output properties.
   *
   * @return a copy of the properties.
   */
  public Properties getOutputProperties()
  {
    if (_output == null)
      _output = (Properties) _stylesheet.getOutputProperties().clone();
    
    return (Properties) _output.clone();
  }

  /**
   * Sets the output properties.
   *
   * @param properties the new output properties.
   */
  public void setOutputProperties(Properties properties)
  {
    _output = properties;
  }

  /**
   * Sets a single xsl:output property.
   *
   * @param name the name of the property.
   * @param value the value of the property.
   */
  public void setOutputProperty(String name, String value)
  {
    if (_output == null)
      _output = (Properties) _stylesheet.getOutputProperties().clone();

    _output.put(name, value);
  }

  /**
   * Returns the value of a single named xsl:output property.
   *
   * @param name the name of the property.
   */
  public String getOutputProperty(String name)
  {
    if (_output == null)
      _output = (Properties) _stylesheet.getOutputProperties().clone();

    return (String) _output.get(name);
  }

  /**
   * Returns the named stylesheet parameter.
   *
   * @param name the name of the parameter.
   *
   * @ return the value of the named parameter.
   */
  public Object getParameter(String name)
  {
    if (_parameters == null)
      return null;
    else
      return _parameters.get(name);
  }

  /**
   * Clears all the external stylesheet parameters.
   */
  public void clearParameters()
  {
    if (_parameters != null)
      _parameters.clear();
    
    if (_cacheDepends != null)
      _cacheDepends.clear();
  }

  /**
   * Adds a new custom function.
   *
   * @param name the name of the function.
   * @param fun the new function.
   */
  public void addFunction(String name, XPathFun fun)
  {
    _stylesheet.addFunction(name, fun);
  }

  /**
   * Transforms the source into the result.
   *
   * @param source descriptor specifying the input source.
   * @param result descriptor specifying the output result.
   */
  public void transform(Source source, Result result)
    throws TransformerException
  {
    try {
      Node node = parseDocument(source);
      
      if (result instanceof StreamResult) {
        StreamResult stream = (StreamResult) result;

        if (stream.getOutputStream() != null)
          transform(node, stream.getOutputStream(), null, result.getSystemId());
        else if (stream.getWriter() != null) {
          Writer writer = stream.getWriter();
          WriteStream os = Vfs.openWrite(writer);

          if (writer instanceof OutputStreamWriter) {
            String javaEncoding = ((OutputStreamWriter) writer).getEncoding();
            String mimeEncoding = Encoding.getMimeName(javaEncoding);
            transform(node, os, mimeEncoding, result.getSystemId());
          }
          else
            transform(node, os, null, result.getSystemId());

          os.flush();
          os.free();
        }
        else {
          WriteStream os = Vfs.lookup(result.getSystemId()).openWrite();
            
          try {
            transform(node, os, null, result.getSystemId());
          } finally {
            os.close();
          }
        }
      }
      else if (result instanceof DOMResult) {
        DOMResult domResult = (DOMResult) result;

        Node resultNode = domResult.getNode();

        domResult.setNode(transform(node, resultNode));
      }
      else if (result instanceof SAXResult) {
        SAXResult sax = (SAXResult) result;

        transform(node, sax.getHandler(), sax.getLexicalHandler());
      }
      else
        throw new TransformerException(String.valueOf(result));
    } catch (TransformerException e) {
      throw e;
    } catch (Exception e) {
      throw new TransformerExceptionWrapper(e);
    }
  }

  public void transform(Node node, OutputStream os)
    throws TransformerException
  {
    if (os instanceof WriteStream) {
      String encoding = ((WriteStream) os).getEncoding();
      if (encoding == null)
        encoding = "ISO-8859-1";

      transform(node, os, encoding, null);
    }
    else
      transform(node, os, null, null);
  }
  /**
   * Transforms from a DOM node to an output stream.
   *
   * @param node the source node
   * @param os the destination stream
   */
  public void transform(Node node, OutputStream os,
                        String encoding, String systemId)
    throws TransformerException
  {
    if (node == null)
      throw new IllegalArgumentException("can't transform null node");

    try {
      _lineMap = null;
      Properties output = getOutputProperties();

      WriteStream ws;

      if (os instanceof WriteStream)
        ws = (WriteStream) os;
      else {
        ws = Vfs.openWrite(os);

        if (systemId != null)
          ws.setPath(Vfs.lookup(systemId));
        else if (node instanceof QNode) {
          String baseURI = ((QNode) node).getBaseURI();

          if (baseURI != null)
            ws.setPath(Vfs.lookup(baseURI));
        }
      }

      XmlPrinter out = new XmlPrinter(ws);

      String method = (String) output.get(OutputKeys.METHOD);
      out.setMethod(method);
      if (encoding == null)
        encoding = (String) output.get(OutputKeys.ENCODING);
      if (encoding == null && ! (os instanceof WriteStream) &&
          ! "html".equals(method))
        encoding = "UTF-8";
      if (encoding != null)
        out.setEncoding(encoding);
      out.setMimeType((String) output.get(OutputKeys.MEDIA_TYPE));
      String omit = (String) output.get(OutputKeys.OMIT_XML_DECLARATION);

      if (omit == null || omit.equals("false") || omit.equals("no"))
        out.setPrintDeclaration(true);
      
      out.setStandalone((String) output.get(OutputKeys.STANDALONE));
      out.setSystemId((String) output.get(OutputKeys.DOCTYPE_SYSTEM));
      out.setPublicId((String) output.get(OutputKeys.DOCTYPE_PUBLIC));
    
      String indent = (String) output.get(OutputKeys.INDENT);
      if (indent != null)
        out.setPretty(indent.equals("true") || indent.equals("yes"));
      
      String jsp = (String) output.get("caucho.jsp");
      if (jsp != null)
        out.setJSP(jsp.equals("true") || jsp.equals("yes"));
      
      out.setVersion((String) output.get(OutputKeys.VERSION));

      String includeContentType = (String) output.get("include-content-type");
      if (includeContentType != null)
        out.setIncludeContentType(includeContentType.equals("true") ||
                                  includeContentType.equals("yes"));
      
      if (! _stylesheet.getGenerateLocation()) {
      }
      else if (node instanceof CauchoNode) {
        String filename = ((CauchoNode) node).getFilename();
        if (filename != null)
          out.setLineMap(filename);
        else
          out.setLineMap("anonymous.xsl");
      }
      else
        out.setLineMap("anonymous.xsl");

      //out.beginDocument();
      _stylesheet.transform(node, out, this);
      //out.endDocument();
      _lineMap = out.getLineMap();
      if (os != ws) {
        ws.flush();
        ws.free();
      }
    } catch (TransformerException e) {
      throw e;
    } catch (Exception e) {
      throw new TransformerExceptionWrapper(e);
    }
  }

  /**
   * Transforms from the source node to the destination node, returning
   * the destination node.
   */
  public Node transform(Node sourceNode, Node destNode)
    throws SAXException, IOException
  {
    _lineMap = null;

    if (destNode == null)
      destNode = Xml.createDocument();

    DOMBuilder out = new DOMBuilder();
    out.init(destNode);

    try {
      out.startDocument();
      _stylesheet.transform(sourceNode, out, this);
      //out.endDocument();
    } catch (Exception e) {
      throw new IOExceptionWrapper(e);
    }

    return destNode;
  }

  /**
   * Transforms from the source node to the sax handlers.
   */
  public void transform(Node sourceNode,
                        ContentHandler contentHandler,
                        LexicalHandler lexicalHandler)
    throws SAXException, IOException, TransformerException
  {
    if (contentHandler == null)
      throw new IllegalArgumentException(L.l("null content handler"));
    
    _lineMap = null;

    SAXBuilder out = new SAXBuilder();
    out.setContentHandler(contentHandler);

    out.startDocument();
    _stylesheet.transform(sourceNode, out, this);
    //out.endDocument();
  }

  /**
   * Parses the source XML document from the source.
   *
   * @param source the JAXP source.
   *
   * @return the parsed document.
   */
  protected Node parseDocument(Source source)
    throws IOException, SAXException, TransformerException
  {
    if (source instanceof StreamSource) {
      StreamSource stream = (StreamSource) source;

      InputSource in = new InputSource();
      in.setSystemId(stream.getSystemId());
      in.setByteStream(stream.getInputStream());
      in.setCharacterStream(stream.getReader());

      XmlParser parser = Xml.create();

      Node node = parser.parseDocument(in);

      parser.free();

      return node;

      // return new QDocument();
    }
    else if (source instanceof DOMSource){
      Node node = ((DOMSource) source).getNode();
      
      return node != null ? node : new QDocument();
    }
    else if (source instanceof StringSource) {
      String string = ((StringSource) source).getString();

      if (string != null)
        return parseStringDocument(string, source.getSystemId());
      else
        return new QDocument();
    }
    else if (source instanceof SAXSource) {
      SAXSource saxSource = (SAXSource) source;
      
      XMLReader reader = saxSource.getXMLReader();

      if (reader == null)
        return new QDocument();
      
      InputSource inputSource = saxSource.getInputSource();

      Document doc = new QDocument();
      DOMBuilder builder = new DOMBuilder();
      builder.init(doc);
      reader.setContentHandler(builder);

      reader.parse(inputSource);
      
      return doc;
    }

    else
      throw new TransformerException(L.l("unknown source {0}", source));
  }
  
  /**
   * Parses the source XML document from the input stream.
   *
   * @param is the source input stream.
   * @param systemId the path of the source
   *
   * @return document DOM node for the parsed XML.
   */
  protected Node parseDocument(InputStream is, String systemId)
    throws IOException, SAXException
  {
    XmlParser parser = Xml.create();

    Node node = parser.parseDocument(is);

    parser.free();

    return node;
  }

  /**
   * Parses the source document specified by a URL
   *
   * @param url path to the document to be parsed.
   *
   * @return the parsed document.
   */
  protected Node parseDocument(String url)
    throws IOException, SAXException
  {
    XmlParser parser = Xml.create();

    Node node = parser.parseDocument(url);

    parser.free();

    return node;
  }
  
  /**
   * Parses a string as an XML document.
   *
   * @param source the string to use as the XML source
   * @param systemId the URL for the string document.
   *
   * @return the parsed document.
   */
  protected Node parseStringDocument(String source, String systemId)
    throws IOException, SAXException
  {
    XmlParser parser = Xml.create();

    Node node = parser.parseDocumentString(source);

    parser.free();

    return node;
  }

  public void addCacheDepend(Path path)
  {
    _cacheDepends.add(path);
  }

  protected void addCacheDepend(String path)
  {
    _cacheDepends.add(Vfs.lookup(path));
  }

  public ArrayList<Path> getCacheDepends()
  {
    return _cacheDepends;
  }
}
