// FormattingSerializer.java
// By Ned Etcode
// Copyright 1995, 1996 Netscape Communications Corp.  All rights reserved.

package netscape.util;

import java.io.OutputStream;
import java.io.IOException;
import java.io.FilterOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;

/** Serializer subclass that formats and indents the
  * ASCII generated by the Serializer. This class makes it possible to insert
  * comments into a serialization.
  */
public class FormattingSerializer extends Serializer {
    private final int MAX_SIZE_FOR_SMALL_EXPRESSION = 80;

    private int indentationLength;
    private int tabLevel;
    private int nextCharIndex;

    /** Constructs a FormattingSerializer that writes to <b>outputStream</b>.
      */
    public FormattingSerializer(OutputStream outputStream) {
        super(outputStream);
        indentationLength = 4;
        tabLevel = 0;
        nextCharIndex = 0;
    }

    /** Sets the number of spaces to indent an expression.  The default value
      * is 4.
      */
    public void setIndentationLength(int numberOfSpaces) {
        indentationLength = numberOfSpaces;
    }

    /** Returns the number of spaces currently used to indent an expression.
     */
    public int indentationLength() {
        return indentationLength;
    }

    /** Writes <b>aComment</b> to the FormattingSerializer's output stream.
      * Ignores all non-ASCII characters. If <b>cStyle</b> is <b>true</b>,
      * the FormattingSerializer will use C-style delimiters, otherwise it will
      * use C++-style delimiters ("//").  <b>aComment</b> should not include
      * comment delimiters.
      */
    public void writeComment(String aComment, boolean cStyle) throws
        IOException {
        int length = aComment.length();
        int delimiterSize;

        if( cStyle )
            delimiterSize = 7;
        else
            delimiterSize = 4;
        /* Does it fits on one line ? */
        if((length + delimiterSize) <= (MAX_SIZE_FOR_SMALL_EXPRESSION -
                                                        nextCharIndex) &&
           aComment.indexOf('\n') == -1 ) {
            int i;
            if( cStyle ) {
                writeCommentCharacter('/');
                writeCommentCharacter('*');
                writeCommentCharacter(' ');
            } else {
                writeCommentCharacter('/');
                writeCommentCharacter('/');
                writeCommentCharacter(' ');
            }
            for( i=0; i < length ; i++ )
                writeCommentCharacter(aComment.charAt(i));
            if( cStyle ) {
                writeCommentCharacter(' ');
                writeCommentCharacter('*');
                writeCommentCharacter('/');
                writeCommentCharacter('\n');
            } else
                writeCommentCharacter('\n');
        } else {
            char ch;
            if( cStyle ) {
                writeCommentCharacter('/');
                writeCommentCharacter('*');
                writeCommentCharacter('\n');
                writeCommentCharacter(' ');
                writeCommentCharacter('*');
                writeCommentCharacter(' ');
            } else {
                writeCommentCharacter('/');
                writeCommentCharacter('/');
                writeCommentCharacter(' ');
            }
            for(int i=0;i < length ; i++ ) {
                ch = aComment.charAt(i);
                if( ch == '\n' ) {
                    writeCommentCharacter('\n');
                    if( cStyle ) {
                        writeCommentCharacter(' ');
                        writeCommentCharacter('*');
                        writeCommentCharacter(' ');
                    } else {
                        writeCommentCharacter('/');
                        writeCommentCharacter('/');
                        writeCommentCharacter(' ');
                    }
                } else
                    writeCommentCharacter(ch);
            }
            if( cStyle ) {
                writeCommentCharacter('\n');
                writeCommentCharacter(' ');
                writeCommentCharacter('*');
                writeCommentCharacter('/');
                writeCommentCharacter('\n');
            } else
                writeCommentCharacter('\n');
        }
    }

    private final void increaseTabLevel() {
        tabLevel++;
    }

    private final void decreaseTabLevel() {
        tabLevel--;
    }

    private final void insertNewLine() throws IOException  {
        int i,c;
        writeCharacter('\n');
        for(i=0,c=tabLevel * indentationLength() ; i < c ; i++ )
            writeCharacter(' ');
    }

    private final void writeCharacter(int c) throws IOException {
        super.writeOutput(c);
        if( c == '\n' )
            nextCharIndex=0;
        else
            nextCharIndex++;
    }

    private final void writeCommentCharacter(int ch) throws IOException {
        if(ch >= 0x20 && ch < 0x7f )
            writeCharacter(ch);
        else switch( ch ) {
        case '\n':
        case '\t':
        case '\r':
            writeCharacter(ch);
        }
    }

