package org.maachang.mimdb.core.impl ;

import java.util.ArrayList;

import org.maachang.mimdb.MimdbException;
import org.maachang.mimdb.core.MimdbSearchType;
import org.maachang.mimdb.core.QueryInfo;
import org.maachang.mimdb.core.util.NumberList;

/**
 * SQL解析.
 * 
 * @version 2013/10/24
 * @author masahito suzuki
 * @since MasterInMemDB 1.00
 */
public final class SqlAnalyzer {
    private SqlAnalyzer() {}
    
    /** List配列長. **/
    private static final int LIST_LENGTH = 32 ;
    
    /**
     * SQL(Selectのみ)を解析.
     * @param out MimdbStmtを設定します.
     * @param sql 対象のSQL文を設定します.
     * @exception Exception 例外.
     */
    public static final void analysis( final QueryInfo out,String sql )
        throws Exception {
        
        // 終端の;マークを削除.
        int p = lastSQL( sql ) ;
        if( p != -1 ) {
            sql = sql.substring( 0,p ) ;
        }
        
        // 返却条件をクリア.
        out.clear() ;
        
        int s ;
        NumberList list ;
        
        sql = sql.trim() ;
        String low = sql.toLowerCase() ;
        int sqlLen = low.length() ;
        
        int off = 0 ;
        // 開始位置にselect文が存在するかチェック.
        if( low.startsWith( "select " ) ) {
            // 存在する場合は、次の文字まで移動.
            off = _noSpace( low,6,sqlLen ) ;
        }
        
        // カラム名群を解析.
        list = new NumberList( LIST_LENGTH ) ;
        while( ( s = _noSpaceString( low,off,sqlLen ) ) != -1 && !eq( low,"from",s ) ) {
            off = _noSpace( low,off+length( s ),sqlLen ) ;
            // カラム文字を解析.
            if( eq( low,",",s ) ) {
                continue ;
            }
            _columnAnalysis( low,list,s ) ;
        }
        // from条件が存在しない場合.
        if( !eq( low,"from",s ) ) {
            throw new MimdbException( "SQL文のテーブル名指定が行われていません:" + sql ) ;
        }
        // カラム名群が設定されていない.
        if( list.size() == 0 ) {
            throw new MimdbException( "SQL文での取得条件が設定されていません:" + sql ) ;
        }
        // 解析したカラム群をStatementにセット.
        _viewColumnBySetStatement( out,list,sql,low ) ;
        list.clear() ;
        
        // テーブル名の取得.
        off = _noSpace( low,off+4,sqlLen ) ;
        s = _noSpaceString( low,off,sqlLen ) ;
        if( s == -1 || eq( low,"where",s ) || eq( low,"order",s ) ) {
            throw new MimdbException( "SQL文にテーブル名が設定されてません:" + sql ) ;
        }
        out.setTable( substring( low,s ) ) ;
        
        // where,order byの取得.
        while( true ) {
            // 次の条件が存在するかチェック.
            if( s == -1 ) {
                off = _noSpace( low,off,sqlLen ) ;
            }
            else {
                off = _noSpace( low,off+length( s ),sqlLen ) ;
            }
            // 存在しない場合は、処理終了.
            if( off == -1 ) {
                return ;
            }
            
            // 続き条件を取得.
            s = _noSpaceString( low,off,sqlLen ) ;
            
            // where条件の場合.
            if( eq( low,"where",s ) ) {
                // 判別条件の処理.
                off = _whereAnalysis( list,off+length( s ),sql,low ) ;
                // 判別条件の追加.
                _whereBySetStatement( out,list,sql,low ) ;
                
                list.clear() ;
                s = -1 ;
            }
            // order by条件の場合.
            else if( eq( low,"order",s ) ) {
                off = _noSpace( low,off+length( s ),sqlLen ) ;
                if( off == -1 ) {
                    throw new MimdbException( "order by条件が不明です:" + sql ) ;
                }
                s = _noSpaceString( low,off,sqlLen ) ;
                if( eq( low,"by",s ) ) {
                    // order byの処理.
                    off = _orderByAnalysis( list,off+length( s ),sql,low ) ;
                    // order by条件の追加.
                    _orderBySetStatement( out,list,sql,low ) ;
                    
                    list.clear() ;
                    s = -1 ;
                }
                else {
                    throw new MimdbException( "order by条件が不明です:" + sql ) ;
                }
            }
            // offset,limit句の場合.
            else if( eq( low,"offset",s ) || eq( low,"limit",s ) ) {
                off = offLimit( out,off,s,sql,low,sqlLen ) ;
                s = -1 ;
            }
        }
    }
    
