package org.maachang.rimdb ;

import org.maachang.rimdb.index.Pointer;
import org.maachang.rimdb.search.CacheSearch;
import org.maachang.rimdb.search.CreateSearch;
import org.maachang.rimdb.search.SearchImpl;
import org.maachang.rimdb.table.Indexs;
import org.maachang.rimdb.table.TableImpl;
import org.maachang.rimdb.table.TableUtil;
import org.maachang.rimdb.util.ConvertUtil;
import org.maachang.rimdb.util.NList;
import org.maachang.rimdb.util.OList;

/**
 * SQL(のような言語)を解析して検索オブジェクトを生成.
 * 
 * @version 2014/07/16
 * @author  masahito suzuki
 * @since   rimdb-1.00
 */
public final class Sql {
    protected Sql() {}
    
    private static final CacheSearch cache = TableFactory.getInstance().getCacheSearch() ;
    private static final TableFactory tableFactory = TableFactory.getInstance() ;
    
    /**
     * Searchキャッシュをクリア.
     * @param name 対象のキャッシュ名を設定します.
     */
    public static final void remove( final String name ) {
        cache.remove( name ) ;
    }
    
    /**
     * キャッシュ情報を取得.
     * @param name 対象のキャッシュ名を設定します.
     * @return Search キャッシュされた検索オブジェクトが返却されます.
     */
    public static final Search get( final String name ) {
        return cache.get( name ) ;
    }
    