    private final int serializedStringFitsIn(String str,int maxSize) {
        int i,c;
        int length;
        char ch;

        if( str == null || str.length() == 0 ) {
            return 2;
        }

        if( (c=str.length()) > maxSize ) {
            return c; /* Ok in fact it is even more but it is greater than maxSize anyway */
        }

        if( stringRequiresQuotes( str ))
            length = 2;
        else
            length = 0;

        for(i=0 ; i < c ; i++ ) {
            ch = str.charAt(i);
            if( ch < 0xff ) {
                if( ch >= '#' && ch <= '~' && ch != '\\' )
                    length++;
                else switch(ch) {
                case ' ':
                case '!':
                    length++;
                    break;
                case '"':
                case '\t':
                case '\n':
                case '\r':
                case '\\':
                    length+=2;
                    break;
                default:
                    length+=4; /* Octal representation */
                }
            } else { /* Unicode */
                length += 6;
            }
            if( length > maxSize )
                return length;
        }
        return length;
    }

    private final int serializedHashtableFitsIn(Hashtable h,int maxSize ) {
        int length=2; /* for { and } */
        Enumeration e = h.keys();
        Object key;
        while( e.hasMoreElements() ) {
            key = e.nextElement();
            length++; /* space */

            length += serializedObjectFitsIn( key , maxSize-length);
            if( length > maxSize )
                return length;

            length += 3; /* space=space */

            length += serializedObjectFitsIn( h.get(key),maxSize-length);
            length++; /* ; */
            if( length > maxSize )
                return length;
        }
        return length;
    }

    private final int serializedArrayFitsIn(Object a[],int maxSize ) {
        int length = 3; /* [ space ] */
        int i,c;
        Object o;
        for(i=0,c=a.length ; i < c ; i++ ) {
            length++; /* space */
            length += serializedObjectFitsIn( a[i] , maxSize-length );
            if(length > maxSize )
                return length;
            if(i<(c-1)) /* , */
                length++;
        }
        return length;
    }

    private final int serializedVectorFitsIn(Vector v,int maxSize ) {
        int length = 3; /* ( space ) */
        int i,c;
        Object o;
        for(i=0,c=v.count() ; i < c ; i++ ) {
            length++; /* space */
            length += serializedObjectFitsIn( v.elementAt(i) , maxSize-length );
            if(length > maxSize )
                return length;
            if(i<(c-1)) /* , */
                length++;
        }
        return length;
    }

    private final int serializedNullFitsIn() {
        return 1;
    }

    private final int serializedObjectFitsIn(Object anObject,int maxSize) {
        if( anObject instanceof String )
            return serializedStringFitsIn((String)anObject,maxSize);
        else if( anObject instanceof Hashtable)
            return serializedHashtableFitsIn((Hashtable)anObject,maxSize);
        else if( anObject instanceof Object[])
            return serializedArrayFitsIn((Object[])anObject,maxSize);
        else if( anObject instanceof Vector)
            return serializedVectorFitsIn((Vector)anObject,maxSize);
        else if( anObject == null)
            return serializedNullFitsIn();
        else {
            return serializedStringFitsIn( anObject.toString(),maxSize);
        }
    }

    private final boolean canFitExpressionOnLine(Object anObject) {
        int maxLength = MAX_SIZE_FOR_SMALL_EXPRESSION - nextCharIndex;
        if( serializedObjectFitsIn(anObject,maxLength) > maxLength )
            return false;
        else
            return true;
    }

    private final void formatVector(Vector aVector) throws IOException {
        int count = aVector.count();
        int i;
        if( canFitExpressionOnLine(aVector) ) {
            writeCharacter('(');
            for(i=0 ; i < count ; i++ ) {
                writeCharacter(' ');
                formatObject(aVector.elementAt(i));
                if( i < (count-1)) {
                    writeCharacter(',');
                }
            }
            writeCharacter(' ');
            writeCharacter(')');
        } else {
            writeCharacter('(');
            increaseTabLevel();
            for(i=0 ; i < count ; i++ ) {
                insertNewLine();
                formatObject(aVector.elementAt(i));
                if( i < (count-1)) {
                    writeCharacter(',');
                }
            }
            decreaseTabLevel();
            insertNewLine();
            writeCharacter(')');
        }
    }

