/*
 * blanco Framework
 * Copyright (C) 2004-2007 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.valueobjectfactory;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;

import blanco.cg.BlancoCgObjectFactory;
import blanco.cg.BlancoCgSupportedLang;
import blanco.cg.transformer.BlancoCgTransformerFactory;
import blanco.cg.util.BlancoCgSourceUtil;
import blanco.cg.valueobject.BlancoCgClass;
import blanco.cg.valueobject.BlancoCgMethod;
import blanco.cg.valueobject.BlancoCgSourceFile;
import blanco.commons.util.BlancoNameAdjuster;
import blanco.commons.util.BlancoNameUtil;
import blanco.commons.util.BlancoStringUtil;
import blanco.resourcebundle.BlancoResourceBundleXmlParser;
import blanco.resourcebundle.valueobject.BlancoResourceBundleBundleStructure;
import blanco.valueobject.BlancoValueObjectXmlParser;
import blanco.valueobject.valueobject.BlancoValueObjectClassStructure;
import blanco.valueobject.valueobject.BlancoValueObjectFieldStructure;
import blanco.valueobjectfactory.resourcebundle.BlancoValueObjectFactoryResourceBundle;
import blanco.valueobjectfactory.valueobject.BlancoValueObjectFactoryStructure;
import blanco.xml.bind.BlancoXmlBindingUtil;
import blanco.xml.bind.BlancoXmlUnmarshaller;
import blanco.xml.bind.valueobject.BlancoXmlDocument;
import blanco.xml.bind.valueobject.BlancoXmlElement;

/**
 * 「バリューオブジェクトファクトリ定義書」Excel様式から バリューオブジェクトのためのファクトリクラス・ソースコードを生成。
 * 
 * このクラスは、中間XMLファイルからソースコードを自動生成する機能を担います。
 * 
 * @author IGA Tosiki
 */
public class BlancoValueObjectFactoryXml2SourceFile {
    /**
     * このプロダクトのリソースバンドルへのアクセスオブジェクト。
     */
    private final BlancoValueObjectFactoryResourceBundle fBundle = new BlancoValueObjectFactoryResourceBundle();

    /**
     * 内部的に利用するblancoCg用ファクトリ。
     */
    private BlancoCgObjectFactory fCgFactory = null;

    /**
     * 内部的に利用するblancoCg用ソースファイル情報。
     */
    private BlancoCgSourceFile fCgSourceFile = null;

    /**
     * 内部的に利用するblancoCg用クラス情報。
     */
    private BlancoCgClass fCgClass = null;

