package org.maachang.mimdb.core;

import org.maachang.mimdb.MimdbException;
import org.maachang.mimdb.core.impl.EqualsNoList;
import org.maachang.mimdb.core.impl.MimdbUtils;
import org.maachang.mimdb.core.impl.WhereBlock;

/**
 * Mimdbプリコンパイル済みクエリー要素.
 *
 * @version 2013/10/24
 * @author masahito suzuki
 * @since MasterInMemDB 1.00
 */
public class MimdbQueryPrepared implements MimdbBase {

    /** コンパイル済みオブジェクト. **/
    protected QueryCompileInfo info = null ;

    /** 実行用Preparedパラメータ. **/
    private MimdbSearchElement[] executionParams = null ;

    /** 表示オフセット. **/
    private int viewOffset = -1 ;

    /** 表示リミット. **/
    private int viewLimit = -1 ;

    /**
     * コンストラクタ.
     */
    public MimdbQueryPrepared() {}

    /**
     * コンストラクタ.
     * @param o PreparedInfo情報を設定します.
     */
    public MimdbQueryPrepared( QueryCompileInfo o ) {
        create( o ) ;
    }

    /** デストラクタ. **/
    protected void finalize() throws Exception {
        clear() ;
    }

    /**
     * 情報生成.
     * @param o PreparedInfo情報を設定します.
     */
    public void create( QueryCompileInfo o ) {
        if( o == null ) {
            throw new MimdbException( "QueryCompileInfoが定義されていません" ) ;
        }
        info = o ;
        executionParams = null ;
        viewOffset = -1 ;
        viewLimit = -1 ;
    }

    /**
     * クリア.
     */
    public void clear() {
        info = null ;
        executionParams = null ;
        viewOffset = -1 ;
        viewLimit = -1 ;
    }

    /**
     * オブジェクトがクリアーされているかチェック.
     * @return boolean [true]の場合、クリアされています.
     */
    public boolean isClear() {
        return info == null ;
    }

    /** チェック処理. **/
    protected final void check() {
        if( info == null ) {
            throw new MimdbException( "オブジェクトはクリアされています" ) ;
        }
    }

    /**
     * DB更新IDの取得.
     * @return long DB更新IDが返却されます.
     */
    public long getDbId() {
        check() ;
        return info.dbId ;
    }

    /**
     * テーブル名を取得.
     * @return String テーブル名が返却されます.
     */
    public String getName() {
        check() ;
        return info.name ;
    }

    /**
     * preparedパラメータ数を取得.
     * @return int パラメータ数が返却されます.
     */
    public int paramsLength() {
        check() ;
        return info.preparedParamsSize ;
    }

    /**
     * preparedパラメータをクリア.
     * @return MimdbQueryPrepared このオブジェクトが返却されます.
     */
    public MimdbQueryPrepared clearParams() {
        check() ;
        executionParams = null ;
        return this ;
    }

    /**
     * preparedパラメータを設定.
     * @param index 対象のパラメータ項番を設定します.
     * @param value 対象の設定情報を設定します.
     * @return MimdbQueryPrepared このオブジェクトが返却されます.
     */
    public MimdbQueryPrepared setParams( int index,Object value ) {
        check() ;
        if( info.preparedParams == null ||
            index < 0 || index >= info.preparedParamsSize ) {
            return this ;
        }
        if( executionParams == null ) {
            executionParams = new MimdbSearchElement[ info.preparedParamsSize ] ;
        }
        executionParams[ index ] = info.preparedParams[ index ] ;
        executionParams[ index ].setValue( value ) ;
        return this ;
    }

    /**
     * preparedパラメータのカラム名を取得.
     * @param index 対象のパラメータ項番を設定します.
     * @return String カラム名が返却されます.
     *                指定パラメータが存在しない場合は[null].
     */
    public String paramNoByColumnName( int index ) {
        check() ;
        if( info.preparedParams == null ||
            index < 0 || index >= info.preparedParamsSize ) {
            return null ;
        }
        return info.preparedParams[ index ].getColumn() ;
    }

