/*
 * 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.extension.dxo.converter.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import org.seasar.extension.dxo.converter.Converter;
import org.seasar.extension.dxo.converter.ConverterFactory;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.Disposable;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.MapUtil;

/**
 * コンバータファクトリの実装クラスです。
 * 
 * @author Satoshi Kimura
 * @author koichik
 */
public class ConverterFactoryImpl implements ConverterFactory, Disposable {

    /** プリミティブ型の配列クラスとラッパー型の配列クラスのマッピング */
    protected static Map PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY = new HashMap();
    static {
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(boolean[].class, Boolean[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(char[].class, Character[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(byte[].class, Byte[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(short[].class, Short[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(int[].class, Integer[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(long[].class, Long[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(float[].class, Float[].class);
        PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY.put(double[].class, Double[].class);
    }

    /** インスタンスが初期化済みであることを示します。 */
    protected volatile boolean initialized;

    /** このファクトリを管理しているS2コンテナです。 */
    protected S2Container container;

    /** S2コンテナに登録されているコンバータの配列です。 */
    protected Converter[] converters;

    /** コンバータのキャッシュです。 */
    protected final Map converterCache = MapUtil.createHashMap();

    /**
     * <code>ConverterFactoryImpl</code>のインスタンスを構築します。
     * 
     */
    public ConverterFactoryImpl() {
    }

    /**
     * S2コンテナを設定します。
     * 
     * @param container
     *            S2コンテナ
     */
    public void setContainer(final S2Container container) {
        this.container = container.getRoot();
    }

    /**
     * インスタンスを初期化します。
     * 
     */
    public void initialize() {
        if (initialized) {
            return;
        }
        converters = (Converter[]) container.findAllComponents(Converter.class);
        DisposableUtil.add(this);
        initialized = true;
    }

    /**
     * キャッシュ情報等を破棄し、インスタンスを未初期化状態に戻します。
     * 
     */
    public void dispose() {
        converters = null;
        converterCache.clear();
        initialized = false;
    }

    public Converter getConverter(final Class sourceClass, final Class destClass) {
        initialize();
        final Class destType = ClassUtil.getWrapperClassIfPrimitive(destClass);
        final String cacheKey = sourceClass.getName() + destType.getName();
        final Converter converter = (Converter) converterCache.get(cacheKey);
        if (converter != null) {
            return converter;
        }
        return detectConverter(sourceClass, destType);
    }

    private Converter detectConverter(final Class sourceClass,
            final Class destClass) {
        final Converter converter = getDistanceTable(sourceClass, destClass);
        final String cacheKey = sourceClass.getName() + destClass.getName();
        converterCache.put(cacheKey, converter);
        return converter;
    }

    private Converter getDistanceTable(final Class sourceClass,
            final Class destClass) {
        final Map distanceTable = new TreeMap();
        for (int i = 0; i < converters.length; ++i) {
            final Converter converter = converters[i];
            if (!canConvert(sourceClass, destClass, converter)) {
                continue;
            }
            final double distance = getDistance(converter.getDestClass(),
                    destClass);
            distanceTable.put(new Double(distance), converter);
        }
        if (distanceTable.isEmpty()) {
            throw new IllegalStateException("converter　was not found."); // TODO
        }
        return (Converter) distanceTable.values().iterator().next();
    }

    private boolean canConvert(final Class sourceClass, final Class destClass,
            final Converter converter) {
        if (!converter.getDestClass().isAssignableFrom(destClass)) {
            final Class wrapperArray = (Class) PRIMITIVE_ARRAY_TO_WRAPPER_ARRAY
                    .get(destClass);
            if (wrapperArray != null) {
                return canConvert(sourceClass, wrapperArray, converter);
            }
            return false;
        }

        final Class[] sourceClasses = converter.getSourceClasses();
        for (int i = 0; i < sourceClasses.length; i++) {
            if (sourceClasses[i].isAssignableFrom(sourceClass)) {
                return true;
            }
        }

        return false;
    }

    private double getDistance(final Class assigner, final Class assignee) {
        if (assignee.isArray() && assigner.isArray()) {
            return getDistance(assigner.getComponentType(), assignee
                    .getComponentType(), 0);
        } else if (assignee.isArray()) {
            return getDistance(assigner, assignee, 10); // TODO 一方だけ配列の時のクラス間の距離
            // 一先ず10にしておく
        }
        return getDistance(assigner, assignee, 0);
    }

    private double getDistance(final Class assigner, final Class assignee,
            final double distance) {
        if (assignee.equals(assigner)) {
            return distance;
        }
        if (assigner.getName().equals("java.lang.Enum")
                && TigerSupport.instance.isEnum(assignee)) {
            return distance + 0.5;
        }
        if (isImplements(assigner, assignee)) {
            return distance + 0.5;
        }

        final Class superClass = assigner.getSuperclass();
        if (superClass == null) {
            return distance + 1;
        }
        return getDistance(superClass, assignee, distance + 1);
    }

    private boolean isImplements(final Class assigner, final Class assignee) {
        return !assigner.isInterface() && assignee.isInterface()
                && assignee.isAssignableFrom(assigner);
    }

}
