/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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.seasar.framework.convention.impl;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.seasar.framework.convention.NamingConvention;
import org.seasar.framework.exception.EmptyRuntimeException;
import org.seasar.framework.util.ArrayUtil;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.Disposable;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.MapUtil;
import org.seasar.framework.util.ResourceUtil;
import org.seasar.framework.util.ResourcesUtil;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.ResourcesUtil.Resources;

/**
 * {@link NamingConvention}の実装クラスです。
 * 
 * @author higa
 * @author shot
 */
public class NamingConventionImpl implements NamingConvention, Disposable {

    private static final char PACKAGE_SEPARATOR = '_';

    private static final String PACKAGE_SEPARATOR_STR = "_";

    private boolean initialized;

    private String viewRootPath = "/view";

    private String viewExtension = ".html";

    private String implementationSuffix = "Impl";

    private String pageSuffix = "Page";

    private String actionSuffix = "Action";

    private String serviceSuffix = "Service";

    private String dxoSuffix = "Dxo";

    private String logicSuffix = "Logic";

    private String daoSuffix = "Dao";

    private String helperSuffix = "Helper";

    private String interceptorSuffix = "Interceptor";

    private String validatorSuffix = "Validator";

    private String converterSuffix = "Converter";

    private String dtoSuffix = "Dto";

    private String connectorSuffix = "Connector";

    private String subApplicationRootPackageName = "web";

    private String entityPackageName = "entity";

    private String[] rootPackageNames = new String[0];

    private Set hotdeployRootPackageNames = new HashSet();

    private String[] ignorePackageNames = new String[0];

    private Map existCheckerArrays = MapUtil.createHashMap();

    private Map interfaceToImplementationMap = new HashMap();

    private Map implementationToInterfaceMap = new HashMap();

    /**
     * {@link NamingConventionImpl}を作成します。
     */
    public NamingConventionImpl() {
        initialize();
    }

    /**
     * 初期化します。
     */
    public void initialize() {
        if (!initialized) {
            for (int i = 0; i < rootPackageNames.length; ++i) {
                addExistChecker(rootPackageNames[i]);
            }
            DisposableUtil.add(this);
            initialized = true;
        }
    }

    public void dispose() {
        for (final Iterator it = existCheckerArrays.values().iterator(); it
                .hasNext();) {
            final Resources[] array = (Resources[]) it.next();
            for (int i = 0; i < array.length; ++i) {
                array[i].close();
            }
        }
        existCheckerArrays.clear();
        initialized = false;
    }

    public String getPageSuffix() {
        return pageSuffix;
    }

    /**
     * <code>Page</code>の<code>suffix</code>を設定します。
     * 
     * @param pageSuffix
     */
    public void setPageSuffix(final String pageSuffix) {
        this.pageSuffix = pageSuffix;
    }

    public String getActionSuffix() {
        return actionSuffix;
    }

    /**
     * <code>Action</code>の<code>suffix</code>を設定します。
     * 
     * @param actionSuffix
     */
    public void setActionSuffix(final String actionSuffix) {
        this.actionSuffix = actionSuffix;
    }

    public String getConnectorSuffix() {
        return connectorSuffix;
    }

    /**
     * <code>Connector</code>の<code>suffix</code>を設定します。
     * 
     * @param connectorSuffix
     */
    public void setConnectorSuffix(final String connectorSuffix) {
        this.connectorSuffix = connectorSuffix;
    }

    public String getDaoSuffix() {
        return daoSuffix;
    }

    /**
     * <code>Dao</code>の<code>suffix</code>を設定します。
     * 
     * @param daoSuffix
     */
    public void setDaoSuffix(final String daoSuffix) {
        this.daoSuffix = daoSuffix;
    }

    public String getDtoSuffix() {
        return dtoSuffix;
    }

    /**
     * <code>Dto</code>の<code>suffix</code>を設定します。
     * 
     * @param dtoSuffix
     */
    public void setDtoSuffix(final String dtoSuffix) {
        this.dtoSuffix = dtoSuffix;
    }

