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

import javax.persistence.TableGenerator;
import javax.sql.DataSource;

import org.seasar.extension.jdbc.EntityMeta;
import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.SqlLogger;
import org.seasar.extension.jdbc.exception.IdGenerationFailedRuntimeException;
import org.seasar.extension.jdbc.handler.ObjectResultSetHandler;
import org.seasar.extension.jdbc.impl.BasicSelectHandler;
import org.seasar.extension.jdbc.impl.BasicUpdateHandler;
import org.seasar.extension.jdbc.manager.JdbcManagerImplementor;
import org.seasar.extension.jdbc.types.LongType;
import org.seasar.extension.tx.TransactionCallback;
import org.seasar.extension.tx.TransactionManagerAdapter;
import org.seasar.framework.container.SingletonS2Container;
import org.seasar.framework.util.StringUtil;

/**
 * @author koichik
 */
public class TableIdGenerator extends AbstractPreAllocateIdGenerator {

    /** デフォルトの採番テーブル名 */
    public static final String DEFAULT_TABLE = "ID_GENERATOR";

    /** デフォルトの採番テーブルの識別子のカラム名 */
    public static final String DEFAULT_PK_COLUMN_NAME = "PK";

    /** デフォルトの採番テーブルの値のカラム名 */
    public static final String DEFAULT_VALUE_COLUMN_NAME = "VALUE";

    /** 識別子に付けられたアノテーション */
    protected TableGenerator tableGenerator;

    /** 採番テーブルのカタログ名 */
    protected String catalog;

    /** 採番テーブルのスキーマ名 */
    protected String schema;

    /** 採番テーブル名 */
    protected String table;

    /** 採番テーブルの識別子のカラム名 */
    protected String pkColumnName;

    /** 採番テーブルの識別子の値 */
    protected String pkColumnValue;

    /** 採番テーブルの値のカラム名 */
    protected String valueColumnName;

    /** 採番テーブルを更新するSQL */
    protected String updateSql;

    /** 採番テーブルから値を取得するするSQL */
    protected String selectSql;

    /**
     * インスタンスを構築します。
     * 
     * @param entityMeta
     *            エンティティのメタデータ
     * @param propertyMeta
     *            識別子を表すプロパティのメタデータ
     * @param tableGenerator
     *            識別子に付けられたアノテーション
     */
    public TableIdGenerator(final EntityMeta entityMeta,
            final PropertyMeta propertyMeta, final TableGenerator tableGenerator) {
        super(entityMeta, propertyMeta, tableGenerator.allocationSize());
        this.tableGenerator = tableGenerator;
        catalog = getCatalog();
        schema = getSchema();
        table = getTable();
        pkColumnName = getPkColumnName();
        pkColumnValue = getPkColumnValue();
        valueColumnName = getValueColumnName();
        updateSql = createUpdateSql();
        selectSql = createSelectSql();
    }

    @Override
    protected long getNewInitialValue(final JdbcManagerImplementor jdbcManager,
            final SqlLogger sqlLogger) {
        try {
            final TransactionManagerAdapter txAdapter = SingletonS2Container
                    .getComponent(TransactionManagerAdapter.class);
            final Object result = txAdapter
                    .requiresNew(new TransactionCallback() {

                        public Object execute(
                                final TransactionManagerAdapter adapter)
                                throws Throwable {
                            return updateIdTable(jdbcManager);
                        }

                    });
            return Number.class.cast(result).longValue() - allocationSize;
        } catch (final IdGenerationFailedRuntimeException e) {
            throw e;
        } catch (final Throwable t) {
            throw new IdGenerationFailedRuntimeException(entityMeta.getName(),
                    propertyMeta.getName(), t);
        }
    }

