/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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 com.android.dx.command.dump;

import com.android.dx.cf.code.ConcreteMethod;
import com.android.dx.cf.iface.Member;
import com.android.dx.cf.iface.ParseObserver;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;
import com.android.dx.util.IndentingWriter;
import com.android.dx.util.TwoColumnOutput;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;

/**
 * Base class for the various human-friendly dumpers.
 */
public abstract class BaseDumper
        implements ParseObserver {
    /** {@code non-null;} array of data being dumped */
    private final byte[] bytes;

    /** whether or not to include the raw bytes (in a column on the left) */
    private final boolean rawBytes;

    /** {@code non-null;} where to dump to */
    private final PrintStream out;

    /** width of the output in columns */
    private final int width;

    /**
     * {@code non-null;} the file path for the class, excluding any base
     * directory specification
     */
    private final String filePath;

    /** whether to be strict about parsing */
    private final boolean strictParse;

     /** number of bytes per line in hex dumps */
    private final int hexCols;

    /** the current level of indentation */
    private int indent;

    /** {@code non-null;} the current column separator string */
    private String separator;

    /** the offset of the next byte to dump */
    private int at;

    /** commandline parsedArgs */
    protected Args args;

    /**
     * Constructs an instance.
     *
     * @param bytes {@code non-null;} bytes of the (alleged) class file
     * on the left)
     * @param out {@code non-null;} where to dump to
     * @param filePath the file path for the class, excluding any base
     * directory specification
     */
    public BaseDumper(byte[] bytes, PrintStream out,
                      String filePath, Args args) {
        this.bytes = bytes;
        this.rawBytes = args.rawBytes;
        this.out = out;
        this.width = (args.width <= 0) ? 79 : args.width;
        this.filePath = filePath;
        this.strictParse = args.strictParse;
        this.indent = 0;
        this.separator = rawBytes ? "|" : "";
        this.at = 0;
        this.args = args;

        int hexCols = (((width - 5) / 15) + 1) & ~1;
        if (hexCols < 6) {
            hexCols = 6;
        } else if (hexCols > 10) {
            hexCols = 10;
        }
        this.hexCols = hexCols;
    }

    /**
     * Computes the total width, in register-units, of the parameters for
     * this method.
     * @param meth method to process
     * @return width in register-units
     */
    static int computeParamWidth(ConcreteMethod meth, boolean isStatic) {
        return meth.getEffectiveDescriptor().getParameterTypes().
            getWordCount();
    }

    /** {@inheritDoc} */
    public void changeIndent(int indentDelta) {
        indent += indentDelta;

        separator = rawBytes ? "|" : "";
        for (int i = 0; i < indent; i++) {
            separator += "  ";
        }
    }

    /** {@inheritDoc} */
    public void parsed(ByteArray bytes, int offset, int len, String human) {
        offset = bytes.underlyingOffset(offset, getBytes());

        boolean rawBytes = getRawBytes();

        if (offset < at) {
            println("<dump skipped backwards to " + Hex.u4(offset) + ">");
            at = offset;
        } else if (offset > at) {
            String hex = rawBytes ? hexDump(at, offset - at) : "";
            print(twoColumns(hex, "<skipped to " + Hex.u4(offset) + ">"));
            at = offset;
        }

        String hex = rawBytes ? hexDump(offset, len) : "";
        print(twoColumns(hex, human));
        at += len;
    }

    /** {@inheritDoc} */
    public void startParsingMember(ByteArray bytes, int offset, String name,
                                   String descriptor) {
        // This space intentionally left blank.
    }

    /** {@inheritDoc} */
    public void endParsingMember(ByteArray bytes, int offset, String name,
                                 String descriptor, Member member) {
        // This space intentionally left blank.
    }

    /**
     * Gets the current dump cursor (that is, the offset of the expected
     * next byte to dump).
     *
     * @return {@code >= 0;} the dump cursor
     */
    protected final int getAt() {
        return at;
    }

    /**
     * Sets the dump cursor to the indicated offset in the given array.
     *
     * @param arr {@code non-null;} array in question
     * @param offset {@code >= 0;} offset into the array
     */
    protected final void setAt(ByteArray arr, int offset) {
        at = arr.underlyingOffset(offset, bytes);
    }

    /**
     * Gets the array of {@code byte}s to process.
     *
     * @return {@code non-null;} the bytes
     */
    protected final byte[] getBytes() {
        return bytes;
    }

    /**
     * Gets the filesystem/jar path of the file being dumped.
     *
     * @return {@code non-null;} the path
     */
    protected final String getFilePath() {
        return filePath;
    }

    /**
     * Gets whether to be strict about parsing.
     *
     * @return whether to be strict about parsing
     */
    protected final boolean getStrictParse() {
        return strictParse;
    }

    /**
     * Prints the given string to this instance's output stream.
     *
     * @param s {@code null-ok;} string to print
     */
    protected final void print(String s) {
        out.print(s);
    }

    /**
     * Prints the given string to this instance's output stream, followed
     * by a newline.
     *
     * @param s {@code null-ok;} string to print
     */
    protected final void println(String s) {
        out.println(s);
    }

    /**
     * Gets whether this dump is to include raw bytes.
     *
     * @return the raw bytes flag
     */
    protected final boolean getRawBytes() {
        return rawBytes;
    }

    /**
     * Gets the width of the first column of output. This is {@code 0}
     * unless raw bytes are being included in the output.
     *
     * @return {@code >= 0;} the width of the first column
     */
    protected final int getWidth1() {
        if (rawBytes) {
            return 5 + (hexCols * 2) + (hexCols / 2);
        }

        return 0;
    }

    /**
     * Gets the width of the second column of output.
     *
     * @return {@code >= 0;} the width of the second column
     */
    protected final int getWidth2() {
        int w1 = rawBytes ? (getWidth1() + 1) : 0;
        return width - w1 - (indent * 2);
    }

    /**
     * Constructs a hex data dump of the given portion of {@link #bytes}.
     *
     * @param offset offset to start dumping at
     * @param len length to dump
     * @return {@code non-null;} the dump
     */
    protected final String hexDump(int offset, int len) {
        return Hex.dump(bytes, offset, len, offset, hexCols, 4);
    }

    /**
     * Combines a pair of strings as two columns, or if this is one-column
     * output, format the otherwise-second column.
     *
     * @param s1 {@code non-null;} the first column's string
     * @param s2 {@code non-null;} the second column's string
     * @return {@code non-null;} the combined output
     */
    protected final String twoColumns(String s1, String s2) {
        int w1 = getWidth1();
        int w2 = getWidth2();

        try {
            if (w1 == 0) {
                int len2 = s2.length();
                StringWriter sw = new StringWriter(len2 * 2);
                IndentingWriter iw = new IndentingWriter(sw, w2, separator);

                iw.write(s2);
                if ((len2 == 0) || (s2.charAt(len2 - 1) != '\n')) {
                    iw.write('\n');
                }
                iw.flush();

                return sw.toString();
            } else {
                return TwoColumnOutput.toString(s1, w1, separator, s2, w2);
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