    public String getDxoSuffix() {
        return dxoSuffix;
    }

    /**
     * <code>Dxo</code>の<code>suffix</code>を設定します。
     * 
     * @param dxoSuffix
     */
    public void setDxoSuffix(final String dxoSuffix) {
        this.dxoSuffix = dxoSuffix;
    }

    public String getHelperSuffix() {
        return helperSuffix;
    }

    /**
     * <code>Helper</code>の<code>suffix</code>を設定します。
     * 
     * @param helperSuffix
     */
    public void setHelperSuffix(final String helperSuffix) {
        this.helperSuffix = helperSuffix;
    }

    public String getInterceptorSuffix() {
        return interceptorSuffix;
    }

    /**
     * <code>Interceptor</code>の<code>suffix</code>を設定します。
     * 
     * @param interceptorSuffix
     */
    public void setInterceptorSuffix(final String interceptorSuffix) {
        this.interceptorSuffix = interceptorSuffix;
    }

    public String getLogicSuffix() {
        return logicSuffix;
    }

    /**
     * <code>Logic</code>の<code>suffix</code>を設定します。
     * 
     * @param logicSuffix
     */
    public void setLogicSuffix(final String logicSuffix) {
        this.logicSuffix = logicSuffix;
    }

    public String getServiceSuffix() {
        return serviceSuffix;
    }

    /**
     * <code>Service</code>の<code>suffix</code>を設定します。
     * 
     * @param serviceSuffix
     */
    public void setServiceSuffix(final String serviceSuffix) {
        this.serviceSuffix = serviceSuffix;
    }

    public String getValidatorSuffix() {
        return validatorSuffix;
    }

    /**
     * <code>Validator</code>の<code>suffix</code>を設定します。
     * 
     * @param validatorSuffix
     */
    public void setValidatorSuffix(final String validatorSuffix) {
        this.validatorSuffix = validatorSuffix;
    }

    public String getConverterSuffix() {
        return converterSuffix;
    }

    /**
     * <code>Converter</code>の<code>suffix</code>を設定します。
     * 
     * @param converterSuffix
     */
    public void setConverterSuffix(final String converterSuffix) {
        this.converterSuffix = converterSuffix;
    }

    public String getImplementationSuffix() {
        return implementationSuffix;
    }

    /**
     * 実装クラスの<code>suffix</code>を設定します。
     * 
     * @param implementationSuffix
     */
    public void setImplementationSuffix(final String implementationSuffix) {
        this.implementationSuffix = implementationSuffix;
    }

    public String getDaoPackageName() {
        return fromSuffixToPackageName(daoSuffix);
    }

    public String getDtoPackageName() {
        return fromSuffixToPackageName(dtoSuffix);
    }

    public String getDxoPackageName() {
        return fromSuffixToPackageName(dxoSuffix);
    }

    public String getHelperPackageName() {
        return fromSuffixToPackageName(helperSuffix);
    }

    public String getInterceptorPackageName() {
        return fromSuffixToPackageName(interceptorSuffix);
    }

    public String getLogicPackageName() {
        return fromSuffixToPackageName(logicSuffix);
    }

    public String getServicePackageName() {
        return fromSuffixToPackageName(serviceSuffix);
    }

    public String getValidatorPackageName() {
        return fromSuffixToPackageName(validatorSuffix);
    }

    public String getConverterPackageName() {
        return fromSuffixToPackageName(converterSuffix);
    }

    public String getConnectorPackageName() {
        return fromSuffixToPackageName(connectorSuffix);
    }

    public String getImplementationPackageName() {
        return fromSuffixToPackageName(implementationSuffix);
    }

    public String getEntityPackageName() {
        return entityPackageName;
    }

    /**
     * <code>Entity</code>のパッケージを設定します。
     * 
     * @param entityPackage
     */
    public void setEntityPackage(final String entityPackage) {
        this.entityPackageName = entityPackage;
    }