    /** 最終位置の[;]コード位置を取得. **/
    private static final int lastSQL( final String sql ) {
        char c,b ;
        b = 0 ;
        int cote = -1 ;
        for( int i = sql.length() - 1 ; i >= 0 ; i -- ) {
            c = sql.charAt( i ) ;
            if( cote != -1 ) {
                if( c == cote && b != '\\' ) {
                    cote = -1 ;
                    continue ;
                }
            }
            else if( c == ';' ) {
                return i ;
            }
            else if( c == '\'' || c == '\"' ) {
                cote = c ;
            }
            b = c ;
        }
        return -1 ;
    }
    
    /** 対象文字の一致チェック. **/
    private static final boolean eq( final String sql,final String chk,final int mg ) {
        int o = 0 ;
        int p = mg & 0x0000ffff ;
        int len = ( mg & 0xffff0000 ) >> 16 ;
        while( p < len ) {
            if( sql.charAt( p ++ ) != chk.charAt( o ++ ) ) {
                return false ;
            }
        }
        return ( chk.length() == o ) ;
    }
    
    /** 開始位置を取得. **/
    private static final int startPos( final int mg ) {
        return mg & 0x0000ffff ;
    }
    
    /** 終端位置を取得. **/
    private static final int endPos( final int mg ) {
        return mg >> 16 ;
    }
    
    /** 長さを取得. **/
    private static final int length( final int mg ) {
        return ( mg >> 16 ) - ( mg & 0x0000ffff ) ;
    }
    
    /** 開始点と終端点をマージ. **/
    private static final int marge( final int s,final int e ) {
        return s | ( e << 16 ) ;
    }
    
    /** 文字情報として返却. **/
    private static final String substring( final String src,final int mg ) {
        return src.substring( startPos( mg ),endPos( mg ) ) ;
    }
    
    /** スペースでない情報まで移動. **/
    private static final int _noSpace( final String sql,int pos,final int sqlLen ) {
        if( pos < sqlLen && sql.charAt( pos ) == ' ' ) {
            while( ++ pos < sqlLen && sql.charAt( pos ) == ' ' ) ;
        }
        if( pos >= sqlLen ) {
            return -1 ;
        }
        return pos ;
    }
    
    /** 有効文字数を抜き出す. **/
    private static final int _noSpaceString( final String sql,final int pos,final int sqlLen ) {
        if( pos >= sqlLen || sql.charAt( pos ) == ' ' ) {
            return -1 ;
        }
        int p = pos ;
        while( ++ p < sqlLen && sql.charAt( p ) != ' ' ) ;
        if( p > sqlLen ) {
            return -1 ;
        }
        return marge( pos,p ) ;
    }
    
    /** カラム情報解析. **/
    private static final void _columnAnalysis( final String src,final NumberList out,final int pos ) {
        char c ;
        int st = startPos( pos ) ;
        int ed = endPos( pos ) ;
        int s = -1 ;
        for( int i = st ; i < ed ; i ++ ) {
            
            // 区切り.
            if( ( c = src.charAt( i ) ) == ',' ) {
                if( s != -1 ) {
                    out.add( marge( s,i ) ) ;
                    s = -1 ;
                }
            }
            // 特殊条件.
            else if( c == '(' || c == ')' ) {
                if( s != -1 ) {
                    out.add( marge( s,i ) ) ;
                    s = -1 ;
                }
                out.add( marge( i,i+1 ) ) ;
            }
            
            // 有効文字列.
            else if( s == -1 ) {
                s = i ;
            }
        }
        if( s != -1 ) {
            out.add( marge( s,ed ) ) ;
        }
    }
    
