//  Copyright 2004 The Apache Software Foundation
//
// Licensed 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.

package org.apache.tapestry.util.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;

/**
 *  A kind of super-formatter.  It is sent a stream of binary data and
 *  formats it in a human-readable dump format which is forwarded to
 *  its output stream.
 *
 * <p>Currently, output is in hex though options to change that may
 * be introduced.
 *
 *  @author Howard Lewis Ship
 *  @version $Id: BinaryDumpOutputStream.java 243791 2004-02-19 17:38:13Z hlship $
 * 
 **/

public class BinaryDumpOutputStream extends OutputStream
{
    private PrintWriter out;

    private boolean locked = false;

    private boolean showOffset = true;
    private int bytesPerLine = 16;
    private int spacingInterval = 4;
    private char substituteChar = '.';
    private String offsetSeperator = ": ";
    private int offset = 0;
    private int lineCount = 0;
    private int bytesSinceSpace = 0;
    private char[] ascii = null;
    private boolean showAscii = true;
    private String asciiBegin = "  |";
    private String asciiEnd = "|";

    private static final char[] HEX =
        {
            '0',
            '1',
            '2',
            '3',
            '4',
            '5',
            '6',
            '7',
            '8',
            '9',
            'a',
            'b',
            'c',
            'd',
            'e',
            'f' };

    /**
     *  Creates a <code>PrintWriter</code> for <code>System.out</code>.
     *
     **/

    public BinaryDumpOutputStream()
    {
        this(new PrintWriter(System.out, true));
    }

    public BinaryDumpOutputStream(PrintWriter out)
    {
        this.out = out;
    }

    public BinaryDumpOutputStream(Writer out)
    {
        this.out = new PrintWriter(out);
    }

    public void close() throws IOException
    {
        if (out != null)
        {
            if (lineCount > 0)
                finishFinalLine();

            out.close();
        }

        out = null;
    }

    private void finishFinalLine()
    {
        // Since we only finish the final line after at least one byte has
        // been written to it, we don't need to worry about
        // the offset.

        while (lineCount < bytesPerLine)
        {
            // After every <n> bytes, emit a space.

            if (spacingInterval > 0 && bytesSinceSpace == spacingInterval)
            {
                out.print(' ');
                bytesSinceSpace = 0;
            }

            // Two spaces to substitute for the two hex digits.

            out.print("  ");

            if (showAscii)
                ascii[lineCount] = ' ';

            lineCount++;
            bytesSinceSpace++;
        }

        if (showAscii)
        {
            out.print(asciiBegin);
            out.print(ascii);
            out.print(asciiEnd);
        }

        out.println();
    }

    /**
     *  Forwards the <code>flush()</code> to the <code>PrintWriter</code>.
     *
     **/

    public void flush() throws IOException
    {
        out.flush();
    }

    public String getAsciiBegin()
    {
        return asciiBegin;
    }

    public String getAsciiEnd()
    {
        return asciiEnd;
    }

    public int getBytesPerLine()
    {
        return bytesPerLine;
    }

    public String getOffsetSeperator()
    {
        return offsetSeperator;
    }

    public boolean getShowAscii()
    {
        return showAscii;
    }

    public char getSubstituteChar()
    {
        return substituteChar;
    }

    public void setAsciiBegin(String value)
    {
        if (locked)
            throw new IllegalStateException();

        asciiBegin = value;
    }

    public void setAsciiEnd(String value)
    {
        if (locked)
            throw new IllegalStateException();

        asciiEnd = value;
    }

    public void setBytesPerLine(int value)
    {
        if (locked)
            throw new IllegalStateException();

        bytesPerLine = value;

        ascii = null;
    }

    public void setOffsetSeperator(String value)
    {
        if (locked)
            throw new IllegalStateException();

        offsetSeperator = value;
    }

    public void setShowAscii(boolean value)
    {
        if (locked)
            throw new IllegalStateException();

        showAscii = value;
    }

    /**
     *  Sets the character used in the ASCII dump that substitutes for characters
     *  outside the range of 32..126.
     *
     **/

    public void setSubstituteChar(char value)
    {
        if (locked)
            throw new IllegalStateException();

        substituteChar = value;
    }

    public void write(int b) throws IOException
    {
        char letter;

        if (showAscii && ascii == null)
            ascii = new char[bytesPerLine];

        // Prevent further customization after output starts being written.

        locked = true;

        if (lineCount == bytesPerLine)
        {
            if (showAscii)
            {
                out.print(asciiBegin);
                out.print(ascii);
                out.print(asciiEnd);
            }

            out.println();

            bytesSinceSpace = 0;
            lineCount = 0;
            offset += bytesPerLine;
        }

        if (lineCount == 0 && showOffset)
        {
            writeHex(offset, 4);
            out.print(offsetSeperator);
        }

        // After every <n> bytes, emit a space.

        if (spacingInterval > 0 && bytesSinceSpace == spacingInterval)
        {
            out.print(' ');
            bytesSinceSpace = 0;
        }

        writeHex(b, 2);

        if (showAscii)
        {
            if (b < 32 | b > 127)
                letter = substituteChar;
            else
                letter = (char) b;

            ascii[lineCount] = letter;
        }

        lineCount++;
        bytesSinceSpace++;
    }

    private void writeHex(int value, int digits)
    {
        int i;
        int nybble;

        for (i = 0; i < digits; i++)
        {
            nybble = (value >> 4 * (digits - i - 1)) & 0x0f;

            out.print(HEX[nybble]);
        }
    }
}