/*
 * 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.jdbc.meta;

import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;

import org.seasar.extension.jdbc.EntityMeta;
import org.seasar.extension.jdbc.EntityMetaFactory;
import org.seasar.extension.jdbc.JoinColumnMeta;
import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.PropertyMetaFactory;
import org.seasar.extension.jdbc.RelationshipType;
import org.seasar.extension.jdbc.TableMeta;
import org.seasar.extension.jdbc.TableMetaFactory;
import org.seasar.extension.jdbc.exception.FieldDuplicatedRuntimeException;
import org.seasar.extension.jdbc.exception.JoinColumnAutoConfigurationRuntimeException;
import org.seasar.extension.jdbc.exception.JoinColumnNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.ManyToOneFKNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.MappedByNotIdenticalRuntimeException;
import org.seasar.extension.jdbc.exception.MappedByPropertyNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.NonEntityRuntimeException;
import org.seasar.extension.jdbc.exception.OneToOneFKNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.ReferencedColumnNameNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.UnsupportedInheritanceRuntimeException;
import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.framework.container.annotation.tiger.BindingType;
import org.seasar.framework.container.annotation.tiger.InitMethod;
import org.seasar.framework.convention.PersistenceConvention;
import org.seasar.framework.util.ArrayMap;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.Disposable;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.ModifierUtil;
import org.seasar.framework.util.StringUtil;

/**
 * @author higa
 * 
 */
public class EntityMetaFactoryImpl implements EntityMetaFactory {

    /**
     * エンティティメタデータのマップです。
     */
    protected ConcurrentHashMap<String, EntityMeta> entityMetaMap = new ConcurrentHashMap<String, EntityMeta>(
            200);

    /**
     * テーブルメタデータのファクトリです。
     */
    protected TableMetaFactory tableMetaFactory;

    /**
     * プロパティメタデータのファクトリです。
     */
    protected PropertyMetaFactory propertyMetaFactory;

    /**
     * 初期化されたかどうかです。
     */
    protected volatile boolean initialized;

    /**
     * 永続化層の規約です。
     */
    protected PersistenceConvention persistenceConvention;

    public EntityMeta getEntityMeta(Class<?> entityClass) {
        if (entityClass == null) {
            throw new NullPointerException("entityClass");
        }
        if (!initialized) {
            initialize();
        }
        EntityMeta entityMeta = getEntityMetaInternal(entityClass);
        if (!entityMeta.isRelationshipResolved()) {
            synchronized (entityMeta) {
                if (!entityMeta.isRelationshipResolved()) {
                    resolveRelationship(entityMeta);
                }
            }
        }
        return entityMeta;
    }

    /**
     * 内部的に使われるエンティティメタデータを返すメソッドです。
     * 
     * @param entityClass
     *            エンティティクラス
     * @return エンティティメタデータ
     */
    protected EntityMeta getEntityMetaInternal(Class<?> entityClass) {
        String className = entityClass.getName();
        EntityMeta entityMeta = entityMetaMap.get(className);
        if (entityMeta != null) {
            return entityMeta;
        }
        entityMeta = createEntityMeta(entityClass);
        EntityMeta entityMeta2 = entityMetaMap.putIfAbsent(className,
                entityMeta);
        return entityMeta2 != null ? entityMeta2 : entityMeta;
    }

    /**
     * エンティティメタデータを作成します。
     * 
     * @param entityClass
     *            エンティティクラス
     * @return エンティティメタデータ
     * @throws NonEntityRuntimeException
     *             クラスがエンティティではない場合。
     */
    protected EntityMeta createEntityMeta(Class<?> entityClass)
            throws NonEntityRuntimeException {
        Entity entity = entityClass.getAnnotation(Entity.class);
        if (entity == null) {
            throw new NonEntityRuntimeException(entityClass);
        }
        EntityMeta entityMeta = new EntityMeta();
        doEntityClass(entityMeta, entityClass);
        doName(entityMeta, entityClass, entity);
        doTableMeta(entityMeta, entityClass);
        doPropertyMeta(entityMeta, entityClass);
        doCustomize(entityMeta, entityClass);
        return entityMeta;
    }

    /**
     * エンティティクラスを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param entityClass
     *            エンティティクラス
     */
    protected void doEntityClass(EntityMeta entityMeta, Class<?> entityClass) {
        entityMeta.setEntityClass(entityClass);
    }

    /**
     * 名前を処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param entityClass
     *            エンティティクラス
     * @param entity
     *            エンティティアノテーション
     */
    protected void doName(EntityMeta entityMeta, Class<?> entityClass,
            Entity entity) {
        if (!StringUtil.isEmpty(entity.name())) {
            entityMeta.setName(entity.name());
        } else {
            String entityName = fromClassToEntityName(entityClass);
            entityMeta.setName(entityName);
        }
    }

