/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the  "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id$
 */
package org.apache.xml.serializer;

import java.io.IOException;

import org.apache.xml.serializer.utils.MsgKey;
import org.apache.xml.serializer.utils.Utils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * This class is not a public API.
 * It is only public because it is used in other packages. 
 * This class converts SAX or SAX-like calls to a 
 * serialized document for xsl:output method of "text".
 * @xsl.usage internal
 */
public class ToTextStream extends ToStream 
{ 

       
  /**
   * Default constructor.
   */
  public ToTextStream()
  {
    super();
  }

 
 
  /**
   * Receive notification of the beginning of a document.
   *
   * <p>The SAX parser will invoke this method only once, before any
   * other methods in this interface or in DTDHandler (except for
   * setDocumentLocator).</p>
   *
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   *
   * @throws org.xml.sax.SAXException
   */
  protected void startDocumentInternal() throws org.xml.sax.SAXException
  {
    super.startDocumentInternal();

    m_needToCallStartDocument = false;

    // No action for the moment.
  }

  /**
   * Receive notification of the end of a document.
   *
   * <p>The SAX parser will invoke this method only once, and it will
   * be the last method invoked during the parse.  The parser shall
   * not invoke this method until it has either abandoned parsing
   * (because of an unrecoverable error) or reached the end of
   * input.</p>
   *
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   *
   * @throws org.xml.sax.SAXException
   */
  public void endDocument() throws org.xml.sax.SAXException
  {
    flushPending();
    flushWriter();
    if (m_tracer != null)
        super.fireEndDoc();
  }

  /**
   * Receive notification of the beginning of an element.
   *
   * <p>The Parser will invoke this method at the beginning of every
   * element in the XML document; there will be a corresponding
   * endElement() event for every startElement() event (even when the
   * element is empty). All of the element's content will be
   * reported, in order, before the corresponding endElement()
   * event.</p>
   *
   * <p>If the element name has a namespace prefix, the prefix will
   * still be attached.  Note that the attribute list provided will
   * contain only attributes with explicit values (specified or
   * defaulted): #IMPLIED attributes will be omitted.</p>
   *
   *
   * @param namespaceURI The Namespace URI, or the empty string if the
   *        element has no Namespace URI or if Namespace
   *        processing is not being performed.
   * @param localName The local name (without prefix), or the
   *        empty string if Namespace processing is not being
   *        performed.
   * @param name The qualified name (with prefix), or the
   *        empty string if qualified names are not available.
   * @param atts The attributes attached to the element, if any.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   * @see #endElement
   * @see org.xml.sax.AttributeList
   *
   * @throws org.xml.sax.SAXException
   */
  public void startElement(
          String namespaceURI, String localName, String name, Attributes atts)
            throws org.xml.sax.SAXException
  {
    // time to fire off startElement event
    if (m_tracer != null) {
        super.fireStartElem(name);
        this.firePseudoAttributes();
    }
    return;
  }

  /**
   * Receive notification of the end of an element.
   *
   * <p>The SAX parser will invoke this method at the end of every
   * element in the XML document; there will be a corresponding
   * startElement() event for every endElement() event (even when the
   * element is empty).</p>
   *
   * <p>If the element name has a namespace prefix, the prefix will
   * still be attached to the name.</p>
   *
   *
   * @param namespaceURI The Namespace URI, or the empty string if the
   *        element has no Namespace URI or if Namespace
   *        processing is not being performed.
   * @param localName The local name (without prefix), or the
   *        empty string if Namespace processing is not being
   *        performed.
   * @param name The qualified name (with prefix), or the
   *        empty string if qualified names are not available.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   *
   * @throws org.xml.sax.SAXException
   */
  public void endElement(String namespaceURI, String localName, String name)
          throws org.xml.sax.SAXException
  {
        if (m_tracer != null)
            super.fireEndElem(name);           
  }