    /**
     * 採番テーブルを更新して次の識別子の値を返します。
     * <p>
     * このメソッドはエンティティのINSERTとは独立したトランザクションで実行されます。
     * </p>
     * 
     * @param jdbcManager
     *            内部的なJDBCマネージャ
     * @return 次の識別子の値
     */
    protected Number updateIdTable(final JdbcManagerImplementor jdbcManager) {
        final DataSource ds = jdbcManager.getDataSource();
        final BasicUpdateHandler updateHandler = new BasicUpdateHandler(ds,
                updateSql);
        final int rows = updateHandler.execute(new Object[] { allocationSize,
                pkColumnValue });
        if (rows != 1) {
            throw new IdGenerationFailedRuntimeException(entityMeta.getName(),
                    propertyMeta.getName());
        }

        final BasicSelectHandler selectHandler = new BasicSelectHandler(ds,
                selectSql,
                new ObjectResultSetHandler(new LongType(), selectSql));
        final Object result = selectHandler.execute(
                new Object[] { pkColumnValue }, new Class[] { String.class });
        if (result == null || !Number.class.isInstance(result)) {
            throw new IdGenerationFailedRuntimeException(entityMeta.getName(),
                    propertyMeta.getName());
        }
        return Number.class.cast(result);
    }

    /**
     * 採番テーブルのカタログ名を返します。
     * 
     * @return 採番テーブルのカタログ名
     */
    protected String getCatalog() {
        final String catalog = tableGenerator.catalog();
        if (!StringUtil.isEmpty(catalog)) {
            return catalog;
        }
        return entityMeta.getTableMeta().getCatalog();
    }

    /**
     * 採番テーブルのスキーマ名を返します。
     * 
     * @return 採番テーブルのスキーマ名
     */
    protected String getSchema() {
        final String schema = tableGenerator.schema();
        if (!StringUtil.isEmpty(schema)) {
            return schema;
        }
        return entityMeta.getTableMeta().getSchema();
    }

    /**
     * 採番テーブル名を返します。
     * 
     * @return 採番テーブル名
     */
    protected String getTable() {
        final String table = tableGenerator.table();
        if (!StringUtil.isEmpty(table)) {
            return table;
        }
        return DEFAULT_TABLE;
    }

    /**
     * 採番テーブルの識別子のカラム名を返します。
     * 
     * @return 採番テーブルの識別子のカラム名
     */
    protected String getPkColumnName() {
        final String pkColumnName = tableGenerator.pkColumnName();
        if (!StringUtil.isEmpty(pkColumnName)) {
            return pkColumnName;
        }
        return DEFAULT_PK_COLUMN_NAME;
    }

    /**
     * 採番テーブルの識別子の値を返します。
     * 
     * @return 採番テーブルの識別子の値
     */
    protected String getPkColumnValue() {
        final String pkColumnValue = tableGenerator.pkColumnValue();
        if (!StringUtil.isEmpty(pkColumnValue)) {
            return pkColumnValue;
        }
        return entityMeta.getTableMeta().getName() + "_"
                + propertyMeta.getColumnMeta().getName();
    }

    /**
     * 採番テーブルの値のカラム名を返します。
     * 
     * @return 採番テーブルの値のカラム名
     */
    protected String getValueColumnName() {
        final String valueColumnName = tableGenerator.valueColumnName();
        if (!StringUtil.isEmpty(valueColumnName)) {
            return valueColumnName;
        }
        return DEFAULT_VALUE_COLUMN_NAME;
    }

    /**
     * 採番テーブルを更新するSQLを作成して返します。
     * 
     * @return 採番テーブルを更新するSQL
     */
    protected String createUpdateSql() {
        final StringBuilder buf = new StringBuilder(100);
        buf.append("update ");
        if (!StringUtil.isEmpty(catalog)) {
            buf.append(catalog).append('.');
        }
        if (!StringUtil.isEmpty(schema)) {
            buf.append(schema).append('.');
        }
        buf.append(table).append(" set ").append(valueColumnName).append(" = ")
                .append(valueColumnName).append(" + ? where ").append(
                        pkColumnName).append(" = ?");
        return new String(buf);
    }

    /**
     * 採番テーブルから値を取得するするSQLを作成して返します。
     * 
     * @return 採番テーブルから値を取得するするSQL
     */
    protected String createSelectSql() {
        final StringBuilder buf = new StringBuilder(100);
        buf.append("select ").append(valueColumnName).append(" from ");
        if (!StringUtil.isEmpty(catalog)) {
            buf.append(catalog).append('.');
        }
        if (!StringUtil.isEmpty(schema)) {
            buf.append(schema).append('.');
        }
        buf.append(table).append(" where ").append(pkColumnName).append(" = ?");
        return new String(buf);
    }

}