    /**
     * クラスをエンティティ名に変換します。
     * 
     * @param entityClass
     *            エンティティクラス
     * @return エンティティ名
     */
    protected String fromClassToEntityName(Class<?> entityClass) {
        return ClassUtil.getShortClassName(entityClass);
    }

    /**
     * テーブルメタデータを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param entityClass
     *            エンティティクラス
     */
    protected void doTableMeta(EntityMeta entityMeta, Class<?> entityClass) {
        TableMeta tableMeta = tableMetaFactory.createTableMeta(entityClass,
                entityMeta);
        entityMeta.setTableMeta(tableMeta);
    }

    /**
     * プロパティメタデータを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param entityClass
     *            エンティティクラス
     */
    protected void doPropertyMeta(EntityMeta entityMeta, Class<?> entityClass) {
        Field[] fields = getFields(entityClass);
        for (Field f : fields) {
            f.setAccessible(true);
            entityMeta.addPropertyMeta(propertyMetaFactory.createPropertyMeta(
                    f, entityMeta));
        }
    }

    /**
     * フィールドの配列を返します。
     * 
     * @param entityClass
     *            エンティティクラス
     * @return フィールドの配列
     */
    protected Field[] getFields(Class<?> entityClass) {
        ArrayMap fields = new ArrayMap();
        for (Field f : ClassUtil.getDeclaredFields(entityClass)) {
            if (!ModifierUtil.isInstanceField(f)) {
                continue;
            }
            fields.put(f.getName(), f);
        }

        for (Class<?> clazz = entityClass.getSuperclass(); clazz != Object.class; clazz = clazz
                .getSuperclass()) {
            if (clazz.isAnnotationPresent(Entity.class)) {
                throw new UnsupportedInheritanceRuntimeException(entityClass);
            }
            if (clazz.isAnnotationPresent(MappedSuperclass.class)) {
                for (Field f : ClassUtil.getDeclaredFields(clazz)) {
                    if (!ModifierUtil.isInstanceField(f)) {
                        continue;
                    }
                    String name = f.getName();
                    if (!fields.containsKey(name)) {
                        fields.put(name, f);
                    } else {
                        throw new FieldDuplicatedRuntimeException(f);
                    }
                }
            }
        }
        return (Field[]) fields.toArray(new Field[fields.size()]);
    }

    /**
     * カスタマイズします。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param entityClass
     *            エンティティクラス
     */
    @SuppressWarnings("unused")
    protected void doCustomize(EntityMeta entityMeta, Class<?> entityClass) {
    }

    /**
     * 関連を解決します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     */
    protected void resolveRelationship(EntityMeta entityMeta) {
        int size = entityMeta.getPropertyMetaSize();
        for (int i = 0; i < size; i++) {
            PropertyMeta propertyMeta = entityMeta.getPropertyMeta(i);
            if (!propertyMeta.isRelationship()) {
                continue;
            }
            if (propertyMeta.getMappedBy() != null) {
                checkMappedBy(propertyMeta, entityMeta);
            }
            if (propertyMeta.getRelationshipType() == RelationshipType.MANY_TO_ONE
                    || propertyMeta.getRelationshipType() == RelationshipType.ONE_TO_ONE
                    && propertyMeta.getMappedBy() == null) {
                resolveJoinColumn(entityMeta, propertyMeta);
            }
        }
        entityMeta.setRelationshipResolved(true);
    }

    /**
     * mappedByで指定されたプロパティが存在するかチェックします。
     * 
     * @param propertyMeta
     *            プロパティメタデータ
     * @param entityMeta
     *            エンティティメタデータ
     */
    protected void checkMappedBy(PropertyMeta propertyMeta,
            EntityMeta entityMeta) {
        String mappedBy = propertyMeta.getMappedBy();
        Class<?> relationshipClass = propertyMeta.getRelationshipClass();
        EntityMeta relationshipEntityMeta = getEntityMetaInternal(relationshipClass);
        if (relationshipEntityMeta.hasPropertyMeta(mappedBy)) {
            Field f = relationshipEntityMeta.getPropertyMeta(mappedBy)
                    .getField();
            if (entityMeta.getEntityClass() != f.getType()) {
                throw new MappedByNotIdenticalRuntimeException(entityMeta
                        .getName(), propertyMeta.getName(), propertyMeta
                        .getMappedBy(), entityMeta.getEntityClass(),
                        propertyMeta.getRelationshipClass(), propertyMeta
                                .getMappedBy(), f.getType());
            }
        } else {
            throw new MappedByPropertyNotFoundRuntimeException(entityMeta
                    .getName(), propertyMeta.getName(), propertyMeta
                    .getMappedBy(), propertyMeta.getRelationshipClass());
        }
    }