    /** 判別情報解析. **/
    private static final int _whereAnalysis( final NumberList out,final int off,final String sql,final String low )
        throws Exception {
        char c ;
        int cote = -1 ;
        int par = 0 ;
        int len = low.lastIndexOf( " order " ) ;
        if( len == -1 ) {
            len = low.length() ;
        }
        int s = -1 ;
        for( int i = off ; i < len ; i ++ ) {
            // 文字コーテーション条件.
            if( ( c = low.charAt( i ) ) == '\'' || c == '\"' ) {
                // 文字コーテーション開始.
                if( cote == -1 ) {
                    if( c == '\'' ) {
                        cote = 0 ;
                    }
                    else {
                        cote = 1 ;
                    }
                    if( s != -1 ) {
                        out.add( marge( s,i ) ) ;
                        s = -1 ;
                    }
                    s = i+1 ; // 次の文字から有効.
                }
                // 文字コーテーション終了.
                else if( ( cote == 0 && c == '\'' ) ||
                    ( cote == 1 && c == '\"' ) ) {
                    cote = -1 ;
                    if( s != -1 ) {
                        out.add( marge( s,i ) ) ;
                        s = -1 ;
                    }
                }
            }
            // 文字コーテーション中.
            else if( cote != -1 ) {
                continue ;
            }
            
            // 区切り.
            else if( c == ' ' || c == ';' || c == ',' ) {
                if( s != -1 ) {
                    out.add( marge( s,i ) ) ;
                    s = -1 ;
                }
            }
            // 特殊文字.
            else if( c == '<' || c == '>' || c == '=' || c == '!' ||
                c == '(' || c == ')' ) {
                if( s != -1 ) {
                    out.add( marge( s,i ) ) ;
                    s = -1 ;
                }
                out.add( marge( i,i+1 ) ) ;
                if( c == '(' ) {
                    par ++ ;
                }
                else if( c == ')' ) {
                    par -- ;
                }
            }
            // 有効文字.
            else if( s == -1 ) {
                s = i ;
            }
        }
        if( cote != -1 || par != 0 ) {
            if( cote != -1 ) {
                throw new MimdbException( "文字指定の閉じ条件が設定されていません:" + sql ) ;
            }
            throw new MimdbException( "括弧の条件が不正です:" + sql ) ;
        }
        else if( s != -1 ) {
            out.add( marge( s,len ) ) ;
        }
        return len ;
    }
    
    /** 並び順情報解析. **/
    private static final int _orderByAnalysis( final NumberList out,final int off,final String sql,final String low ) {
        char c ;
        int len = low.length() ;
        int s = -1 ;
        for( int i = off ; i < len ; i ++ ) {
            // 区切り文字.
            if( ( c = low.charAt( i ) ) == ' ' || c == ',' || c == ';' ) {
                if( s != -1 ) {
                    out.add( marge( s,i ) ) ;
                    s = -1 ;
                }
            }
            // 有効文字.
            else if( s == -1 ) {
                s = i ;
            }
        }
        if( s != -1 ) {
            out.add( marge( s,len ) ) ;
        }
        return len ;
    }
    
    /** 表示カラム名群をStatementにセット. **/
    private static final void _viewColumnBySetStatement( final QueryInfo out, final NumberList in,final String sql,final String low )
        throws Exception {
        int len = in.size() ;
        // 全カラム指定の場合.
        if( len == 1 && eq( low,"*",in.get( 0 ) ) ) {
            out.setViewColumn( "*" ) ;
        }
        else if( len >= 4 &&
            eq( low,"count",in.get( 0 ) ) &&
            eq( low,"(",in.get( 1 ) ) &&
            eq( low,")",in.get( 3 ) ) ) {
            out.setViewCount() ;
        }
        else {
            int s ;
            for( int i = 0 ; i < len ; i ++ ) {
                // 禁止条件.
                if( length( s = in.get( i ) ) == 1 && ( eq( low,"(",s ) || eq( low,")",s ) || eq( low,"*",s ) ) ) {
                    throw new MimdbException( "不正なSQL表示条件[" + substring( sql,s ) + "]:" + sql ) ;
                }
                out.setViewColumn( substring( low,s ) ) ;
            }
        }
    }
    
