/*
 * 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.gen.internal.desc;

import java.lang.reflect.Field;

import javax.persistence.Column;
import javax.persistence.GenerationType;

import org.seasar.extension.jdbc.ColumnMeta;
import org.seasar.extension.jdbc.EntityMeta;
import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.gen.desc.ColumnDesc;
import org.seasar.extension.jdbc.gen.desc.ColumnDescFactory;
import org.seasar.extension.jdbc.gen.dialect.GenDialect;
import org.seasar.extension.jdbc.gen.internal.exception.NullableUniqueNotSupportedRuntimeException;
import org.seasar.extension.jdbc.gen.internal.exception.UnsupportedGenerationTypeRuntimeException;
import org.seasar.extension.jdbc.gen.internal.util.AnnotationUtil;
import org.seasar.extension.jdbc.gen.internal.util.PropertyMetaUtil;
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider;
import org.seasar.extension.jdbc.gen.sqltype.SqlType;
import org.seasar.framework.util.StringUtil;

/**
 * {@link ColumnDescFactory}の実装クラスです。
 * 
 * @author taedium
 */
public class ColumnDescFactoryImpl implements ColumnDescFactory {

    /** 方言 */
    protected GenDialect dialect;

    /** {@link ValueType}の提供者 */
    protected ValueTypeProvider valueTypeProvider;

    /**
     * インスタンスを構築します。
     * 
     * @param dialect
     *            方言
     * @param valueTypeProvider
     *            {@link ValueType}の提供者
     */
    public ColumnDescFactoryImpl(GenDialect dialect,
            ValueTypeProvider valueTypeProvider) {
        if (dialect == null) {
            throw new NullPointerException("dialect");
        }
        if (valueTypeProvider == null) {
            throw new NullPointerException("valueTypeResolver");
        }
        this.dialect = dialect;
        this.valueTypeProvider = valueTypeProvider;
    }

    public ColumnDesc getColumnDesc(EntityMeta entityMeta,
            PropertyMeta propertyMeta) {
        if (propertyMeta.isTransient() || propertyMeta.isRelationship()) {
            return null;
        }
        Column column = getColumn(propertyMeta);
        ColumnDesc columnDesc = new ColumnDesc();
        doName(entityMeta, propertyMeta, columnDesc, column);
        doComment(entityMeta, propertyMeta, columnDesc, column);
        doIdentity(entityMeta, propertyMeta, columnDesc, column);
        doDefinition(entityMeta, propertyMeta, columnDesc, column);
        doNullable(entityMeta, propertyMeta, columnDesc, column);
        doUnique(entityMeta, propertyMeta, columnDesc, column);
        doSqlType(entityMeta, propertyMeta, columnDesc, column);
        return columnDesc;
    }

    /**
     * 名前を処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doName(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        ColumnMeta columnMeta = propertyMeta.getColumnMeta();
        columnDesc.setName(columnMeta.getName());
    }

    /**
     * コメントを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doComment(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        String comment = PropertyMetaUtil.getComment(propertyMeta);
        columnDesc.setComment(comment);
    }

    /**
     * IDENTITYカラムを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doIdentity(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        GenerationType generationType = propertyMeta.getGenerationType();
        if (generationType == GenerationType.AUTO) {
            generationType = dialect.getDefaultGenerationType();
        }
        if (generationType == GenerationType.IDENTITY) {
            if (!dialect.supportsIdentity()) {
                throw new UnsupportedGenerationTypeRuntimeException(
                        GenerationType.IDENTITY, entityMeta.getName(),
                        propertyMeta.getName());
            }
            columnDesc.setIdentity(true);
        }
    }

    /**
     * 定義を処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doDefinition(EntityMeta entityMeta,
            PropertyMeta propertyMeta, ColumnDesc columnDesc, Column column) {
        SqlType type = dialect.getSqlType(valueTypeProvider, propertyMeta);
        String dataType = type.getDataType(column.length(), column.precision(),
                column.scale(), columnDesc.isIdentity());

        String definition;
        if (!StringUtil.isEmpty(column.columnDefinition())) {
            if (StringUtil.startsWithIgnoreCase(column.columnDefinition(),
                    "default ")) {
                definition = dataType + " " + column.columnDefinition();
            } else if (StringUtil.startsWithIgnoreCase(column
                    .columnDefinition(), "check ")) {
                definition = dataType + " " + column.columnDefinition();
            } else {
                definition = column.columnDefinition();
            }
        } else {
            definition = dataType;
        }
        columnDesc.setDefinition(definition);
    }

    /**
     * NULL可能かどうかを処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doNullable(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        if (propertyMeta.isId()) {
            columnDesc.setNullable(false);
        } else if (column != AnnotationUtil.getDefaultColumn()) {
            columnDesc.setNullable(column.nullable());
        } else {
            Class<?> clazz = propertyMeta.getField().getType();
            columnDesc.setNullable(!clazz.isPrimitive());
        }
    }

    /**
     * 一意性を処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doUnique(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        columnDesc.setUnique(column.unique());
        if (!dialect.supportsNullableUnique()) {
            if (columnDesc.isNullable() && columnDesc.isUnique()) {
                throw new NullableUniqueNotSupportedRuntimeException(entityMeta
                        .getName(), propertyMeta.getName());
            }
        }
    }

    /**
     * SQL型を処理します。
     * 
     * @param entityMeta
     *            エンティティメタデータ
     * @param propertyMeta
     *            プロパティメタデータ
     * @param columnDesc
     *            カラム記述
     * @param column
     *            カラム
     */
    protected void doSqlType(EntityMeta entityMeta, PropertyMeta propertyMeta,
            ColumnDesc columnDesc, Column column) {
        SqlType sqlType = dialect.getSqlType(valueTypeProvider, propertyMeta);
        columnDesc.setSqlType(sqlType);
    }

    /**
     * カラムを返します。
     * 
     * @param propertyMeta
     *            プロパティメタデータ
     * @return カラム
     */
    protected Column getColumn(PropertyMeta propertyMeta) {
        Field field = propertyMeta.getField();
        Column column = field.getAnnotation(Column.class);
        return column != null ? column : AnnotationUtil.getDefaultColumn();
    }

}