    /**
     * preparedパラメータのカラム位置を取得.
     * @param index 対象のパラメータ項番を設定します.
     * @return int カラム位置が返却されます.
     *             指定パラメータが存在しない場合は[-1].
     */
    public int paramNoByColumnNo( int index ) {
        check() ;
        if( info.preparedParams == null ||
            index < 0 || index >= info.preparedParamsSize ) {
            return -1 ;
        }
        MimdbSearchElement em = info.preparedParams[ index ] ;
        // 指定インデックス情報がオフセット、リミット条件の場合.
        if( em.isOffLimit() ) {
            return -1 ;
        }
        return MimdbTableManager.getInstance().get( info.name ).
            getColumnNameByNo( em.getColumn() ) ;
    }

    /**
     * preparedパラメータのカラムタイプを取得.
     * @param index 対象のパラメータ項番を設定します.
     * @return int カラムタイプが返却されます.
     *             指定パラメータが存在しない場合は[-1].
     */
    public int paramNoByColumnType( int index ) {
        check() ;
        if( info.preparedParams == null ||
            index < 0 || index >= info.preparedParamsSize ) {
            return -1 ;
        }
        MimdbSearchElement em = info.preparedParams[ index ] ;
        
        // 指定インデックス情報がオフセット、リミット条件の場合.
        if( em.isOffLimit() ) {
            
            // 数値条件を返却.
            return MimdbIndex.COLUMN_INT ;
        }
        return MimdbTableManager.getInstance().get( info.name ).
            getColumnType( em.getColumn() ) ;
    }

    /**
     * preparedパラメータのカラム条件がリスト形式していかチェック.
     * @param index 対象のパラメータ項番を設定します.
     * @return boolean [true]の場合、対象処理条件は[IN]および[BETWEEN]です.
     */
    public boolean isPamraNoByListParam( int index ) {
        check() ;
        if( info.preparedParams == null ||
            index < 0 || index >= info.preparedParamsSize ) {
            return false ;
        }
        int type = info.preparedParams[ index ].getType() ;
        return type == MimdbSearchType.TYPE_IN ||
            type == MimdbSearchType.TYPE_BETWEEN ;
    }

    /**
     * 表示オフセット値を設定.
     * この条件により、表示位置を確定できます.
     * @param off 表示オフセット値を設定します.
     *            [-1]が設定された場合、表示幅は確定されません.
     * @return MimdbQueryPrepared このオブジェクトが返却されます.
     */
    public MimdbQueryPrepared setOffset( int off ) {
        check() ;
        viewOffset = ( off <= -1 ) ? -1 : off ;
        return this ;
    }

    /**
     * 表示リミット値を設定.
     * この条件により、表示位置を確定できます.
     * @param limit 表示リミット値を設定します.
     *            [-1]が設定された場合、表示幅は確定されません.
     * @return MimdbQueryPrepared このオブジェクトが返却されます.
     */
    public MimdbQueryPrepared setLimit( int limit ) {
        check() ;
        viewLimit = ( limit <= -1 ) ? -1 : limit ;
        return this ;
    }

    /**
     * コンパイル済みオブジェクトを取得.
     * @return QueryCompileInfo コンパイル済みオブジェクトが返却されます.
     */
    public QueryCompileInfo getInfo() {
        check() ;
        return info ;
    }

    /**
     * SQL文に変換.
     * @return String SQL文に変換されます.
     * @exception Exception 例外.
     */
    public String getSql() throws Exception {
        check() ;
        return info.getSql( MimdbTableManager.getInstance() ) ;
    }

    /**
     * クエリー実行.
     * @return MimdbResult 実行結果が返却されます.
     * @exception Exception 例外.
     */
    public MimdbResult executeQuery() throws Exception {
        return executeQuery( false ) ;
    }