  /**
   * Receive notification of character data.
   *
   * <p>The Parser will call this method to report each chunk of
   * character data.  SAX parsers may return all contiguous character
   * data in a single chunk, or they may split it into several
   * chunks; however, all of the characters in any single event
   * must come from the same external entity, so that the Locator
   * provides useful information.</p>
   *
   * <p>The application must not attempt to read from the array
   * outside of the specified range.</p>
   *
   * <p>Note that some parsers will report whitespace using the
   * ignorableWhitespace() method rather than this one (validating
   * parsers must do so).</p>
   *
   * @param ch The characters from the XML document.
   * @param start The start position in the array.
   * @param length The number of characters to read from the array.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   * @see #ignorableWhitespace
   * @see org.xml.sax.Locator
   */
  public void characters(char ch[], int start, int length)
          throws org.xml.sax.SAXException
  {

    flushPending();    
    
    try
    {
        if (inTemporaryOutputState()) {
            /* leave characters un-processed as we are
             * creating temporary output, the output generated by
             * this serializer will be input to a final serializer 
             * later on and it will do the processing in final
             * output state (not temporary output state).
             * 
             * A "temporary" ToTextStream serializer is used to
             * evaluate attribute value templates (for example),
             * and the result of evaluating such a thing
             * is fed into a final serializer later on.
             */
            m_writer.write(ch, start, length);
        }
        else {
            // In final output state we do process the characters!
            writeNormalizedChars(ch, start, length, m_lineSepUse);
        }
            
        if (m_tracer != null)
            super.fireCharEvent(ch, start, length);      
    }
    catch(IOException ioe)
    {
      throw new SAXException(ioe);
    }
  }

  /**
   * If available, when the disable-output-escaping attribute is used,
   * output raw text without escaping.
   *
   * @param ch The characters from the XML document.
   * @param start The start position in the array.
   * @param length The number of characters to read from the array.
   *
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   */
  public void charactersRaw(char ch[], int start, int length)
          throws org.xml.sax.SAXException
  {

    try
    {
      writeNormalizedChars(ch, start, length, m_lineSepUse);
    }
    catch(IOException ioe)
    {
      throw new SAXException(ioe);
    }
  }
  
    /**
     * Normalize the characters, but don't escape.  Different from 
     * SerializerToXML#writeNormalizedChars because it does not attempt to do 
     * XML escaping at all.
     *
     * @param ch The characters from the XML document.
     * @param start The start position in the array.
     * @param length The number of characters to read from the array.
     * @param useLineSep true if the operating systems 
     * end-of-line separator should be output rather than a new-line character.
     * 
     * @throws IOException
     * @throws org.xml.sax.SAXException
     */
    void writeNormalizedChars(
        final char ch[],
            final int start,
            final int length,
            final boolean useLineSep)
            throws IOException, org.xml.sax.SAXException 
    {
        final String encoding = getEncoding();
        final java.io.Writer writer = m_writer;
        final int end = start + length;

        /* copy a few "constants" before the loop for performance */
        final char S_LINEFEED = CharInfo.S_LINEFEED;

        // This for() loop always increments i by one at the end
        // of the loop.  Additional increments of i adjust for when
        // two input characters (a high/low UTF16 surrogate pair)
        // are processed.
        for (int i = start; i < end; i++) {
            final char c = ch[i];

            if (S_LINEFEED == c && useLineSep) {
                writer.write(m_lineSep, 0, m_lineSepLen);
                // one input char processed
            } else if (m_encodingInfo.isInEncoding(c)) {
                writer.write(c);
                // one input char processed    
            } else if (Encodings.isHighUTF16Surrogate(c)) {
                final int codePoint = writeUTF16Surrogate(c, ch, i, end);
                if (codePoint != 0) {
                    // I think we can just emit the message,
                    // not crash and burn.
                    final String integralValue = Integer.toString(codePoint);
                    final String msg = Utils.messages.createMessage(
                        MsgKey.ER_ILLEGAL_CHARACTER,
                        new Object[] { integralValue, encoding });
                      
                    //Older behavior was to throw the message,
                    //but newer gentler behavior is to write a message to System.err
                    //throw new SAXException(msg);
                    System.err.println(msg);                            

                }
                i++; // two input chars processed               
            } else {
                // Don't know what to do with this char, it is
                // not in the encoding and not a high char in
                // a surrogate pair, so write out as an entity ref
                if (encoding != null) {
                    /* The output encoding is known, 
                     * so somthing is wrong.
                     */

                    // not in the encoding, so write out a character reference
                    writer.write('&');
                    writer.write('#');
                    writer.write(Integer.toString(c));
                    writer.write(';');

                    // I think we can just emit the message,
                    // not crash and burn.
                    final String integralValue = Integer.toString(c);
                    final String msg = Utils.messages.createMessage(
                        MsgKey.ER_ILLEGAL_CHARACTER,
                        new Object[] { integralValue, encoding });
                      
                    //Older behavior was to throw the message,
                    //but newer gentler behavior is to write a message to System.err
                    //throw new SAXException(msg);
                    System.err.println(msg); 
                } else {
                    /* The output encoding is not known,
                     * so just write it out as-is.
                     */
                    writer.write(c);
                }

                // one input char was processed
            }
        }
    }