    /**
     * 関連のJoinColumnを解決します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     */
    protected void resolveJoinColumn(EntityMeta entityMeta,
            PropertyMeta propertyMeta) {
        List<JoinColumnMeta> jcmList = propertyMeta.getJoinColumnMetaList();
        EntityMeta inverseEntityMeta = getEntityMetaInternal(propertyMeta
                .getRelationshipClass());
        if (jcmList.size() == 0) {
            if (inverseEntityMeta.getIdPropertyMetaList().size() != 1) {
                throw new JoinColumnNotFoundRuntimeException(entityMeta
                        .getName(), propertyMeta.getName());
            }
            propertyMeta.addJoinColumnMeta(new JoinColumnMeta());
        }
        if (jcmList.size() == 1) {
            JoinColumnMeta joinColumnMeta = jcmList.get(0);
            PropertyMeta inverseIdPropertyMeta = null;
            if (inverseEntityMeta.getIdPropertyMetaList().size() == 1) {
                inverseIdPropertyMeta = inverseEntityMeta
                        .getIdPropertyMetaList().get(0);
            }
            if (joinColumnMeta.getName() == null) {
                if (inverseIdPropertyMeta == null) {
                    throw new JoinColumnAutoConfigurationRuntimeException(
                            entityMeta.getName(), propertyMeta.getName(),
                            inverseEntityMeta.getName());
                }
                joinColumnMeta
                        .setName(persistenceConvention
                                .fromPropertyNameToColumnName(propertyMeta
                                        .getName())
                                + "_"
                                + inverseIdPropertyMeta.getColumnMeta()
                                        .getName());
            }
            if (joinColumnMeta.getReferencedColumnName() == null) {
                if (inverseIdPropertyMeta == null) {
                    throw new JoinColumnAutoConfigurationRuntimeException(
                            entityMeta.getName(), propertyMeta.getName(),
                            inverseEntityMeta.getName());
                }
                joinColumnMeta.setReferencedColumnName(inverseIdPropertyMeta
                        .getColumnMeta().getName());
            }
        }
        for (JoinColumnMeta jcm : jcmList) {
            if (!entityMeta.hasColumnPropertyMeta(jcm.getName())) {
                if (propertyMeta.getRelationshipType() == RelationshipType.MANY_TO_ONE) {
                    throw new ManyToOneFKNotFoundRuntimeException(entityMeta
                            .getName(), propertyMeta.getName(), jcm.getName());
                }
                throw new OneToOneFKNotFoundRuntimeException(entityMeta
                        .getName(), propertyMeta.getName(), jcm.getName());
            }
            if (!inverseEntityMeta.hasColumnPropertyMeta(jcm
                    .getReferencedColumnName())) {
                throw new ReferencedColumnNameNotFoundRuntimeException(
                        entityMeta.getName(), propertyMeta.getName(),
                        inverseEntityMeta.getName(), jcm
                                .getReferencedColumnName());
            }
        }
    }

    /**
     * テーブルメタデータファクトリを設定します。
     * 
     * @param tableMetaFactory
     *            テーブルメタデータファクトリ
     */
    @Binding(bindingType = BindingType.MUST)
    public void setTableMetaFactory(TableMetaFactory tableMetaFactory) {
        this.tableMetaFactory = tableMetaFactory;
    }

    /**
     * プロパティメタデータファクトリを設定します。
     * 
     * @param propertyMetaFactory
     *            プロパティメタデータファクトリ
     */
    @Binding(bindingType = BindingType.MUST)
    public void setPropertyMetaFactory(PropertyMetaFactory propertyMetaFactory) {
        this.propertyMetaFactory = propertyMetaFactory;
    }

    /**
     * 永続化層の規約を設定します
     * 
     * @param persistenceConvention
     *            永続化層の規約
     */
    @Binding(bindingType = BindingType.MUST)
    public void setPersistenceConvention(
            PersistenceConvention persistenceConvention) {
        this.persistenceConvention = persistenceConvention;
    }

    /**
     * 初期化を行ないます。
     */
    @InitMethod
    public void initialize() {
        DisposableUtil.add(new Disposable() {

            public void dispose() {
                clear();
            }
        });
        initialized = true;
    }

    /**
     * キャッシュをクリアします。
     */
    public void clear() {
        entityMetaMap.clear();
        initialized = false;
    }
}