    /**
     * クエリー実行.
     * @param mode [true]の場合、テーブルが新しく更新されている場合はエラーを出力します.
     * @return MimdbResult 実行結果が返却されます.
     * @exception Exception 例外.
     */
    public MimdbResult executeQuery( boolean mode ) throws Exception {
        check() ;
        // データが設定されていないかチェック.
        if( info.dbId == -1L ) {
            throw new MimdbException( "必要なデータが存在しません" ) ;
        }
        int pOff,pLmt ;
        pOff = info.defOffset ;
        pLmt = info.defLimit ;

        int preparedParamsSize = info.preparedParamsSize ;
        String name = info.name ;
        EqualsNoList columns = info.columns ;
        boolean countFlag = info.countFlag ;
        SortElement sort = info.sort ;
        WhereBlock block = info.block ;

        MimdbSearchElement em ;
        // preparedStatement条件が設定されているかチェック.
        if( preparedParamsSize > 0 ) {
            if( executionParams == null ) {
                throw new MimdbException( "実行パラメータが設定されていません" ) ;
            }
            for( int i = 0 ; i < preparedParamsSize ; i ++ ) {
                if( ( em = executionParams[ i ] ) == null ) {
                    throw new MimdbException( "[" + i + "]番目のパラメータが設定されていません" ) ;
                }
                // 対象パラメータがオフセット、リミット定義の場合.
                if( em.getType() == MimdbSearchType.OFF_LIMIT ) {
                    if( "offset".equals( em.getColumn() ) ) {
                        pOff = MimdbUtils.convertInt( em.getValue() ) ;
                    }
                    else {
                        pLmt = MimdbUtils.convertInt( em.getValue() ) ;
                    }
                }
            }
        }

        // テーブルオブジェクトの取得.
        MimdbTable table = (MimdbTable)MimdbTableManager.getInstance().get( name ) ;
        if( table == null ) {
            throw new MimdbException( "指定テーブル(" + name + ")情報は存在しません" ) ;
        }
        // テーブルが新しく更新されている場合.
        if( table.getDbId() != info.dbId ) {
            if( mode ) {
                throw new MimdbException( "対象テーブル[" + name + "]は更新されています" ) ;
            }
            // テーブル更新IDを再設定.
            info.dbId = table.getDbId() ;
        }
        long dbId = info.dbId ;

        // ソート条件の初期化.
        initSortElement( table,sort ) ;

        // 更新パラメータをクリア.
        int off = viewOffset ;
        int limit = viewLimit ;
        executionParams = null ;
        viewOffset = -1 ;
        viewLimit = -1 ;

        // オフセット値が設定されていない場合.
        if( off == -1 && limit == -1 ) {
            if( pOff != -1 ) {
                off = pOff ;
            }
            if( pLmt != -1 ) {
                limit = pLmt ;
            }
        }

        // オフセット値、リミット値を整形.
        if( off != -1 || limit != -1 ) {
            if( off != -1 ) {
                if( limit == -1 ) {
                    limit = 999999999 ;
                }
            }
            else if( limit != -1 ) {
                off = 0 ;
            }
        }

        // Resultキャッシュを取得.
        ResultCache cache = ResultCache.get() ;
        ResultImpl ret = cache.result ;
        MimdbMetaData meta ;
        MimdbResultRow row ;

        // 検索条件が存在しない場合.
        if( block == null || block.size() == 0 ) {

            // 全情報を出力対象とする.

            // 件数表示の場合.
            if( countFlag ) {
                meta = cache.countMeta ;
                cache.countMeta.create( ret ) ;
                row = cache.countRow ;
                cache.countRow.create( ret,table.size() ) ;
                ret.create( dbId,table,cache,meta,row,null,1,1 ) ;
            }
            // データ返却の場合.
            else {
                meta = cache.meta ;
                cache.meta.create( ret,columns ) ;
                row = cache.row ;
                cache.row.create( ret ) ;
                cache.pointer.create( table.mainTable,sort,off,limit ) ;
                cache.pointer.execute() ;
                ret.create( dbId,table,cache,meta,row,cache.pointer,
                    cache.pointer.resultLength(),table.size() ) ;
            }

            return ret ;
        }
        else {

            // 検索処理を実施.
            MimdbMiddleSearch res = search( dbId,table,null,block ) ;

            // 検索結果が存在しない場合.
            if( res == null || res.size() <= 0 ) {
                // 件数表示の場合.
                if( countFlag ) {
                    meta = cache.countMeta ;
                    cache.countMeta.create( ret ) ;
                    row = cache.countRow ;
                    cache.countRow.create( ret,0 ) ;
                    ret.create( dbId,table,cache,meta,row,null,1,1 ) ;
                }
                // データ返却の場合.
                else {
                    meta = cache.meta ;
                    cache.meta.create( ret,columns ) ;
                    row = cache.row ;
                    cache.row.create( ret ) ;
                    cache.pointer.createZero( table.mainTable,sort,off,limit ) ;
                    cache.pointer.execute() ;
                    ret.create( dbId,table,cache,meta,row,cache.pointer,
                        cache.pointer.resultLength(),table.size() ) ;
                }
                return ret ;
            }

            // 検索結果が存在する場合.

            // 件数表示の場合.
            if( countFlag ) {
                meta = cache.countMeta ;
                cache.countMeta.create( ret ) ;
                row = cache.countRow ;
                cache.countRow.create( ret,res.size() ) ;
                ret.create( dbId,table,cache,meta,row,null,1,1 ) ;
            }
            // データ返却の場合.
            else {
                meta = cache.meta ;
                cache.meta.create( ret,columns ) ;
                row = cache.row ;
                cache.row.create( ret ) ;
                cache.pointer.create( res,table.mainTable,sort,off,limit ) ;
                cache.pointer.execute() ;
                ret.create( dbId,table,cache,meta,row,cache.pointer,
                    cache.pointer.resultLength(),table.size() ) ;
            }
            return ret ;
        }
    }