    public String getViewExtension() {
        return viewExtension;
    }

    /**
     * <code>View</code>の拡張子を設定します。
     * 
     * @param viewExtension
     */
    public void setViewExtension(final String viewExtension) {
        this.viewExtension = viewExtension;
    }

    public String getViewRootPath() {
        return viewRootPath;
    }

    /**
     * <code>View</code>のルートパスを設定します。
     * 
     * @param viewRootPath
     */
    public void setViewRootPath(final String viewRootPath) {
        this.viewRootPath = viewRootPath;
    }

    public String adjustViewRootPath() {
        return "/".equals(viewRootPath) ? "" : viewRootPath;
    }

    public String getSubApplicationRootPackageName() {
        return subApplicationRootPackageName;
    }

    /**
     * サブアプリケーションのルートパッケージ名を設定します。
     * 
     * @param subApplicationRootPackageName
     */
    public void setSubApplicationRootPackageName(
            final String subApplicationRootPackageName) {
        this.subApplicationRootPackageName = subApplicationRootPackageName;
    }

    public String[] getRootPackageNames() {
        return rootPackageNames;
    }

    /**
     * ルートパッケージ名を追加します。
     * 
     * @param rootPackageName
     *            ルートパッケージ
     */
    public void addRootPackageName(final String rootPackageName) {
        addRootPackageName(rootPackageName, true);
    }

    /**
     * ルートパッケージ名を追加します。
     * 
     * @param rootPackageName
     *            ルートパッケージ
     * @param hotdeploy
     *            HOT deployの対象なら<code>true</code>
     */
    public void addRootPackageName(final String rootPackageName,
            final boolean hotdeploy) {
        rootPackageNames = (String[]) ArrayUtil.add(rootPackageNames,
                rootPackageName);
        if (hotdeploy) {
            hotdeployRootPackageNames.add(rootPackageName);
        }
        addExistChecker(rootPackageName);
    }

    public String[] getIgnorePackageNames() {
        return ignorePackageNames;
    }

    /**
     * 無視するパッケージ名を追加します。
     * 
     * @param ignorePackageName
     */
    public void addIgnorePackageName(final String ignorePackageName) {
        ignorePackageNames = (String[]) ArrayUtil.add(ignorePackageNames,
                ignorePackageName);
    }

    /**
     * インターフェース名と実装クラス名の関連を追加します。
     * 
     * @param interfaceName
     * @param implementationClassName
     */
    public void addInterfaceToImplementationClassName(
            final String interfaceName, final String implementationClassName) {
        interfaceToImplementationMap
                .put(interfaceName, implementationClassName);
        implementationToInterfaceMap
                .put(implementationClassName, interfaceName);
    }

    public String fromSuffixToPackageName(final String suffix) {
        if (StringUtil.isEmpty(suffix)) {
            throw new EmptyRuntimeException("suffix");
        }
        return suffix.toLowerCase();
    }

    public String fromClassNameToShortComponentName(final String className) {
        if (StringUtil.isEmpty(className)) {
            throw new EmptyRuntimeException("className");
        }
        String s = StringUtil.decapitalize(ClassUtil
                .getShortClassName(className));
        if (s.endsWith(implementationSuffix)) {
            return s.substring(0, s.length() - implementationSuffix.length());
        }
        return s;
    }

