/*
 * blanco Framework
 * Copyright (C) 2004-2005 IGA Tosiki
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */
package blanco.bcel;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;

import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.Type;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import blanco.commons.util.BlancoNameUtil;
import blanco.commons.util.BlancoXmlUtil;

/**
 * Apache Jakarta BCELを用いて JavaクラスファイルをXMLテキストに変換します。
 * 
 * @author IGA Tosiki
 */
public class BlancoBcelJavaClass2Xml {
    /**
     * デバッグモードで動作させるかどうか。<br>
     * デバッグモードの場合、オリジナル行データがXML出力されます。
     */
    private static final boolean IS_DEBUG = false;

    /**
     * classファイルを入力して BCEL解析結果を出力します。
     * 
     * @param sourceFileName
     *            ソースファイル名
     * @param outFileBcel
     *            BCELを出力するファイル
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public final void process(final String sourceFileName,
            final File outFileBcel) throws IOException {
        if (sourceFileName == null) {
            throw new IllegalArgumentException(
                    "Classファイルを解析してXML化する処理において、入力クラスファイル名にnullが与えられました。");
        }
        if (new File(sourceFileName).exists() == false) {
            throw new IllegalArgumentException(
                    "Classファイルを解析してXML化する処理において、入力クラスファイル[" + sourceFileName
                            + "]が見つかりませんでした。");
        }

        final JavaClass javaClass = new ClassParser(sourceFileName).parse();

        final Document document = BlancoXmlUtil.newDocument();
        final Element elementRoot = document.createElement("blancoBcel");
        document.appendChild(elementRoot);

        /**
         * クラスの情報を処理します。
         */
        processClass(javaClass, document);

        /**
         * フィールドの情報を処理します。
         */
        processField(javaClass, document);

        final Element elementMethods = document.createElement("Methods");
        document.getDocumentElement().appendChild(elementMethods);

        final Method[] methods = javaClass.getMethods();
        for (int indexMethod = 0; indexMethod < methods.length; indexMethod++) {
            /**
             * メソッドの情報を処理します。
             */
            processMethod(javaClass, methods[indexMethod], document,
                    elementMethods);
        }