  /**
   * Receive notification of cdata.
   *
   * <p>The Parser will call this method to report each chunk of
   * character data.  SAX parsers may return all contiguous character
   * data in a single chunk, or they may split it into several
   * chunks; however, all of the characters in any single event
   * must come from the same external entity, so that the Locator
   * provides useful information.</p>
   *
   * <p>The application must not attempt to read from the array
   * outside of the specified range.</p>
   *
   * <p>Note that some parsers will report whitespace using the
   * ignorableWhitespace() method rather than this one (validating
   * parsers must do so).</p>
   *
   * @param ch The characters from the XML document.
   * @param start The start position in the array.
   * @param length The number of characters to read from the array.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   * @see #ignorableWhitespace
   * @see org.xml.sax.Locator
   */
  public void cdata(char ch[], int start, int length)
          throws org.xml.sax.SAXException
  {
    try
    {
        writeNormalizedChars(ch, start, length, m_lineSepUse);
        if (m_tracer != null)
            super.fireCDATAEvent(ch, start, length);              
    }
    catch(IOException ioe)
    {
      throw new SAXException(ioe);
    }
  }

  /**
   * Receive notification of ignorable whitespace in element content.
   *
   * <p>Validating Parsers must use this method to report each chunk
   * of ignorable whitespace (see the W3C XML 1.0 recommendation,
   * section 2.10): non-validating parsers may also use this method
   * if they are capable of parsing and using content models.</p>
   *
   * <p>SAX parsers may return all contiguous whitespace in a single
   * chunk, or they may split it into several chunks; however, all of
   * the characters in any single event must come from the same
   * external entity, so that the Locator provides useful
   * information.</p>
   *
   * <p>The application must not attempt to read from the array
   * outside of the specified range.</p>
   *
   * @param ch The characters from the XML document.
   * @param start The start position in the array.
   * @param length The number of characters to read from the array.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   * @see #characters
   *
   * @throws org.xml.sax.SAXException
   */
  public void ignorableWhitespace(char ch[], int start, int length)
          throws org.xml.sax.SAXException
  {

    try
    {
      writeNormalizedChars(ch, start, length, m_lineSepUse);
    }
    catch(IOException ioe)
    {
      throw new SAXException(ioe);
    }
  }

  /**
   * Receive notification of a processing instruction.
   *
   * <p>The Parser will invoke this method once for each processing
   * instruction found: note that processing instructions may occur
   * before or after the main document element.</p>
   *
   * <p>A SAX parser should never report an XML declaration (XML 1.0,
   * section 2.8) or a text declaration (XML 1.0, section 4.3.1)
   * using this method.</p>
   *
   * @param target The processing instruction target.
   * @param data The processing instruction data, or null if
   *        none was supplied.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   *
   * @throws org.xml.sax.SAXException
   */
  public void processingInstruction(String target, String data)
          throws org.xml.sax.SAXException
  {
    // flush anything pending first
    flushPending();  
    
    if (m_tracer != null)
        super.fireEscapingEvent(target, data);  
  }