    /** 判別条件をStatementにセット. **/
    private static final void _whereBySetStatement( final QueryInfo out, final NumberList in,final String sql,final String low )
        throws Exception {
        char c ;
        int type,s ;
        String column ;
        ArrayList<Object> list ;
        int len = in.size() ;
        
        for( int i = 0 ; i < len ; i ++ ) {
            // 単一条件をセット.
            if( eq( low,"and",( s = in.get( i ) ) ) || eq( low,"&",s ) ) {
                out.addWhere( new SqlSearchElement( MimdbSearchType.AND ) ) ;
            }
            else if( eq( low,"or",s ) || eq( low,"|",s ) ) {
                out.addWhere( new SqlSearchElement( MimdbSearchType.OR ) ) ;
            }
            else if( eq( low,"(",s ) ) {
                out.addWhere( new SqlSearchElement( MimdbSearchType.PAR_START ) ) ;
            }
            else if( eq( low,")",s ) ) {
                out.addWhere( new SqlSearchElement( MimdbSearchType.PAR_END ) ) ;
            }
            else {
                column = substring( low,s ) ;
                if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件は不正です:" + sql ) ;
                s = in.get( i ) ;
                
                // [=].
                if( ( c = low.charAt( startPos( s ) ) ) == '=' ) {
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[=]は不正です:" + sql ) ;
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_EQ ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_EQ,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // [!].
                else if( c == '!' ) {
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[!]は不正です:" + sql ) ;
                    if( !eq( low,"=",in.get( i ) ) ) {
                        throw new MimdbException( "where判別[" + column + "]条件[!]は xxx != 0 で定義が必要です:" + sql ) ;
                    }
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[!]は不正です:" + sql ) ;
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_NEQ ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_NEQ,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // [>].
                else if( c == '>' ) {
                    type = MimdbSearchType.TYPE_LBIG ;
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[>]は不正です:" + sql ) ;
                    if( eq( low,"=",in.get( i ) ) ) {
                        if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[>=]は不正です:" + sql ) ;
                        type = MimdbSearchType.TYPE_LEQBIG ;
                    }
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,type ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( column,type,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // [<].
                else if( c == '<' ) {
                    type = MimdbSearchType.TYPE_RBIG ;
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[<]は不正です:" + sql ) ;
                    if( eq( low,"=",in.get( i ) ) ) {
                        if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[<=]は不正です:" + sql ) ;
                        type = MimdbSearchType.TYPE_REQBIG ;
                    }
                    else if( eq( low,">",in.get( i ) ) ) {
                        if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[<>]は不正です:" + sql ) ;
                        type = MimdbSearchType.TYPE_NEQ ;
                    }
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,type ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( column,type,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // in, between 条件の場合.
                else if( eq( low,"in",s ) || eq( low,"between",s ) ) {
                    int wtype ;
                    String vtype ;
                    if( eq( low,"in",s ) ) {
                        wtype = MimdbSearchType.TYPE_IN ;
                        vtype = "IN" ;
                    }
                    else {
                        wtype = MimdbSearchType.TYPE_BETWEEN ;
                        vtype = "BETWEEN" ;
                    }
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は不正です:" + sql ) ;
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,wtype ) ) ;
                        continue ;
                    }
                    
                    // 通常.
                    if( !eq( low,"(",s ) ) {
                        throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は、xxx "+vtype+" ( ... ) の定義が必要です:" + sql ) ;
                    }
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は不正です:" + sql ) ;
                    
                    // preparedStatement.
                    if( eq( low,"?",in.get( i ) ) ) {
                        if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は不正です:" + sql ) ;
                        if( !eq( low,")",in.get( i ) ) ) {
                            throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は、xxx "+vtype+" (?) の定義が必要です:" + sql ) ;
                        }
                        out.addWhere( new SqlSearchElement( column,wtype ) ) ;
                        continue ;
                    }
                    // 中身が定義されていない場合.
                    else if( eq( low,")",( s = in.get( i ) ) ) ) {
                        throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は、xxx "+vtype+" ( ... ) の定義が必要です:" + sql ) ;
                    }
                    
                    // 中身の情報を取得.
                    list = new ArrayList<Object>( 16 ) ;
                    while( true ) {
                        list.add( _null( substring( sql,s ) ) ) ;
                        if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件["+vtype+"]は不正です:" + sql ) ;
                        if( eq( low,")",( s = in.get( i ) ) ) ) {
                            // 終端.
                            break ;
                        }
                    }
                    
                    // 条件セット.
                    out.addWhere( new SqlSearchElement( column,wtype,list ) ) ;
                    list = null ;
                    
                }
                // like条件の場合.
                else if( eq( low,"like",s ) ) {
                    if( ++ i >= len ) throw new MimdbException( "where判別[" + column + "]条件[LIKE]は不正です:" + sql ) ;
                    // preparedStatement.
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_LIKE ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( column,MimdbSearchType.TYPE_LIKE,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // offset,limit条件の場合.
                else if( "offset".equals( column ) || "limit".equals( column ) ) {
                    if( eq( low,"?",( s = in.get( i ) ) ) ) {
                        // パラメータがパラメータ設定の場合.
                        if( "offset".equals( column ) ) {
                            out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT ) ) ;
                        }
                        else {
                            out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT ) ) ;
                        }
                    }
                    else {
                        // パラメータが直接指定の場合.
                        if( "offset".equals( column ) ) {
                            out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
                        }
                        else {
                            out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
                        }
                    }
                }
                else {
                    throw new MimdbException( "where判別[" + column + "]条件は不正です:" + sql ) ;
                }
            }
        }
    }
    
    /** order byの条件追加. **/
    private static final void _orderBySetStatement( final QueryInfo out, final NumberList in,final String sql,final String low )
        throws Exception {
        int len = in.size() ;
        if( len == 0 ) {
            throw new MimdbException( "order定義において、カラム名が設定されていません:" + sql ) ;
        }
        int s ;
        int last = -1 ;
        // offset,limit条件が存在するかチェック.
        for( int i = 0 ; i < len ; i ++ ) {
            s = in.get( i ) ;
            if( eq( low,"offset",s ) || eq( low,"limit",s ) ) {
                boolean offsetFlag = eq( low,"offset",s ) ;
                s = in.get( i+1 ) ;
                if( eq( low,"?",s ) ) {
                    // パラメータがパラメータ設定の場合.
                    if( offsetFlag ) {
                        out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT ) ) ;
                    }
                }
                else {
                    // パラメータが直接指定の場合.
                    if( offsetFlag ) {
                        out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
                    }
                    else {
                        out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
                    }
                }
                // 検出条件以降は無視.
                if( last == -1 ) {
                    last = i ;
                }
            }
        }
        // offset,limit条件を検出した場合は、order by の検出長を削減.
        if( last != -1 ) {
            len = last ;
        }
        boolean desc = false ;
        for( int i = len-1 ; i >= 0 ; i -- ) {
            if( eq( low,"desc",( s = in.get( i ) ) ) ) {
                desc = true ;
            }
            else if( eq( low,"asc",s ) ) {
                desc = false ;
            }
            else {
                out.addSortColumn( substring( low,s ),desc ) ;
            }
        }
    }
    
    /** オフセット、リミット値条件処理. **/
    private static final int offLimit( final QueryInfo out,int off,int s,String sql,String low,int sqlLen )
        throws Exception {
        int off_limit ;
        if( eq( low,"offset",s ) ) {
            // offset.
            off_limit = 1 ;
        }
        else {
            // limit.
            off_limit = 2 ;
        }
        // パラメータ条件を取得.
        off = _noSpace( low,off+length( s ),sqlLen ) ;
        if( off == -1 ) {
            if( off_limit == 1 ) {
                throw new MimdbException( "offset条件が不明です:" + sql ) ;
            }
            throw new MimdbException( "limit条件が不明です:" + sql ) ;
        }
        s = _noSpaceString( low,off,sqlLen ) ;
        if( eq( low,"?",s ) ) {
            // パラメータがパラメータ設定の場合.
            if( off_limit == 1 ) {
                out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT ) ) ;
            }
            else {
                out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT ) ) ;
            }
        }
        else {
            // パラメータが直接指定の場合.
            if( off_limit == 1 ) {
                out.addWhere( new SqlSearchElement( "offset",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
            }
            else {
                out.addWhere( new SqlSearchElement( "limit",MimdbSearchType.OFF_LIMIT,_null( substring( sql,s ) ) ) ) ;
            }
        }
        return off+length( s ) ;
    }
    
    /** 文字条件がnullの場合は、null返却. **/
    private static final Object _null( String s ) {
        return ( s == null || "null".equals( s.toLowerCase() ) ) ? null : s ;
    }
    
}