    /**
     * 中間XMLファイルからソースコードを自動生成します。
     * 
     * このクラスの主たるメソッドとなります。
     * 
     * あらかじめ、バリューオブジェクト情報のマップとリソースバンドル情報のマップを準備しておいてからこのメソッドを呼び出します。
     * 
     * @param argMetaXmlSourceFile
     *            メタ情報が含まれているXMLファイル。
     * @param argValueObjectMap
     *            バリューオブジェクト情報のマップ。
     * @param argResourceBundleMap
     *            リソースバンドル情報のマップ。
     * @param argDirectoryTarget
     *            ソースコード生成先ディレクトリ (/mainを除く部分を指定します)。
     * @throws IOException
     *             入出力例外が発生した場合。
     */
    public void process(final File argMetaXmlSourceFile,
            final HashMap argValueObjectMap,
            final HashMap argResourceBundleMap, final File argDirectoryTarget)
            throws IOException {
        // メタ情報を解析してバリューオブジェクトのツリーを取得します。
        final BlancoXmlDocument xmlDocument = new BlancoXmlUnmarshaller()
                .unmarshal(argMetaXmlSourceFile);

        // ルートエレメントを取得します。
        final BlancoXmlElement elementRoot = BlancoXmlBindingUtil
                .getDocumentElement(xmlDocument);
        if (elementRoot == null) {
            // ルートエレメントが無い場合には処理中断します。
            return;
        }

        // sheet(Excelシート)のリストを取得します。
        final List listSheet = BlancoXmlBindingUtil.getElementsByTagName(
                elementRoot, "sheet");
        final int sizeListSheet = listSheet.size();
        for (int index = 0; index < sizeListSheet; index++) {
            // おのおののシートを処理します。
            final BlancoXmlElement elementSheet = (BlancoXmlElement) listSheet
                    .get(index);

            // 共通情報を取得します。
            final BlancoXmlElement elementCommon = BlancoXmlBindingUtil
                    .getElement(elementSheet, fBundle
                            .getMeta2xmlElementCommon());
            if (elementCommon == null) {
                // commonが無い場合には、このシートの処理をスキップします。
                continue;
            }

            final String name = BlancoXmlBindingUtil.getTextContent(
                    elementCommon, "name");
            if (BlancoStringUtil.null2Blank(name).trim().length() == 0) {
                // nameが空の場合には処理をスキップします。
                continue;
            }

            // データソース情報を取得します。
            final BlancoXmlElement elementDataSource = BlancoXmlBindingUtil
                    .getElement(elementSheet, fBundle
                            .getMeta2xmlElementDatasource());

            // シートから詳細な情報を取得します。
            final BlancoValueObjectFactoryStructure processStructure = parseSheet(
                    elementCommon, elementDataSource, argDirectoryTarget);
            if (processStructure == null) {
                continue;
            }

            if (processStructure.getName() == null) {
                throw new IllegalArgumentException("ありえない！これはダメ。");
            }

            // メタ情報の解析結果をもとにソースコード自動生成を実行します。

            if (argValueObjectMap.get(processStructure.getName()) == null) {
                // 見つからないのは例外扱い。
                throw new IllegalArgumentException("バリューオブジェクトファクトリ定義書["
                        + processStructure.getName()
                        + "]に対応するバリューオブジェクトが見つかりません");
            }

            if (argResourceBundleMap.get(processStructure
                    .getDataSourceResourceBundle()) == null) {
                // 見つからないのは例外扱い。
                throw new IllegalArgumentException("バリューオブジェクトファクトリ定義書["
                        + processStructure.getName() + "]に対応するリソースバンドル["
                        + processStructure.getDataSourceResourceBundle()
                        + "]が見つかりません");
            }

            final BlancoValueObjectClassStructure valueObjectClassStructure = (BlancoValueObjectClassStructure) argValueObjectMap
                    .get(processStructure.getName());
            final BlancoResourceBundleBundleStructure resourceBundleClassStructure = (BlancoResourceBundleBundleStructure) argResourceBundleMap
                    .get(processStructure.getDataSourceResourceBundle());

            expandSourceFile(processStructure, valueObjectClassStructure,
                    resourceBundleClassStructure, argDirectoryTarget);
        }
    }

    /**
     * sheetエレメントを展開します。
     * 
     * @param argElementCommon
     *            現在処理しているCommonノード。
     * @param elementBindtarget
     *            現在処理しているBindtargetノード。
     * @param argDirectoryTarget
     *            ソースコードの出力先フォルダ。
     * @return 収集されたメタ情報構造データ。
     */
    private BlancoValueObjectFactoryStructure parseSheet(
            final BlancoXmlElement argElementCommon,
            final BlancoXmlElement elementBindtarget,
            final File argDirectoryTarget) {

        final BlancoValueObjectFactoryStructure processStructure = new BlancoValueObjectFactoryStructure();
        processStructure.setName(BlancoXmlBindingUtil.getTextContent(
                argElementCommon, "name"));
        processStructure.setPackage(BlancoXmlBindingUtil.getTextContent(
                argElementCommon, "package"));

        if (processStructure.getName() == null) {
            // 名称がnullのものについては 処理をスキップします。
            return null;
        }

        if (BlancoStringUtil.null2Blank(processStructure.getPackage()).trim()
                .length() == 0) {
            throw new IllegalArgumentException(fBundle
                    .getXml2sourceFileErr001(processStructure.getName()));
        }

        if (BlancoXmlBindingUtil
                .getTextContent(argElementCommon, "description") != null) {
            processStructure.setDescription(BlancoXmlBindingUtil
                    .getTextContent(argElementCommon, "description"));
        }

        if (BlancoXmlBindingUtil.getTextContent(argElementCommon, "suffix") != null) {
            processStructure.setSuffix(BlancoXmlBindingUtil.getTextContent(
                    argElementCommon, "suffix"));
        }

        if (elementBindtarget == null) {
            // TODO 例外処理！
        }

        processStructure.setDataSourceResourceBundle(BlancoXmlBindingUtil
                .getTextContent(elementBindtarget, "resourceBundle"));

        // TODO ターゲットのリソースバンドル無指定はエラー扱い！

        return processStructure;
    }