    /**
     * Searchオブジェクトを取得.
     * @param sql 対象のSQL文を設定します.
     * @return Search 検索オブジェクトが返却されます.
     */
    public static final Search create( final String sql ) {
        
        int i ;
        char c ;
        String cacheName = null ;
        
        // 先頭の文字が＄の場合は、検索キャッシュ名として処理.
        int off = 0 ;
        final int sqlLen = sql.length() ;
        if( sql.charAt( 0 ) == '$' ) {
            
            // 文字の先頭が$の場合は、キャッシュ名の登録、呼び出し.
            for( i = 1 ; i < sqlLen ; i ++ ) {
                if( ( c = sql.charAt( i ) ) == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ';' ) {
                    cacheName = sql.substring( 1,i ) ;
                    off = i ;
                    break ;
                }
            }
            
            // キャッシュ名だけが定義されている場合.
            if( cacheName == null ) {
                
                // 対象キャッシュ名でキャッシュ情報を取得.
                return cache.get( sql.substring( 1 ) ) ;
            }
            
            // キャッシュ情報が存在する場合のみ処理.
            SearchImpl ret = cache.get( cacheName ) ;
            if( ret != null ) {
                return ret ;
            }
            
        }
        
        // SQL条件をカット.
        NList code = cutCode( sql,off,sqlLen ) ;
        final int len = code.size() ;
        
        if( len == 0 ) {
            throw null ;
        }
        
        final int[] array = code.toArray() ;
        i = 0 ;
        
        // selectが存在する場合は、読み飛ばす.
        if( ConvertUtil.eqEng( sql,array[i],array[i+1],"select" ) ) {
            i += 2 ;
        }
        
        int s,e ;
        String o ;
        OList<String> params ;
        
        String column = null ;
        int pointerType = -1 ;
        boolean descFlag = false ;
        
        TableImpl table = null ;
        Indexs indexTable = null ;
        
        int type = 0 ;
        String name = null ;
        CreateSearch search = null ;
        
        // 処理解析.
        for( ; i < len ; i += 2 ) {
            
            s = array[i] ;
            e = array[i+1] ;
            
            // 終端検知.
            if( ConvertUtil.eqEng( sql,s,e,";" ) ) {
                
                // 終端を検知した場合は、処理終了.
                break ;
            }
            // キャッシュ名が定義されている場合.
            else if( sql.charAt( s ) == '$' ) {
                
                cacheName = sql.substring( s+1,e ) ;
                continue ;
            }
            
            switch( type ) {
                
                case 0 : // count,fromを検索.
                    if( ConvertUtil.eqEng( sql,s,e,"from" ) ) {
                        type = 1 ;
                    }
                    
                    break ;
                
                case 1 : // テーブル名を取得.
                    
                    // テーブル名取得.
                    name = TableUtil.convertJavaNameByDBName( sql.substring( s,e ) ) ;
                    
                    // テーブルオブジェクトを取得.
                    if( ( table = (TableImpl)tableFactory.get( name ) ) == null ) {
                        throw new RimDbException( "指定テーブル名[" +
                            name + "]は存在しません:" + sql ) ;
                    }
                    indexTable = table.getIndexs() ;
                    
                    // 検索オブジェクトを取得.
                    search = new CreateSearch( name ) ;
                    type = 2 ;
                    
                    break ;
                
                case 2 : // 条件定義取得.
                    
                    if( ConvertUtil.eqEng( sql,s,e,"where" ) ) {
                        column = null ;
                        pointerType = -1 ;
                        type = 4 ;
                    }
                    else if( ConvertUtil.eqEng( sql,s,e,"order" ) ) {
                        type = 3 ;
                    }
                    else {
                        throw new RimDbException( "不明のコマンド[" +
                            sql.substring( s,e ) + "]が検出されました:" + sql ) ;
                    }
                    
                    break ;
                
                case 3 : // order [by]チェック.
                    
                    if( ConvertUtil.eqEng( sql,s,e,"by" ) ) {
                        column = null ;
                        pointerType = -1 ;
                        descFlag = false ;
                        type = 5 ;
                    }
                    else {
                        
                        throw new RimDbException( "不明のorderコマンド[" +
                            sql.substring( s,e ) + "]が検出されました:" + sql ) ;
                    }
                    
                    break ;
                    
                case 4 : // where構文.
                    
                    // order byの場合.
                    if( ConvertUtil.eqEng( sql,s,e,"order" ) ) {
                        type = 3 ;
                        break ;
                    }
                    
                    // カラム名が設定されていない場合.
                    if( column == null ) {
                        
                        if( ( c = sql.charAt( s ) ) == '(' ) {
                            search.begin() ;
                        }
                        else if( c == ')' ) {
                            search.end() ;
                        }
                        else if( c == '&' || ConvertUtil.eqEng( sql,s,e,"and" ) ) {
                            search.and() ;
                        }
                        else if( c == '|' || ConvertUtil.eqEng( sql,s,e,"or" ) ) {
                            search.or() ;
                        }
                        // 空間インデックス検索.
                        else if( ConvertUtil.eqEng( sql,s,e,"point" ) ) {
                            column = "point" ;
                            pointerType = Pointer.TYPE_POSITION ;
                        }
                        // カラム名.
                        else {
                            
                            // 特定条件以外は、カラム名.
                            column = TableUtil.convertJavaNameByDBName( sql.substring( s,e ) ) ;
                        }
                        break ;
                    }
                    // 検索ポインターが定義されていない場合.
                    else if( pointerType == -1 ) {
                        
                        if( ( c = sql.charAt( s ) ) == '=' ) {
                            pointerType = Pointer.TYPE_EQ ;
                        }
                        else if( c == '!' || ConvertUtil.eqEng( sql,s,e,"<>" ) ) {
                            pointerType = Pointer.TYPE_NEQ ;
                        }
                        else if( c == '>' ) {
                            if( ConvertUtil.eqEng( sql,s,e,">=" ) ) {
                                pointerType = Pointer.TYPE_BIG_EQ ;
                            }
                            else {
                                pointerType = Pointer.TYPE_BIG ;
                            }
                        }
                        else if( c == '<' ) {
                            if( ConvertUtil.eqEng( sql,s,e,"<=" ) ) {
                                pointerType = Pointer.TYPE_SMALL_EQ ;
                            }
                            else {
                                pointerType = Pointer.TYPE_SMALL ;
                            }
                        }
                        else if( ConvertUtil.eqEng( sql,s,e,"in" ) ) {
                            pointerType = Pointer.TYPE_IN ;
                        }
                        else if( ConvertUtil.eqEng( sql,s,e,"between" ) ) {
                            pointerType = Pointer.TYPE_BETWEEN ;
                        }
                        else if( ConvertUtil.eqEng( sql,s,e,"like" ) ) {
                            pointerType = Pointer.TYPE_LIKE ;
                        }
                        else {
                            
                            throw new RimDbException( "不明な条件式[" +
                                sql.substring( s,e ) + "]が定義されています:" + sql ) ;
                        }
                        break ;
                    }
                    
                    // データー側の処理.
                    
                    // プリペアステートメントの場合.
                    if( ( c = sql.charAt( s ) ) == '?' ) {
                        
                        // 条件ポインターセット.
                        search.pointer( getPointer( indexTable,column,pointerType ) ) ;
                        column = null ;
                        pointerType = -1 ;
                        break ;
                    }
                    // 連続定義の場合.
                    else if( c == '(' ) {
                        
                        // (?)条件の場合.
                        if( i + 4 < len && sql.charAt( array[i+2] ) == '?' &&
                            sql.charAt( array[i+4] ) == ')' ) {
                            i += 4 ;
                            
                            // 条件ポインターセット.
                            search.pointer( getPointer( indexTable,column,pointerType ) ) ;
                            column = null ;
                            pointerType = -1 ;
                            break ;
                        }
                        
                        // (xx,xxx,xxxx)の場合.
                        params = new OList<String>() ;
                        while( true ) {
                            s = array[i+=2] ;
                            e = array[i+1] ;
                            
                            // 終端が検出された場合.
                            if( ( c = sql.charAt( s ) ) == ')' ) {
                                break ;
                            }
                            else if( c == ',' ) {
                                continue ;
                            }
                            // 文字列定義の場合.
                            if( ( c = ( o = sql.substring( s,e ) ).charAt( 0 ) ) == '\"' || c == '\'' ) {
                                params.add( o.substring( 1,o.length()-1 ) ) ;
                            }
                            else {
                                params.add( o ) ;
                            }
                            o = null ;
                        }
                        
                        // 条件ポインターセット.
                        search.pointer( getPointer( indexTable,column,pointerType ),params.getArray() ) ;
                        column = null ;
                        pointerType = -1 ;
                        
                        params = null ;
                        break ;
                    }
                    // 単一定義の場合.
                    else {
                        
                        // 文字列定義の場合.
                        if( ( c = ( o = sql.substring( s,e ) ).charAt( 0 ) ) == '\"' || c == '\'' ) {
                            o = o.substring( 1,o.length()-1 ) ;
                        }
                        
                        // 条件ポインターセット.
                        search.pointer( getPointer( indexTable,column,pointerType ),o ) ;
                        column = null ;
                        pointerType = -1 ;
                        
                        o = null ;
                        break ;
                    }
                    
                case 5 : // order by定義の場合.
                    
                    // whereの場合.
                    if( ConvertUtil.eqEng( sql,s,e,"where" ) ) {
                        column = null ;
                        pointerType = -1 ;
                        type = 4 ;
                        break ;
                    }
                    // 区切り情報が定義されている場合.
                    else if( sql.charAt( s ) == ',' ) {
                        continue ;
                    }
                    
                    // カラム名が設定されている場合.
                    if( column != null ) {
                        
                        c = 0 ;
                        if( ConvertUtil.eqEng( sql,s,e,"asc" ) ) {
                            c = 1 ;
                            descFlag = false ;
                        }
                        else if( ConvertUtil.eqEng( sql,s,e,"desc" ) ) {
                            c = 1 ;
                            descFlag = true ;
                        }
                        
                        // ソート条件をセット.
                        if( "point".equals( column ) ) {
                            
                            // 空間インデックスソート.
                            search.sort( descFlag ) ;
                            column = null ;
                        }
                        else {
                            
                            // 通常インデックスソート.
                            search.sort( descFlag,column ) ;
                            column = null ;
                        }
                        
                        // 今回昇順降順条件が定義されている場合.
                        if( c == 1 ) {
                            break ;
                        }
                        
                    }
                    
                    // ポイント検索用の場合.
                    if( ConvertUtil.eqEng( sql,s,e,"point" ) ) {
                        column = "point" ;
                    }
                    // 通常カラム名の場合.
                    else {
                        column = TableUtil.convertJavaNameByDBName( sql.substring( s,e ) ) ;
                    }
                    
                    break ;
            }
            
        }
        
        // ソートカラム名が存在する場合.
        if( type == 5 && column != null ) {
            
            // ソート条件をセット.
            if( "point".equals( column ) ) {
                
                // 空間インデックスソート.
                search.sort( descFlag ) ;
            }
            else {
                
                // 通常インデックスソート.
                search.sort( descFlag,column ) ;
            }
        }
        
        if( search == null ) {
            throw new RimDbException( "対象SQLの解析に失敗しました:" + sql ) ;
        }
        
        // キャッシュ名が設定されている場合.
        if( cacheName != null ) {
            SearchImpl ret = search.create() ;
            cache.set( cacheName,ret ) ;
            return ret ;
        }
        
        return search.create() ;
    }
    
