/*
 * Copyright (c) 2010, Stanislav Muhametsin. All Rights Reserved.
 * Copyright (c) 2010, Paul Merlin. All Rights Reserved.
 *
 * 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.qi4j.entitystore.sql.internal;

import java.util.ArrayList;
import java.util.List;
import org.qi4j.api.injection.scope.This;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sql.generation.api.grammar.builders.modification.DeleteBySearchBuilder;
import org.sql.generation.api.grammar.builders.modification.UpdateBySearchBuilder;
import org.sql.generation.api.grammar.common.SQLStatement;
import org.sql.generation.api.grammar.common.datatypes.SQLDataType;
import org.sql.generation.api.grammar.definition.table.AutoGenerationPolicy;
import org.sql.generation.api.grammar.definition.table.UniqueSpecification;
import org.sql.generation.api.grammar.factories.BooleanFactory;
import org.sql.generation.api.grammar.factories.ColumnsFactory;
import org.sql.generation.api.grammar.factories.DefinitionFactory;
import org.sql.generation.api.grammar.factories.LiteralFactory;
import org.sql.generation.api.grammar.factories.ModificationFactory;
import org.sql.generation.api.grammar.factories.QueryFactory;
import org.sql.generation.api.grammar.factories.TableReferenceFactory;
import org.sql.generation.api.vendor.SQLVendor;

public interface DatabaseSQLStringsBuilder
{
    void init();

    String[] buildSQLForSchemaCreation();

    String[] buildSQLForIndexCreation();

    String[] buildSQLForTableCreation();

    String buildSQLForSelectAllEntitiesStatement();

    String buildSQLForSelectEntityStatement();

    String buildSQLForInsertEntityStatement();

    String buildSQLForUpdateEntityStatement();

    String buildSQLForRemoveEntityStatement();

    @SuppressWarnings("PublicInnerClass")
    abstract class CommonMixin
        implements DatabaseSQLStringsBuilder
    {

        private static final Logger LOGGER = LoggerFactory.getLogger( DatabaseSQLStringsBuilder.class );

        @This
        private DatabaseSQLServiceState _state;

        private SQLVendor vendor;

        private String schemaName;

        private String[] schemaCreationSQLs;

        private String[] indexCreationSQLs;

        private String[] tableCreationSQLs;

        private String selectAllEntitiesSQL;

        private String selectEntitySQL;

        private String insertEntitySQL;

        private String updateEntitySQL;

        private String removeEntitySQL;

        @Override
        public void init()
        {
            this.vendor = this._state.vendor().get();

            this.schemaName = this._state.schemaName().get();

            this.schemaCreationSQLs = this.toString( this.createSchemaStatements( this.vendor ) );
            LOGGER.trace( "SQL for schema creation: {}", this.schemaCreationSQLs );

            this.indexCreationSQLs = this.toString( this.createIndicesStatements( this.vendor ) );
            LOGGER.trace( "SQL for index creation: {}", this.indexCreationSQLs );

            this.tableCreationSQLs = this.toString( this.createTableStatements( this.vendor ) );
            LOGGER.trace( "SQL for table creation: {}", this.tableCreationSQLs );

            this.selectAllEntitiesSQL = this.vendor.toString( this.createSelectAllEntitiesStatement( this.vendor ) );
            LOGGER.trace( "SQL for select all entities: {}", this.selectAllEntitiesSQL );

            this.selectEntitySQL = this.vendor.toString( this.createSelectEntityStatement( this.vendor ) );
            LOGGER.trace( "SQL for select entity: {}", this.selectEntitySQL );

            this.insertEntitySQL = this.vendor.toString( this.createInsertEntityStatement( this.vendor ) );
            LOGGER.trace( "SQL for insert entity: {}", this.insertEntitySQL );

            this.updateEntitySQL = this.vendor.toString( this.createUpdateEntityStatement( this.vendor ) );
            LOGGER.trace( "SQL for update entity: {}", this.updateEntitySQL );

            this.removeEntitySQL = this.vendor.toString( this.createRemoveEntityStatement( this.vendor ) );
            LOGGER.trace( "SQL for remove entity: {}", this.removeEntitySQL );
        }

        protected String[] toString( SQLStatement[] stmts )
        {
            List<String> result = new ArrayList<String>();
            if( stmts != null )
            {
                for( Integer idx = 0; idx < stmts.length; ++idx )
                {
                    SQLStatement statement = stmts[idx];
                    if( statement != null )
                    {
                        String stringStatement = this.vendor.toString( statement );
                        if( stringStatement != null && stringStatement.length() > 0 )
                        {
                            result.add( this.vendor.toString( statement ) );
                        }
                    }
                }
            }
            return result.toArray( new String[ result.size() ] );
        }

        protected SQLVendor getVendor()
        {
            return this.vendor;
        }

        protected String getSchemaName()
        {
            return this.schemaName;
        }

        protected SQLStatement[] createSchemaStatements( SQLVendor vendor )
        {
            // @formatter:off
            return new SQLStatement[]
            {
                vendor.getDefinitionFactory().createSchemaDefinitionBuilder()
                .setSchemaName( this.schemaName )
                .createExpression()
            };
            // @formatter:on
        }

        protected SQLStatement[] createIndicesStatements( SQLVendor vendor )
        {
            // TODO
            return new SQLStatement[] {};
        }

        protected SQLStatement[] createTableStatements( SQLVendor vendor )
        {
            DefinitionFactory d = vendor.getDefinitionFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();


            // @formatter:off
            return new SQLStatement[]
            {
                d.createTableDefinitionBuilder()
                    .setTableName( t.tableName( this.getSchemaName(), SQLs.TABLE_NAME ) )
                    .setTableContentsSource( d.createTableElementListBuilder()
                        .addTableElement( d.createColumnDefinition( SQLs.ENTITY_PK_COLUMN_NAME, this.getPKType(), false, AutoGenerationPolicy.BY_DEFAULT ) )
                        .addTableElement( d.createColumnDefinition( SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME, this.getOptimisticLockType(), false ) )
                        .addTableElement( d.createColumnDefinition( SQLs.ENTITY_IDENTITY_COLUMN_NAME, this.getIDType(), false ) )
                        .addTableElement( d.createColumnDefinition( SQLs.ENTITY_STATE_COLUMN_NAME, this.getStateType(), false ) )
                        .addTableElement( d.createColumnDefinition( SQLs.ENTITY_LAST_MODIFIED_COLUMN_NAME, this.getLastModifiedType(), false ) )
                        .addTableElement( d.createTableConstraintDefinition( d.createUniqueConstraintBuilder()
                            .setUniqueness( UniqueSpecification.PRIMARY_KEY )
                            .addColumns( SQLs.ENTITY_PK_COLUMN_NAME )
                            .createExpression() ) )
                        .addTableElement( d.createTableConstraintDefinition( d.createUniqueConstraintBuilder()
                            .setUniqueness( UniqueSpecification.UNIQUE )
                            .addColumns( SQLs.ENTITY_IDENTITY_COLUMN_NAME )
                            .createExpression() ) )
                        .createExpression()
                        )
                   .createExpression()
            };
            // @formatter:on
        }

        protected SQLStatement createSelectAllEntitiesStatement( SQLVendor vendor )
        {
            QueryFactory q = vendor.getQueryFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();

            // @formatter:off
            return q.simpleQueryBuilder()
                .select( SQLs.ENTITY_PK_COLUMN_NAME, SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME, SQLs.ENTITY_STATE_COLUMN_NAME )
                .from( t.tableName( this.schemaName, SQLs.TABLE_NAME ) )
                .createExpression();
            // @formatter:on
        }

        protected SQLStatement createSelectEntityStatement( SQLVendor vendor )
        {
            QueryFactory q = vendor.getQueryFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();
            BooleanFactory b = vendor.getBooleanFactory();
            ColumnsFactory c = vendor.getColumnsFactory();
            LiteralFactory l = vendor.getLiteralFactory();

            // @formatter:off
            return q.simpleQueryBuilder()
                .select( SQLs.ENTITY_PK_COLUMN_NAME, SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME, SQLs.ENTITY_STATE_COLUMN_NAME )
                .from( t.tableName( this.schemaName, SQLs.TABLE_NAME ) )
                .where( b.eq( c.colName( SQLs.ENTITY_IDENTITY_COLUMN_NAME ), l.param() ) )
                .createExpression();
            // @formatter:on
        }

        protected SQLStatement createInsertEntityStatement( SQLVendor vendor )
        {
            ModificationFactory m = vendor.getModificationFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();
            LiteralFactory l = vendor.getLiteralFactory();

            // @formatter:off
            return m.insert()
                .setTableName( t.tableName( this.schemaName, SQLs.TABLE_NAME ) )
                .setColumnSource( m.columnSourceByValues()
                    .addColumnNames(
                        SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME,
                        SQLs.ENTITY_IDENTITY_COLUMN_NAME,
                        SQLs.ENTITY_STATE_COLUMN_NAME,
                        SQLs.ENTITY_LAST_MODIFIED_COLUMN_NAME
                        )
                    .addValues(
                        l.n( 0 ),
                        l.param(),
                        l.param(),
                        l.param()
                        )
                    .createExpression()
                    )
                 .createExpression();
            // @formatter:on
        }

        protected SQLStatement createUpdateEntityStatement( SQLVendor vendor )
        {
            ModificationFactory m = vendor.getModificationFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();
            LiteralFactory l = vendor.getLiteralFactory();
            BooleanFactory b = vendor.getBooleanFactory();
            ColumnsFactory c = vendor.getColumnsFactory();

            // @formatter:off
            UpdateBySearchBuilder builder = m.updateBySearch()
                .setTargetTable( m.createTargetTable( t.tableName( this.schemaName, SQLs.TABLE_NAME ) ) )
                .addSetClauses(
                    m.setClause( SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME, m.updateSourceByExp( l.param() ) ),
                    m.setClause( SQLs.ENTITY_STATE_COLUMN_NAME, m.updateSourceByExp( l.param() ) ),
                    m.setClause( SQLs.ENTITY_LAST_MODIFIED_COLUMN_NAME, m.updateSourceByExp( l.param() ) )
                    );
            builder
                .getWhereBuilder()
                    .reset( b.eq( c.colName( SQLs.ENTITY_PK_COLUMN_NAME ), l.param() ) )
                    .and( b.eq( c.colName( SQLs.ENTITY_OPTIMISTIC_LOCK_COLUMN_NAME ), l.param() ) );
            return builder.createExpression();
            // @formatter:on
        }

        protected SQLStatement createRemoveEntityStatement( SQLVendor vendor )
        {
            ModificationFactory m = vendor.getModificationFactory();
            TableReferenceFactory t = vendor.getTableReferenceFactory();
            LiteralFactory l = vendor.getLiteralFactory();
            BooleanFactory b = vendor.getBooleanFactory();
            ColumnsFactory c = vendor.getColumnsFactory();

            // @formatter:off
            DeleteBySearchBuilder builder = m.deleteBySearch()
                .setTargetTable( m.createTargetTable( t.tableName( this.schemaName, SQLs.TABLE_NAME ) ) );
            builder.getWhere()
                .reset( b.eq( c.colName( SQLs.ENTITY_PK_COLUMN_NAME ), l.param() ) );
            return builder.createExpression();
            // @formatter:on
        }

        protected SQLDataType getPKType()
        {
            return this.vendor.getDataTypeFactory().bigInt();
        }

        protected SQLDataType getOptimisticLockType()
        {
            return this.vendor.getDataTypeFactory().bigInt();
        }

        protected SQLDataType getIDType()
        {
            return this.vendor.getDataTypeFactory().sqlVarChar( 64 );
        }

        protected SQLDataType getStateType()
        {
            return this.vendor.getDataTypeFactory().sqlVarChar( 10000 );
        }

        protected SQLDataType getLastModifiedType()
        {
            return this.vendor.getDataTypeFactory().bigInt();
        }

        @Override
        public String[] buildSQLForSchemaCreation()
        {
            return this.schemaCreationSQLs;
        }

        @Override
        public String[] buildSQLForIndexCreation()
        {
            return this.indexCreationSQLs;
        }

        @Override
        public String buildSQLForSelectAllEntitiesStatement()
        {
            return this.selectAllEntitiesSQL;
        }

        @Override
        public String buildSQLForSelectEntityStatement()
        {
            return this.selectEntitySQL;
        }

        @Override
        public String buildSQLForInsertEntityStatement()
        {
            return this.insertEntitySQL;
        }

        @Override
        public String buildSQLForUpdateEntityStatement()
        {
            return this.updateEntitySQL;
        }

        @Override
        public String buildSQLForRemoveEntityStatement()
        {
            return this.removeEntitySQL;
        }

        @Override
        public String[] buildSQLForTableCreation()
        {
            return this.tableCreationSQLs;
        }

    }

}