    private final void formatArray(Object anArray[]) throws IOException {
        int count = anArray.length;
        int i;
        if( canFitExpressionOnLine(anArray) ) {
            writeCharacter('[');
            for(i=0 ; i < count ; i++ ) {
                writeCharacter(' ');
                formatObject(anArray[i]);
                if( i < (count-1)) {
                    writeCharacter(',');
                }
            }
            writeCharacter(' ');
            writeCharacter(']');
        } else {
            writeCharacter('[');
            increaseTabLevel();
            for(i=0 ; i < count ; i++ ) {
                insertNewLine();
                formatObject(anArray[i]);
                if( i < (count-1)) {
                    writeCharacter(',');
                }
            }
            decreaseTabLevel();
            insertNewLine();
            writeCharacter(']');
        }
    }

    private final void formatHashtable(Hashtable h) throws IOException {
        int count;
        Enumeration e=h.keys();
        Object key;
        Object value;
        String keys[];
        Object s;
        Object nonStringKeys[];
        int nonStringKeysCount=0;
        int stringCount=0;
        int i;


        count = h.count();
        keys = new String[count];
        nonStringKeys = new Object[count];
        e = h.keys();
        for (i = 0; i < count; i++) {
            s = e.nextElement();
            if( s instanceof String )
                keys[stringCount++] = (String)s;
            else
                nonStringKeys[nonStringKeysCount++] = s;
        }

        if( stringCount > 0 ) {
            Sort.sortStrings(keys, 0, stringCount , true, false);
            System.arraycopy(keys, 0, nonStringKeys,nonStringKeysCount,stringCount);
        }
        if( canFitExpressionOnLine(h)) {
            writeCharacter('{');
            for(i=0 ; i < count ; i++ ) {
                writeCharacter(' ');
                key = nonStringKeys[i];
                value = h.get(key);
                formatObject(key);
                writeCharacter(' '); writeCharacter('='); writeCharacter(' ');
                formatObject(value);
                writeCharacter(';');
            }
            writeCharacter('}');
        } else {
            writeCharacter('{');
            increaseTabLevel();
            for(i=0 ; i < count ; i++ ) {
                key = nonStringKeys[i];
                value = h.get(key);

                insertNewLine();
                formatObject(key);

                writeCharacter(' '); writeCharacter('='); writeCharacter(' ');

                formatObject(value);
                writeCharacter(';');
            }
            decreaseTabLevel();
            insertNewLine();
            writeCharacter('}');
        }
    }

    private final void formatObject(Object anObject) throws IOException {
        if( anObject instanceof String)
            serializeString((String) anObject);
        else if( anObject instanceof Hashtable)
            formatHashtable((Hashtable) anObject);
        else if( anObject instanceof Object[])
            formatArray((Object[])anObject);
        else if( anObject instanceof Vector)
            formatVector((Vector)anObject);
        else if( anObject == null)
            serializeNull();
        else
            serializeString(anObject.toString());
    }

    /** Overridden to produce a formatted serialization of <b>anObject</b>.
      */
    public void writeObject(Object anObject) throws IOException {
        formatObject(anObject);
    }

    /** Convenience method for generating <b>anObject</b>'s ASCII
      * serialization. Returns <b>null</b> on error.
      */
    public static String serializeObject(Object anObject) {
        String result=null;
        if( anObject == null )
            result=null;
        else {
            ByteArrayOutputStream memory = new ByteArrayOutputStream(256);
            FormattingSerializer serializer = new FormattingSerializer(memory);

            try {
                serializer.writeObject(anObject);
                serializer.flush();
            } catch (IOException e) {}


            result = memory.toString();
            try {
                serializer.close();
                memory.close();
            } catch(IOException e) {}
            memory=null;
            serializer=null;
        }
        return result;
    }

    /** Convenience method to format any ASCII serialization produced by a
      * Serializer. Returns <b>null</b> on error.
      */
    public static byte[] formatBytes(byte input[]) {
        ByteArrayInputStream in = new ByteArrayInputStream( input );
        Object o;

        o = Deserializer.readObject( in );
        if( o != null ) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            FormattingSerializer  serializer = new FormattingSerializer(out);
            try {
                serializer.writeObject( o );
                serializer.flush();
            } catch (IOException e) { return null; }
            return out.toByteArray();
        }
        return null;
    }

    /** Convenience method for writing <b>anObject</b's ASCII serialization
      * to <b>outputStream</b>. Returns <b>true</b> if the serialization and
      * writing succeeds, rather than throwing an exception.
      */
    public static boolean writeObject(OutputStream outputStream,
                                      Object anObject) {
        FormattingSerializer serializer;

        try {
            serializer = new FormattingSerializer(outputStream);
            serializer.writeObject(anObject);
            serializer.flush();
        } catch (IOException e) {
            return false;
        }
        return true;
    }
}