  /**
   * Called when a Comment is to be constructed.
   * Note that Xalan will normally invoke the other version of this method.
   * %REVIEW% In fact, is this one ever needed, or was it a mistake?
   *
   * @param   data  The comment data.
   * @throws org.xml.sax.SAXException Any SAX exception, possibly
   *            wrapping another exception.
   */
  public void comment(String data) throws org.xml.sax.SAXException
  {
      final int length = data.length();
      if (length > m_charsBuff.length)
      {
          m_charsBuff = new char[length*2 + 1];
      }
      data.getChars(0, length, m_charsBuff, 0);
      comment(m_charsBuff, 0, length);
  }

  /**
   * Report an XML comment anywhere in the document.
   *
   * This callback will be used for comments inside or outside the
   * document element, including comments in the external DTD
   * subset (if read).
   *
   * @param ch An array holding the characters in the comment.
   * @param start The starting position in the array.
   * @param length The number of characters to use from the array.
   * @throws org.xml.sax.SAXException The application may raise an exception.
   */
  public void comment(char ch[], int start, int length)
          throws org.xml.sax.SAXException
  {

    flushPending();
    if (m_tracer != null)
        super.fireCommentEvent(ch, start, length);
  }

  /**
   * Receive notivication of a entityReference.
   *
   * @param name non-null reference to the name of the entity.
   *
   * @throws org.xml.sax.SAXException
   */
  public void entityReference(String name) throws org.xml.sax.SAXException
  {
        if (m_tracer != null)
            super.fireEntityReference(name);    
  }
  
    /**
     * @see ExtendedContentHandler#addAttribute(String, String, String, String, String)
     */
    public void addAttribute(
        String uri,
        String localName,
        String rawName,
        String type,
        String value,
        boolean XSLAttribute)
    {
        // do nothing, just forget all about the attribute
    }
 
    /**
     * @see org.xml.sax.ext.LexicalHandler#endCDATA()
     */
    public void endCDATA() throws SAXException
    {
        // do nothing
    }

    /**
     * @see ExtendedContentHandler#endElement(String)
     */
    public void endElement(String elemName) throws SAXException
    {
        if (m_tracer != null)
            super.fireEndElem(elemName);                       
    }
 
    /**
     * From XSLTC
     */
    public void startElement(
    String elementNamespaceURI,
    String elementLocalName,
    String elementName) 
    throws SAXException 
    {
        if (m_needToCallStartDocument)
            startDocumentInternal();        
        // time to fire off startlement event.
        if (m_tracer != null) {
            super.fireStartElem(elementName);
            this.firePseudoAttributes();
        }
        
        return;
    }


    /**
     * From XSLTC
     */
    public void characters(String characters) 
    throws SAXException 
    { 
        final int length = characters.length();
        if (length > m_charsBuff.length)
        {
            m_charsBuff = new char[length*2 + 1];
        }
        characters.getChars(0, length, m_charsBuff, 0);
        characters(m_charsBuff, 0, length); 
    }


    /**
     * From XSLTC
     */
    public void addAttribute(String name, String value)
    {
        // do nothing, forget about the attribute
    }
    
    /**
     * Add a unique attribute
     */
    public void addUniqueAttribute(String qName, String value, int flags)
        throws SAXException
    {
        // do nothing, forget about the attribute 
    }

    public boolean startPrefixMapping(
        String prefix,
        String uri,
        boolean shouldFlush)
        throws SAXException
    {
        // no namespace support for HTML
        return false;
    }


    public void startPrefixMapping(String prefix, String uri)
        throws org.xml.sax.SAXException
    {
        // no namespace support for HTML
    }


    public void namespaceAfterStartElement(
        final String prefix,
        final String uri)
        throws SAXException
    {
        // no namespace support for HTML
    }    

    public void flushPending() throws org.xml.sax.SAXException
    {
            if (m_needToCallStartDocument)
            {
                startDocumentInternal();
                m_needToCallStartDocument = false;
            }
    }
}