    /**
     * Where検索実行.
     */
    private static final MimdbMiddleSearch search( long dbId,MimdbTable table,MimdbMiddleSearch res,WhereBlock block )
        throws Exception {
        int len,andor ;
        Object o ;
        MimdbMiddleSearch blockRes ;
        MimdbSearchElement em ;
        len = block.size() ;
        andor = -1 ;
        for( int i = 0 ; i < len ; i ++ ) {
            o = block.get( i ) ;
            // block条件.
            if( o instanceof WhereBlock ) {
                blockRes = search( dbId,table,null,(WhereBlock)o ) ;
                // 検索結果がゼロ件の場合.
                if( blockRes == null ) {
                    // 指定なし及び、and条件中の場合.
                    if( andor == -1 || andor == 0 ) {
                        res = null ;
                    }
                    // or 指定の場合は、条件はそのまま保持.
                }
                else {
                    // 検索結果が存在する場合は、それぞれマージ.
                    if( andor == 0 ) {
                        // and設定.
                        res.and( blockRes ) ;
                    }
                    else if( andor == 1 ) {
                        // or.
                        if( res == null || res.size() <= 0 ) {
                            res = blockRes ;
                        }
                        else {
                            res.or( blockRes ) ;
                        }
                        res = ( res.size() > 0 ) ? res : null ;
                    }
                    else {
                        // normal.
                        res = ( blockRes.size() > 0 ) ? blockRes : null ;
                    }
                }
            }
            // 検索条件.
            else {
                switch( ( em = ( MimdbSearchElement )o ).getType() ) {
                    case MimdbSearchType.AND :
                        andor = 0 ; // and.
                        break ;
                    case MimdbSearchType.OR :
                        andor = 1 ; // or.
                        break ;
                    default :
                        // 検索情報の取得.

                        // normal.
                        if( andor == -1 ) {
                            // and も or も指定されていない条件.
                            res = table.getIndex( em.getColumnNo() ).search( em ) ;
                        }
                        // and.
                        else if( andor == 0 ) {
                            // 条件が存在する場合のみ、AND処理.
                            if( res != null ) {
                                // and条件の場合は、検索結果なし以外で処理対象.
                                res = table.getIndex( em.getColumnNo() ).and( em,res ) ;
                            }
                        }
                        // or.
                        else if( andor == 1 ) {
                            // or の場合は、検索結果なしでも処理.
                            res = table.getIndex( em.getColumnNo() ).or( em,res ) ;
                        }
                        // unknown.
                        else {
                            throw new MimdbException( "不明なand or条件が検出されました(column:" + em.toString() + " code:" + andor + ")" ) ;
                        }
                        andor = -1 ;
                        break ;
                }
            }
        }
        return res ;
    }

    /**
     * ソート条件が存在して、ソートインデックスカラム条件が定義されていない場合.
     * 定義する.
     */
    private final void initSortElement( MimdbTable table,SortElement sort ) {
        if( sort == null || sort.sortNoList == null ) {
            return ;
        }
        // インデックス条件が設定されていない場合.
        if( sort.indexList == null ) {

            int[] noList = sort.sortNoList ;
            int len = noList.length ;
            int[][] idx = new int[ len ][] ;
            MimdbIndex index ;

            for( int i = 0 ; i < len ; i ++ ) {
                index = table.getIndex( noList[ i ] ) ;
                if( index != null ) {
                    idx[ i ] = index.getSortNoList() ;
                }
                else {
                    idx[ i ] = null ;
                }
            }

            // 検索要素が１つの場合は、インデックス数を取得.
            if( len == 1 ) {
                sort.indexLength = table.getIndex( noList[ 0 ] ).getIndexSize() ;
            }
            else {
                sort.indexLength = -1 ;
            }
            sort.indexList = idx ;
        }
    }
}