    public String fromClassNameToComponentName(final String className) {
        if (StringUtil.isEmpty(className)) {
            throw new EmptyRuntimeException("className");
        }
        String cname = toInterfaceClassName(className);
        String suffix = fromClassNameToSuffix(cname);
        String middlePackageName = fromSuffixToPackageName(suffix);
        String name = null;
        for (int i = 0; i < rootPackageNames.length; ++i) {
            String prefix = rootPackageNames[i] + "." + middlePackageName + ".";
            if (cname.startsWith(prefix)) {
                name = cname.substring(prefix.length());
            }
        }
        if (StringUtil.isEmpty(name)) {
            for (int i = 0; i < rootPackageNames.length; ++i) {
                String prefix = rootPackageNames[i] + "."
                        + subApplicationRootPackageName + ".";
                if (cname.startsWith(prefix)) {
                    name = cname.substring(prefix.length());
                }
            }
            if (StringUtil.isEmpty(name)) {
                return fromClassNameToShortComponentName(className);
            }
        }
        String[] array = StringUtil.split(name, ".");
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < array.length; ++i) {
            if (i == array.length - 1) {
                buf.append(StringUtil.decapitalize(array[i]));
            } else {
                buf.append(array[i]);
                buf.append('_');
            }
        }
        return buf.toString();
    }

    public Class fromComponentNameToClass(final String componentName) {
        if (StringUtil.isEmpty(componentName)) {
            throw new EmptyRuntimeException("componentName");
        }
        String suffix = fromComponentNameToSuffix(componentName);
        if (suffix == null) {
            return null;
        }
        String middlePackageName = fromSuffixToPackageName(suffix);
        String partOfClassName = fromComponentNameToPartOfClassName(componentName);
        boolean subAppSuffix = isSubApplicationSuffix(suffix);
        for (int i = 0; i < rootPackageNames.length; ++i) {
            String rootPackageName = rootPackageNames[i];
            if (subAppSuffix) {
                Class clazz = findClass(rootPackageName,
                        subApplicationRootPackageName, partOfClassName);
                if (clazz != null) {
                    return clazz;
                }
                clazz = findClass(rootPackageName, middlePackageName,
                        partOfClassName);
                if (clazz != null) {
                    return clazz;
                }
            } else {
                Class clazz = findClass(rootPackageName, middlePackageName,
                        partOfClassName);
                if (clazz != null) {
                    return clazz;
                }
                clazz = findClass(rootPackageName,
                        subApplicationRootPackageName, partOfClassName);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

    /**
     * サブアプリケーションサフィックスかどうかを返します。
     * 
     * @param suffix
     *            サフィックス
     * @return サブアプリケーションサフィックスかどうか
     */
    protected boolean isSubApplicationSuffix(final String suffix) {
        if (pageSuffix.equals(suffix)) {
            return true;
        }
        if (dxoSuffix.equals(suffix)) {
            return true;
        }
        if (actionSuffix.equals(suffix)) {
            return true;
        }
        if (serviceSuffix.equals(suffix)) {
            return true;
        }
        return false;
    }

    /**
     * クラスを探します。
     * 
     * @param rootPackageName
     *            ルートパッケージ名
     * @param middlePackageName
     *            ミドルパッケージ名
     * @param partOfClassName
     *            クラス名の一部
     * @return クラス
     */
    protected Class findClass(final String rootPackageName,
            final String middlePackageName, final String partOfClassName) {
        initialize();

        final String backPartOfClassName = ClassUtil.concatName(
                middlePackageName, partOfClassName);
        final String className = ClassUtil.concatName(rootPackageName,
                backPartOfClassName);
        final String backPartOfImplClassName = toImplementationClassName(backPartOfClassName);
        final String implClassName = ClassUtil.concatName(rootPackageName,
                backPartOfImplClassName);

        if (!isIgnoreClassName(implClassName)
                && isExist(rootPackageName, backPartOfImplClassName)) {
            return ClassUtil.forName(implClassName);
        }

        if (!isIgnoreClassName(className)
                && isExist(rootPackageName, backPartOfClassName)) {
            return ClassUtil.forName(className);
        }
        return null;
    }

    public String toImplementationClassName(final String className) {
        String implementationClassName = (String) interfaceToImplementationMap
                .get(className);
        if (implementationClassName != null) {
            return implementationClassName;
        }
        int index = className.lastIndexOf('.');
        if (index < 0) {
            return getImplementationPackageName() + "." + className
                    + implementationSuffix;
        }
        return className.substring(0, index) + "."
                + getImplementationPackageName() + "."
                + className.substring(index + 1) + implementationSuffix;
    }

    public String toInterfaceClassName(final String className) {
        String interfaceClassName = (String) implementationToInterfaceMap
                .get(className);
        if (interfaceClassName != null) {
            return interfaceClassName;
        }
        if (!className.endsWith(implementationSuffix)) {
            return className;
        }
        String key = "." + getImplementationPackageName() + ".";
        int index = className.lastIndexOf(key);
        if (index < 0) {
            throw new IllegalArgumentException(className);
        }
        return className.substring(0, index)
                + "."
                + className.substring(index + key.length(), className.length()
                        - implementationSuffix.length());
    }

    public boolean isSkipClass(final Class clazz) {
        if (clazz.isInterface()) {
            return false;
        }
        for (final Iterator it = interfaceToImplementationMap.entrySet()
                .iterator(); it.hasNext();) {
            final Entry entry = (Entry) it.next();
            final Class interfaceClass = ClassUtil.forName((String) entry
                    .getKey());
            if (interfaceClass.isAssignableFrom(clazz)) {
                return true;
            }
        }
        return false;
    }

    public Class toCompleteClass(final Class clazz) {
        if (!clazz.isInterface()) {
            return clazz;
        }
        String className = toImplementationClassName(clazz.getName());
        if (ResourceUtil.isExist(ClassUtil.getResourcePath(className))) {
            return ClassUtil.forName(className);
        }
        return clazz;
    }

    public String fromComponentNameToPartOfClassName(final String componentName) {
        if (componentName == null) {
            throw new EmptyRuntimeException("componentName");
        }
        String[] names = StringUtil.split(componentName, PACKAGE_SEPARATOR_STR);
        StringBuffer buf = new StringBuffer(50);
        for (int i = 0; i < names.length; ++i) {
            if (i == names.length - 1) {
                buf.append(StringUtil.capitalize(names[i]));
            } else {
                buf.append(names[i]).append(".");
            }
        }
        return buf.toString();
    }

    public String fromComponentNameToSuffix(final String componentName) {
        return fromNameToSuffix(componentName);
    }

    public String fromClassNameToSuffix(final String componentName) {
        return fromNameToSuffix(componentName);
    }

    /**
     * 名前をサフィックスに変換します。
     * 
     * @param name
     *            名前
     * @return サフィックス
     */
    protected String fromNameToSuffix(final String name) {
        if (StringUtil.isEmpty(name)) {
            throw new EmptyRuntimeException("name");
        }
        for (int i = name.length() - 1; i >= 0; --i) {
            if (Character.isUpperCase(name.charAt(i))) {
                return name.substring(i);
            }
        }
        return null;
    }

    public boolean isValidViewRootPath(final String path) {
        if (!path.startsWith(viewRootPath) || !path.endsWith(viewExtension)) {
            return false;
        }
        return true;
    }

    public String fromPathToPageName(final String path) {
        return fromPathToComponentName(path, pageSuffix);
    }

    /**
     * パスをコンポーネント名に変換します。
     * 
     * @param path
     *            パス
     * @param nameSuffix
     *            サフィックス
     * @return コンポーネント名
     */
    protected String fromPathToComponentName(final String path,
            final String nameSuffix) {
        if (!path.startsWith(viewRootPath) || !path.endsWith(viewExtension)) {
            throw new IllegalArgumentException(path);
        }
        String componentName = (path.substring(
                adjustViewRootPath().length() + 1, path.length()
                        - viewExtension.length()) + nameSuffix).replace('/',
                '_');
        int pos = componentName.lastIndexOf('_');
        if (pos == -1) {
            return StringUtil.decapitalize(componentName);
        }
        return componentName.substring(0, pos + 1)
                + StringUtil.decapitalize(componentName.substring(pos + 1));
    }

    public String fromPathToActionName(final String path) {
        return fromPathToComponentName(path, actionSuffix);
    }

    public String fromPageNameToPath(final String pageName) {
        if (!pageName.endsWith(pageSuffix)) {
            throw new IllegalArgumentException(pageName);
        }
        String name = pageName.substring(0, pageName.length()
                - pageSuffix.length());
        return adjustViewRootPath() + "/"
                + name.replace(PACKAGE_SEPARATOR, '/') + viewExtension;
    }

    public String fromPageClassToPath(final Class pageClass) {
        String componentName = fromClassNameToComponentName(pageClass.getName());
        return fromPageNameToPath(componentName);
    }

    public String fromActionNameToPath(final String actionName) {
        if (!actionName.endsWith(actionSuffix)) {
            throw new IllegalArgumentException(actionName);
        }
        String name = actionName.substring(0, actionName.length()
                - actionSuffix.length());
        return adjustViewRootPath() + "/"
                + name.replace(PACKAGE_SEPARATOR, '/') + viewExtension;
    }

    public String fromActionNameToPageName(final String actionName) {
        if (!actionName.endsWith(actionSuffix)) {
            throw new IllegalArgumentException(actionName);
        }
        return actionName.substring(0, actionName.length()
                - actionSuffix.length())
                + pageSuffix;
    }

    public boolean isTargetClassName(final String className, final String suffix) {
        if (isIgnoreClassName(className)) {
            return false;
        }
        if (!StringUtil.trimSuffix(className, implementationSuffix).endsWith(
                suffix)) {
            return false;
        }
        final String shortClassName = ClassUtil.getShortClassName(className);
        if (className.endsWith(implementationSuffix)
                && !className.endsWith("." + getImplementationPackageName()
                        + "." + shortClassName)) {
            return false;
        }
        final String middlePkgName = fromSuffixToPackageName(suffix);
        for (int i = 0; i < rootPackageNames.length; ++i) {
            if (className.startsWith(rootPackageNames[i] + "." + middlePkgName
                    + ".")) {
                return true;
            }
            if (className.startsWith(rootPackageNames[i] + "."
                    + subApplicationRootPackageName + ".")) {
                return true;
            }
        }
        return false;
    }

    public boolean isTargetClassName(final String className) {
        if (isIgnoreClassName(className)) {
            return false;
        }
        for (int i = 0; i < rootPackageNames.length; ++i) {
            if (className.startsWith(rootPackageNames[i] + ".")) {
                return true;
            }
        }
        return false;
    }

    public boolean isHotdeployTargetClassName(final String className) {
        if (isIgnoreClassName(className)) {
            return false;
        }
        for (int i = 0; i < rootPackageNames.length; ++i) {
            if (className.startsWith(rootPackageNames[i] + ".")) {
                return hotdeployRootPackageNames.contains(rootPackageNames[i]);
            }
        }
        return false;
    }

    public boolean isIgnoreClassName(final String className) {
        for (int i = 0; i < ignorePackageNames.length; ++i) {
            if (className.startsWith(ignorePackageNames[i] + ".")) {
                return true;
            }
        }
        return false;
    }

    /**
     * 存在するかどうかを返します。
     * 
     * @param rootPackageName
     *            ルートパッケージ名
     * @param lastClassName
     *            クラス名の最後
     * @return 存在するかどうか
     */
    protected boolean isExist(final String rootPackageName,
            final String lastClassName) {
        final Resources[] checkerArray = getExistCheckerArray(rootPackageName);
        for (int i = 0; i < checkerArray.length; ++i) {
            if (checkerArray[i].isExistClass(lastClassName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 存在チェッカの配列を返します。
     * 
     * @param rootPackageName
     *            ルートパッケージ名
     * @return 存在チェッカの配列
     */
    protected Resources[] getExistCheckerArray(final String rootPackageName) {
        return (Resources[]) existCheckerArrays.get(rootPackageName);
    }

    /**
     * 存在チェッカを追加します。
     * 
     * @param rootPackageName
     *            ルートパッケージ名
     */
    protected void addExistChecker(final String rootPackageName) {
        Resources[] checkerArray = ResourcesUtil
                .getResourcesTypes(rootPackageName);
        existCheckerArrays.put(rootPackageName, checkerArray);
    }

}