        BlancoXmlUtil.transformDom2File(document, outFileBcel);
    }

    /**
     * クラス名および親クラス名を与えられたライターに出力します。
     * 
     * @param javaClass
     *            BCELのJavaClassオブジェクト
     * @param document
     *            出力先XMLドキュメント
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    private void processClass(final JavaClass javaClass, final Document document)
            throws IOException {
        final Element eleClass = document.createElement("Class");
        document.getDocumentElement().appendChild(eleClass);

        BlancoXmlUtil.addChildElement(document, eleClass, "Name", javaClass
                .getClassName());
        BlancoXmlUtil.addChildElement(document, eleClass, "SuperclassName",
                javaClass.getSuperclassName());

        final String[] interfaceNames = javaClass.getInterfaceNames();
        for (int index = 0; index < interfaceNames.length; index++) {
            BlancoXmlUtil.addChildElement(document, eleClass, "InterfaceName",
                    interfaceNames[index]);
        }

        document.getDocumentElement()
                .appendChild(document.createTextNode("\n"));
    }

    /**
     * クラスの中から static final なフィールドを列挙します。
     * 
     * @param javaClass
     *            BCELのJavaClassオブジェクト
     * @param document
     *            出力先XMLドキュメント
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    private void processField(final JavaClass javaClass, final Document document)
            throws IOException {
        final Element eleFields = document.createElement("Fields");
        document.getDocumentElement().appendChild(eleFields);

        final Field[] fields = javaClass.getFields();
        for (int index = 0; index < fields.length; index++) {
            if (fields[index].isFinal() == false) {
                // 非finalはスキップします。
                continue;
            }
            if (fields[index].isStatic() == false) {
                // 非staticはスキップします。
                continue;
            }

            final Element eleField = document.createElement("ConstantField");
            eleFields.appendChild(eleField);

            BlancoXmlUtil.addChildElement(document, eleField, "Name",
                    fields[index].getName());
            BlancoXmlUtil.addChildElement(document, eleField, "Value",
                    fields[index].getConstantValue().toString());
        }

        document.getDocumentElement()
                .appendChild(document.createTextNode("\n"));
    }

    /**
     * メソッド名およびメソッドの内容を与えられたライターに出力します。
     * 
     * @param javaClass
     *            BCELのJavaClassオブジェクト
     * @param method
     *            BCELのメソッドオブジェクト
     * @param document
     *            出力先XMLドキュメント
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    private void processMethod(final JavaClass javaClass, final Method method,
            final Document document, final Element elementMethods)
            throws IOException {
        final Element eleMethod = document.createElement("Method");
        elementMethods.appendChild(eleMethod);

        BlancoXmlUtil.addChildElement(document, eleMethod, "Name", method
                .getName());

        final Type[] argumentTypes = method.getArgumentTypes();
        for (int indexArgument = 0; indexArgument < argumentTypes.length; indexArgument++) {
            BlancoXmlUtil.addChildElement(document, eleMethod, "Argument",
                    argumentTypes[indexArgument].getSignature());
        }

        BlancoXmlUtil.addChildElement(document, eleMethod, "Return", method
                .getReturnType().getSignature());

        eleMethod.appendChild(document.createTextNode("\n"));

        final Element eleLines = document.createElement("Lines");
        eleMethod.appendChild(eleLines);

        final Code code = method.getCode();
        if (code == null) {
            /**
             * コードが無い場合には処理中断します。
             */
            return;
        }

        /**
         * メソッドのなかのバイトコードをライターに出力します。 <br>
         * 処理に際してVerboseモードはOFFにしています。
         */
        final String result = Utility.codeToString(code.getCode(), javaClass
                .getConstantPool(), 0, -1, false);
        final BufferedReader reader = new BufferedReader(new StringReader(
                result));
        for (;;) {
            final String line = reader.readLine();
            if (line == null) {
                break;
            }

            eleLines.appendChild(document.createTextNode("\n "));
            final Element eleLine = document.createElement("Line");
            eleLines.appendChild(eleLine);

            final String[] splitedWithKolon = BlancoNameUtil.splitString(line,
                    ':');
            if (splitedWithKolon.length < 2) {
                throw new IllegalArgumentException("行データの取得時に不正な値(" + line
                        + ") 分割後の数(" + splitedWithKolon.length
                        + ")が戻りました。処理中断します。");
            }

            eleLine.appendChild(document.createTextNode("\n  "));
            BlancoXmlUtil.addChildElement(document, eleLine, "Number",
                    splitedWithKolon[0]);

            eleLine.appendChild(document.createTextNode("\n  "));
            final String[] splitedWithTab = BlancoNameUtil.splitString(
                    splitedWithKolon[1], '\t');
            final String operation = splitedWithTab[0].trim();

            BlancoXmlUtil.addChildElement(document, eleLine, "Operation",
                    operation);
            eleLine.appendChild(document.createTextNode("\n  "));

            String operand = line;
            for (boolean isTabFound = false; operand.length() > 0; operand = operand
                    .substring(1)) {
                if (isTabFound == false) {
                    // tab検索中です。
                    if (operand.charAt(0) == '\t') {
                        // tab発見しました
                        isTabFound = true;
                    }
                } else {
                    // 先頭部分の ￥ｔを除去します。
                    if (operand.charAt(0) == '\t') {
                        // 処理続行
                    } else {
                        // これで希望の箇所に到達しています。
                        break;
                    }
                }
            }
            BlancoXmlUtil
                    .addChildElement(document, eleLine, "Operand", operand);

            if (IS_DEBUG) {
                eleLine.appendChild(document.createTextNode("\n  "));
                BlancoXmlUtil.addChildElement(document, eleLine,
                        "DebugOriginalLine", line);
            }

            eleLine.appendChild(document.createTextNode("\n "));
        }

        eleMethod.appendChild(document.createTextNode("\n"));
    }
}