    /**
     * 収集された情報を元に、ソースコードを自動生成します。
     * 
     * @param argProcessStructure
     *            メタファイルから収集できた処理構造データ。
     * @param argValueObjectClassStructure
     *            関連付けられたバリューオブジェクト情報。
     * @param argResourceBundleClassStructure
     *            関連付けられたデータソースのリソースバンドル情報。
     * @param argDirectoryTarget
     *            ソースコードの出力先フォルダ。
     */
    private void expandSourceFile(
            final BlancoValueObjectFactoryStructure argProcessStructure,
            final BlancoValueObjectClassStructure argValueObjectClassStructure,
            final BlancoResourceBundleBundleStructure argResourceBundleClassStructure,
            final File argDirectoryTarget) {

        // 従来と互換性を持たせるため、/mainサブフォルダに出力します。
        final File fileBlancoMain = new File(argDirectoryTarget
                .getAbsolutePath()
                + "/main");

        fCgFactory = BlancoCgObjectFactory.getInstance();
        fCgSourceFile = fCgFactory.createSourceFile(argProcessStructure
                .getPackage(), "このソースコードは blanco Frameworkによって自動生成されています。");
        fCgClass = fCgFactory.createClass(argProcessStructure.getName()
                + BlancoStringUtil.null2Blank(argProcessStructure.getSuffix()),
                BlancoStringUtil.null2Blank(argProcessStructure
                        .getDescription()));
        fCgSourceFile.getClassList().add(fCgClass);

        fCgClass.getLangDoc().getDescriptionList().add(
                "バリューオブジェクト[" + argValueObjectClassStructure.getName()
                        + "]のインスタンスを適切なデフォルト値付で取得するためのクラス。");

        expandField(argProcessStructure);
        expandMethodConstructor(argProcessStructure);
        expandMethodGetInstance(argProcessStructure,
                argValueObjectClassStructure, argResourceBundleClassStructure);

        BlancoCgTransformerFactory.getJavaSourceTransformer().transform(
                fCgSourceFile, fileBlancoMain);
    }

    /**
     * フィールドを展開します。
     * 
     * @param argProcessStructure
     *            メタファイルから収集できた処理構造データ。
     */
    private void expandField(
            final BlancoValueObjectFactoryStructure argProcessStructure) {
        // フィールドは特にありません。
    }

    /**
     * コンストラクタを展開します。
     * 
     * @param argProcessStructure
     *            メタファイルから収集できた処理構造データ。
     */
    private void expandMethodConstructor(
            final BlancoValueObjectFactoryStructure argProcessStructure) {
        // コンストラクタを展開します。

        final BlancoCgMethod cgMethod = fCgFactory.createMethod(fCgClass
                .getName(), "コンストラクタは隠します。");
        cgMethod.setConstructor(true);
        cgMethod.setAccess("private");
        fCgClass.getMethodList().add(cgMethod);
    }