    /** 文字列をカット. **/
    private static final NList cutCode( final String sql,final int offset,int len ) {
        
        char c,cc ;
        int s,b,cote ;
        s = b = cote = -1 ;
        
        NList ret = new NList( len >> 2 ) ;
        for( int i = offset ; i < len ; i ++ ) {
            c = sql.charAt( i ) ;
            if( cote != -1 ) {
                if( c == cote && b != '\\' ) {
                    ret.add( s ) ;
                    ret.add( i+1 ) ;
                    cote = -1 ;
                    s = -1 ;
                }
            }
            else if( c == '\'' || c == '\"' ) {
                if( s != -1 ) {
                    ret.add( s ) ;
                    ret.add( i ) ;
                }
                s = i ;
                cote = c ;
            }
            else if( c == '(' || c == ')' || c == '[' || c == ']' ||
                c == '?' || c == ';' || c == ',' || c == ':' ) {
                if( s != -1 ) {
                    ret.add( s ) ;
                    ret.add( i ) ;
                }
                s = -1 ;
                ret.add( i ) ;
                ret.add( i+1 ) ;
            }
            else if( c == '!' || c == '<' || c == '>' || c == '=' ) {
                if( s != -1 ) {
                    ret.add( s ) ;
                    ret.add( i ) ;
                }
                s = -1 ;
                if( i + 1 < len && ( ( cc = sql.charAt( i+1 ) ) == '=' ||
                    ( c == '<' && cc == '>' ) ) ) {
                    ret.add( i ) ;
                    ret.add( i + 2 ) ;
                    c = cc ;
                    i ++ ;
                }
                else {
                    ret.add( i ) ;
                    ret.add( i+1 ) ;
                }
            }
            else if( c == ' ' || c == '\t' || c == '\r' || c == '\n' ) {
                if( s != -1 ) {
                    ret.add( s ) ;
                    ret.add( i ) ;
                }
                s = -1 ;
            }
            else if( s == -1 ) {
                s = i ;
            }
            b = c ;
        }
        if( s != -1 ) {
            ret.add( s ) ;
            ret.add( len ) ;
        }
        return ret ;
    }
    
    /** 指定カラム名の指定ポインタを取得. **/
    private static final Pointer getPointer( final Indexs indexTable,final String name,final int type ) {
        switch( type ) {
            case Pointer.TYPE_EQ :
                return indexTable.eq( name ) ;
            case Pointer.TYPE_IN :
                return indexTable.in( name ) ;
            case Pointer.TYPE_BIG :
                return indexTable.big( name ) ;
            case Pointer.TYPE_BIG_EQ :
                return indexTable.bigEq( name ) ;
            case Pointer.TYPE_SMALL :
                return indexTable.small( name ) ;
            case Pointer.TYPE_SMALL_EQ :
                return indexTable.smallEq( name ) ;
            case Pointer.TYPE_NEQ :
                return indexTable.not( name ) ;
            case Pointer.TYPE_BETWEEN :
                return indexTable.between( name ) ;
            case Pointer.TYPE_LIKE :
                return indexTable.like( name ) ;
            case Pointer.TYPE_POSITION :
                return indexTable.position() ;
        }
        throw new RimDbException( "カラム名[" + name +
            "]に対して、条件式[" + type + "]が不明です" ) ;
    }
    
}