    /**
     * getInstance メソッドを展開します。
     * 
     * @param argProcessStructure
     *            メタファイルから収集できた処理構造データ。
     * @param argValueObjectClassStructure
     *            関連付けられたバリューオブジェクト情報。
     * @param argResourceBundleClassStructure
     *            関連付けられたデータソースのリソースバンドル情報。
     */
    private void expandMethodGetInstance(
            final BlancoValueObjectFactoryStructure argProcessStructure,
            final BlancoValueObjectClassStructure argValueObjectClassStructure,
            final BlancoResourceBundleBundleStructure argResourceBundleClassStructure) {
        final BlancoCgMethod cgMethod = fCgFactory.createMethod("getInstance",
                BlancoCgSourceUtil.escapeStringAsLangDoc(
                        BlancoCgSupportedLang.JAVA, "バリューオブジェクト["
                                + argValueObjectClassStructure.getName()
                                + "]のインスタンスを取得します。"));
        fCgClass.getMethodList().add(cgMethod);

        cgMethod.setStatic(true);
        cgMethod.getLangDoc().getDescriptionList().add(
                "リソースバンドル(基底名:"
                        + argResourceBundleClassStructure.getBaseName()
                        + ", クラス名:"
                        + argResourceBundleClassStructure.getBaseName()
                        + BlancoStringUtil
                                .null2Blank(argResourceBundleClassStructure
                                        .getSuffix()) + ")からデフォルト値の読み込みを試みます。");

        final String targetValueObjectClassName = argValueObjectClassStructure
                .getPackage()
                + "." + argValueObjectClassStructure.getName();
        final String targetResourceBundleClassName = argResourceBundleClassStructure
                .getPackageName()
                + "."
                + argResourceBundleClassStructure.getBaseName()
                + argResourceBundleClassStructure.getSuffix();

        cgMethod.setReturn(fCgFactory.createReturn(targetValueObjectClassName,
                "デフォルト値が調整済みの[" + argValueObjectClassStructure.getName()
                        + "]バリューオブジェクト。"));

        final List lineList = cgMethod.getLineList();

        // TODO リソースバンドル定義書でサフィックスを無指定ものもへの対応
        // TODO 定義書の指定先がない場合の対応。

        fCgSourceFile.getImportList().add(targetResourceBundleClassName);

        lineList.add("// このメソッドの実行結果となるバリューオブジェクトインスタンス。");
        lineList.add("final " + argValueObjectClassStructure.getName()
                + " result = new " + argValueObjectClassStructure.getName()
                + "();");
        lineList.add("");
        lineList.add("// バリューオブジェクトに設定するデフォルト値のためのデータソースインスタンス。");
        lineList.add("final "
                + BlancoNameUtil.trimJavaPackage(targetResourceBundleClassName)
                + " bundle = new "
                + BlancoNameUtil.trimJavaPackage(targetResourceBundleClassName)
                + "();");
        lineList.add("");

        for (int index = 0; index < argValueObjectClassStructure.getListField()
                .size(); index++) {
            final BlancoValueObjectFieldStructure field = (BlancoValueObjectFieldStructure) argValueObjectClassStructure
                    .getListField().get(index);
            lineList.add("// フィールド [" + field.getName() + "]");

            if ("java.lang.String".equals(field.getType())) {
                // 文字の場合
                lineList.add("if (bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "() != null && bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "().length() > 0) {");
                lineList.add("result.set"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "(bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "());");
                lineList.add("}");
            } else if ("int".equals(field.getType())) {
                // 数値の場合
                lineList.add("if (bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "() != null && bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "().length() > 0) {");
                lineList.add("try {");
                lineList.add("result.set"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "(Integer.parseInt(bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "()));");
                lineList.add("} catch (NumberFormatException e) {");
                lineList.add("// 無視します。");
                lineList.add("}");
                lineList.add("}");
            } else if ("boolean".equals(field.getType())) {
                // 数値の場合
                lineList.add("if (bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "() != null && bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "().length() > 0) {");
                lineList.add("result.set"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "(\"true\".equals(bundle.get"
                        + BlancoNameAdjuster.toClassName(field.getName())
                        + "()));");
                lineList.add("}");
            } else {
                lineList.add("// サポート外の型 [" + field.getType() + "]");
            }

            lineList.add("");
        }

        lineList.add("return result;");
    }

    // ユーティリティ的なメソッド

    /**
     * 指定されたディレクトリの中に含まれるバリューオブジェクト情報(中間XML)を全て取得します。
     * 
     * @param argTargetDirectory
     *            バリューオブジェクト情報(中間XML)が格納されているディレクトリ。
     * @param argMapResult
     *            収集されたバリューオブジェクト情報。
     */
    public static void processBlancoValueObjectXmlDirectory(
            final File argTargetDirectory, final HashMap argMapResult) {
        final File[] fileMeta = argTargetDirectory.listFiles();
        for (int index = 0; index < fileMeta.length; index++) {
            if (fileMeta[index].getName().endsWith(".xml") == false) {
                continue;
            }

            final BlancoValueObjectClassStructure[] structures = new BlancoValueObjectXmlParser()
                    .parseFile(fileMeta[index]);
            for (int indexStructure = 0; indexStructure < structures.length; indexStructure++) {
                argMapResult.put(structures[indexStructure].getName(),
                        structures[indexStructure]);
            }
        }
    }

    /**
     * 指定されたディレクトリの中に含まれるリソースバンドル情報(中間XML)を全て取得します。
     * 
     * @param argTargetDirectory
     *            リソースバンドル情報(中間XML)が格納されているディレクトリ。
     * @param argMapResult
     *            収集されたリソースバンドル情報。
     */
    public static void processBlancoResourceBundleXmlDirectory(
            final File argTargetDirectory, final HashMap argMapResult) {
        final File[] fileMeta = argTargetDirectory.listFiles();
        for (int index = 0; index < fileMeta.length; index++) {
            if (fileMeta[index].getName().endsWith(".xml") == false) {
                continue;
            }

            final BlancoResourceBundleBundleStructure[] structures = new BlancoResourceBundleXmlParser()
                    .parseFile(fileMeta[index]);
            for (int indexStructure = 0; indexStructure < structures.length; indexStructure++) {
                argMapResult.put(structures[indexStructure].getBaseName(),
                        structures[indexStructure]);
            }
        }
    }
}
