package org.maachang.comet.httpd.engine.script.scripts;

import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.comet.ServiceDef;
import org.maachang.comet.httpd.HttpdErrorDef;
import org.maachang.comet.httpd.HttpdRequest;
import org.maachang.comet.httpd.HttpdResponse;
import org.maachang.comet.httpd.HttpdStateException;
import org.maachang.comet.httpd.engine.HttpdDef;
import org.maachang.comet.httpd.engine.HttpdResponseInstance;
import org.maachang.comet.httpd.engine.HttpdTimestamp;
import org.maachang.comet.httpd.engine.comet.CometManager;
import org.maachang.comet.httpd.engine.script.Script;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.manager.GlobalManager;
import org.maachang.util.FileUtil;

/**
 * スクリプト管理関連処理.
 * 
 * @version 2008/11/13
 * @author masahito suzuki
 * @since MaachangComet 1.29
 */
public abstract class ScriptManager {
    private ScriptManager(){}
    
    /**
     * ログ.
     */
    private static final Log LOG = LogFactory.getLog( ScriptManager.class ) ;
    
    /**
     * スクリプト拡張子.
     */
    public static final String SCRIPT_PLUS = ".ms" ;
    
    /**
     * スクリプト埋め込みHTMLファイル拡張子.
     */
    public static final String SCRIPT_HTML_PLUS = ".mhtml" ;
    
    /**
     * テンプレート埋め込みHTML拡張子.
     */
    public static final String TEMPLATE_PLUS = ".mttml" ;
    
    /**
     * フィルタファイル名.
     */
    public static final String FILTER_NAME = "filter" ;
    
    /**
     * スクリプト名に対する実行処理 : Comet.
     */
    public static final String SCRIPT_BY_COMET = "Comet" ;
    
    /**
     * スクリプト名に対する実行処理 : Trigger
     */
    public static final String SCRIPT_BY_TRIGGER = "Trigger" ;
    
    /**
     * スクリプト名に対する実行処理 : Ajax.
     */
    public static final String SCRIPT_BY_AJAX = "Ajax" ;
    
    /**
     * スクリプト名に対する実行処理 : Report.
     */
    public static final String SCRIPT_BY_REPORT = "Report" ;
    
    /**
     * スクリプト名に対する実行処理 : Controller.
     */
    public static final String SCRIPT_BY_CONTROLLER = "Controller" ;
    
    /**
     * スクリプト名に対する実行処理 : RPC.
     */
    public static final String SCRIPT_BY_RPC = "Rpc" ;
    
    /**
     * スクリプト名に対する実行処理 : Inner.
     */
    public static final String SCRIPT_BY_INNER = "Inner" ;
    
    /**
     * スクリプト名に対する実行処理 : Filter.
     */
    public static final String SCRIPT_BY_FILTER = "Filter" ;
    
    /**
     * スクリプト名に対する実行タイプ : なし.
     */
    public static final int SCRIPT_TYPE_BY_NOT = -1 ;
    
    /**
     * スクリプト名に対する実行タイプ : Comet.
     */
    public static final int SCRIPT_TYPE_BY_COMET = 0 ;
    
    /**
     * スクリプト名に対する実行タイプ : Trigger.
     */
    public static final int SCRIPT_TYPE_BY_TRIGGER = 1 ;
    
    /**
     * スクリプト名に対する実行タイプ : Ajax.
     */
    public static final int SCRIPT_TYPE_BY_AJAX = 2 ;
    
    /**
     * スクリプト名に対する実行タイプ : Controller.
     */
    public static final int SCRIPT_TYPE_BY_CONTROLLER = 3 ;
    
    /**
     * スクリプト名に対する実行タイプ : Jsonp.
     */
    public static final int SCRIPT_TYPE_BY_RPC = 4 ;
    
    /**
     * スクリプト名に対する実行タイプ : Report.
     */
    public static final int SCRIPT_TYPE_BY_REPORT = 6 ;
    
    /**
     * スクリプト名に対する実行タイプ : スクリプト用HTML.
     */
    public static final int SCRIPT_TYPE_BY_HTML = 10 ;
    
    /**
     * スクリプト名に対する実行タイプ : Model.
     */
    public static final int SCRIPT_TYPE_BY_MODEL = 20 ;
    
    /**
     * スクリプト名に対する実行タイプ : 外部ライブラリ.
     */
    public static final int SCRIPT_TYPE_BY_LIB = 30 ;
    
    /**
     * スクリプト名に対する実行タイプ : Inner.
     */
    public static final int SCRIPT_TYPE_BY_INNER = 31 ;
    
    /**
     * スクリプト名に対する実行タイプ : Filter.
     */
    public static final int SCRIPT_TYPE_BY_FILTER = 98 ;
    
    /**
     * スクリプト名に対する実行タイプ : その他.
     */
    public static final int SCRIPT_TYPE_BY_ETC = 99 ;
    
    /**
     * 指定名から、スクリプトオブジェクトを生成.
     * @param name 対象のスクリプト名を設定します.
     * @param dir 対象のカレントディレクトリを設定します.
     * @return Script 対象のスクリプト情報が返されます.
     * @exception Exception 例外.
     */
    public static final Script getScript( String name,String dir ) throws Exception {
        if( name == null || ( name = name.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "指定されたスクリプトパスは不正です" ) ;
        }
        Script ret ;
        String key = cutScriptPlus( name ) ;
        if( name.endsWith( "/" + FILTER_NAME ) == true ) {
            ret = new FilterScript( key,dir ) ;
        }
        else if( name.endsWith( SCRIPT_HTML_PLUS ) == true ) {
            ret = new MhtmlScript( name,dir ) ;
        }
        else if( name.endsWith( TEMPLATE_PLUS ) == true ) {
            ret = new TemplateScript( name ) ;
        }
        else {
            int type = getScriptFileType( key+SCRIPT_PLUS ) ;
            switch( type ) {
                case SCRIPT_TYPE_BY_COMET :
                    ret = new CometScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_TRIGGER :
                    ret = new TriggerScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_AJAX :
                    ret = new AjaxScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_REPORT :
                    ret = new ReportScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_CONTROLLER :
                    ret = new ControllerScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_RPC :
                    ret = new RpcScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_INNER :
                    ret = new InnerScript( key,dir ) ;
                    break ;
                case SCRIPT_TYPE_BY_FILTER :
                    ret = new FilterScript( key,dir ) ;
                    break ;
                default :
                    ret = new EtcScript( key,dir ) ;
                    break ;
            }
        }
        return ret ;
    }
    
    /**
     * スクリプト実行処理.
     * @param request 対象のリクエストを設定します.
     * @param appDirectory アプリケーションディレクトリを設定します.
     * @return response 対象のレスポンスを設定します.
     * @exception Exception 例外.
     */
    public static final HttpdResponse execution( HttpdRequest request,String appDirectory )
        throws Exception {
        if( request == null ) {
            throw new HttpdStateException( HttpdErrorDef.HTTP11_400,
                "リクエストが不正です" ) ;
        }
        String path = ScriptManager.trimScriptPath( request.getUrlPath() ) ;
        if( path == null ) {
            throw new HttpdStateException( HttpdErrorDef.HTTP11_404,
                "指定パス[" + request.getUrlPath() + "]の内容は存在しません" ) ;
        }
        HttpdResponse ret = null ;
        try {
            // MTTML(Template)の場合.
            if( path.endsWith( ScriptManager.TEMPLATE_PLUS ) == true ) {
                ret = HttpdResponseInstance.createResponse( request,request.getUrlPath(),
                    request.getKeepAliveTimeout(),request.getKeepAliveCount() ) ;
                ret.setHttpCache( true ) ;
                ret.setHttpClose( false ) ;
                // フィルター処理を実行.
                if( executionFilter( ret,path,request,appDirectory ) == false ) {
                    // フィルタ実行で、レスポンス出力されていない場合は、
                    // 対象の処理を行う.
                    HashMap<String,Object> params = new HashMap<String,Object>() ;
                    ret.setCookieSession( request ) ;
                    ret.getHeader().addHeader( HttpdDef.VALUE_LAST_UPDATE,
                        HttpdTimestamp.getTimestamp( TemplateScript.getTime( path ) ) ) ;
                    params.put( ScriptDef.SCRIPT_BY_REQUEST,request ) ;
                    params.put( ScriptDef.SCRIPT_BY_RESPONSE,ret ) ;
                    Object res = TargetScript.executionScript( path,request,params ) ;
                    resultError( path,res ) ;
                }
            }
            // それ以外の処理の場合.
            else {
                // 指定ファイルが存在するかチェック.
                String fullPath = appDirectory+path.substring( 1,path.length() ) ;
                // 指定ファイル名が存在しない場合は、コントローラ/mhtmlで探す.
                if( FileUtil.isFileExists( fullPath+ScriptManager.SCRIPT_PLUS ) == false ) {
                    
                    // 先にコントローラ名として変換して再度チェック.
                    String controllerPath = path + ScriptManager.SCRIPT_BY_CONTROLLER ;
                    fullPath = appDirectory+controllerPath.substring( 1,controllerPath.length() ) ;
                    
                    // コントローラー情報が存在しない場合.
                    if( FileUtil.isFileExists( fullPath+ScriptManager.SCRIPT_PLUS ) == false ) {
                        // mhtml名を探す.
                        path = path + ScriptManager.SCRIPT_HTML_PLUS ;
                        fullPath = appDirectory+path.substring( 1,path.length() ) ;
                        // mhtmlファイルが存在しない場合.
                        if( FileUtil.isFileExists( fullPath ) == false ) {
                            throw new HttpdStateException( HttpdErrorDef.HTTP11_404,
                                "指定パス[" + request.getUrlPath() + "]の内容は存在しません" ) ;
                        }
                    }
                    // コントローラとして存在する場合は、コントローラとして処理する.
                    else {
                        path = controllerPath + ScriptManager.SCRIPT_PLUS ;
                        fullPath += ScriptManager.SCRIPT_PLUS ;
                    }
                }
                // 指定ファイル名が存在する場合.
                else {
                    path += ScriptManager.SCRIPT_PLUS ;
                    fullPath += ScriptManager.SCRIPT_PLUS ;
                }
                // 指定ファイルの読み込み権限が存在しない場合.
                if( FileUtil.isRead( fullPath ) == false ) {
                    throw new HttpdStateException( HttpdErrorDef.HTTP11_403,
                        "指定パス[" + request.getUrlPath() + "]は読み込み権限がありません" ) ;
                }
                long time = FileUtil.getLastTime( fullPath ) ;
                fullPath = null ;
                int type = ScriptManager.getScriptFileType( path ) ;
                switch( type ) {
                    // コメット処理の場合.
                    case ScriptManager.SCRIPT_TYPE_BY_COMET : {
                        // コメットの場合は、Body情報は使わない.
                        request.setBody( null ) ;
                        // コメットの場合は、コメットマネージャで処理.
                        CometManager man = ( CometManager )GlobalManager.getValue(
                            ServiceDef.MANAGER_BY_COMET ) ;
                        man.cometRequest( request.copy() ) ;
                        // コメットフラグをON.
                        request.getConnectionInfo().cometOn() ;
                        if( LOG.isDebugEnabled() ) {
                            LOG.debug( "set Comet...groupId:" + request.getQuery().getParam( "groupId" ) ) ;
                        }
                    }
                    break ;
                    // 実行可能な条件.
                    case ScriptManager.SCRIPT_TYPE_BY_TRIGGER :
                    case ScriptManager.SCRIPT_TYPE_BY_AJAX :
                    case ScriptManager.SCRIPT_TYPE_BY_REPORT :
                    case ScriptManager.SCRIPT_TYPE_BY_CONTROLLER :
                    case ScriptManager.SCRIPT_TYPE_BY_RPC : {
                        
                        // コメット以外の場合は、スクリプト処理を実施.
                        ret = HttpdResponseInstance.createResponse( request,request.getUrlPath(),
                            request.getKeepAliveTimeout(),request.getKeepAliveCount() ) ;
                        ret.setHttpCache( true ) ;
                        ret.setHttpClose( false ) ;
                        
                        // filter実行を行い、OutputStream出力していない場合は、処理する.
                        if( executionFilter( ret,path,request,appDirectory ) == false ) {
                            if( type == ScriptManager.SCRIPT_TYPE_BY_AJAX ||
                                type == ScriptManager.SCRIPT_TYPE_BY_REPORT ||
                                type == ScriptManager.SCRIPT_TYPE_BY_RPC ) {
                                // Ajax/Jsonp/AjaxRPCは、基本的にCacheをOFFにする.
                                ret.setHttpCache( true ) ;
                            }
                            HashMap<String,Object> params = new HashMap<String,Object>() ;
                            ret.setCookieSession( request ) ;
                            ret.getHeader().addHeader( HttpdDef.VALUE_LAST_UPDATE,
                                HttpdTimestamp.getTimestamp( time ) ) ;
                            params.put( ScriptDef.SCRIPT_BY_REQUEST,request ) ;
                            params.put( ScriptDef.SCRIPT_BY_RESPONSE,ret ) ;
                            Object res = TargetScript.executionScript( path,request,params ) ;
                            resultError( path,res ) ;
                        }
                    }
                    break ;
                    // 実行不可能な条件.
                    case ScriptManager.SCRIPT_TYPE_BY_INNER :
                    case ScriptManager.SCRIPT_TYPE_BY_FILTER : {
                        // エラー403.
                        throw new HttpdStateException( HttpdErrorDef.HTTP11_403,
                            "指定パス[" + path + "]の内容はアクセスできません" ) ;
                    }
                    // その他.
                    default : {
                        
                        // MHTMLファイルの場合.
                        if( path.endsWith( ScriptManager.SCRIPT_HTML_PLUS ) == true ) {
                            
                            // MHTMLファイルの実行.
                            ret = HttpdResponseInstance.createResponse( request,path,
                                request.getKeepAliveTimeout(),request.getKeepAliveCount() ) ;
                            ret.setHttpCache( true ) ;
                            ret.setHttpClose( false ) ;
                            
                            // filter実行を行い、OutputStream出力していない場合は、処理する.
                            if( executionFilter( ret,path,request,appDirectory ) == false ) {
                                HashMap<String,Object> params = new HashMap<String,Object>() ;
                                ret.setCookieSession( request ) ;
                                ret.getHeader().addHeader( HttpdDef.VALUE_LAST_UPDATE,
                                    HttpdTimestamp.getTimestamp( time ) ) ;
                                params.put( ScriptDef.SCRIPT_BY_REQUEST,request ) ;
                                params.put( ScriptDef.SCRIPT_BY_RESPONSE,ret ) ;
                                Object res = TargetScript.executionScript( path,request,params ) ;
                                resultError( path,res ) ;
                            }
                        }
                        // その他では、404エラーとする.
                        else {
                            throw new HttpdStateException( HttpdErrorDef.HTTP11_404,
                                "指定パス[" + path + "]の内容は存在しません" ) ;
                        }
                    }
                    break ;
                }
            }
            return ret ;
        } catch( Exception e ) {
            throw e ;
        } catch( Error e ) {
            throw e ;
        }
    }
    
    /**
     * スクリプト拡張子を除外.
     * @param name 対象の名前を設定します.
     * @return String スクリプト拡張子[.ms]を除外した内容が返されます.
     */
    public static final String cutScriptPlus( String name ) {
        if( name.endsWith( SCRIPT_PLUS ) == true ) {
            return name.substring( 0,name.length()-SCRIPT_PLUS.length() ) ;
        }
        else {
            return name ;
        }
    }
    
    /**
     * ScriptPath名を整形.
     * param path 対象のスクリプトパス名を設定します.
     * @return Stirng 整形されたスクリプトパス名が返されます.
     */
    public static final String trimScriptPath( String path ) {
        String lower = path.toLowerCase() ;
        if( path.endsWith( "/" ) == true ) {
            return path + "index" ;
        }
        if( lower.endsWith( ScriptManager.SCRIPT_PLUS ) == true ) {
            path = path.substring( 0,path.length()-SCRIPT_PLUS.length() ) ;
        }
        else if( lower.endsWith( SCRIPT_HTML_PLUS ) == true ) {
            path = path.substring( 0,path.length()-SCRIPT_HTML_PLUS.length() ) ;
        }
        return path ;
    }
    
    /**
     * 実行可能なフィルタパスを検索.
     * @param path 対象のパスを設定します.
     * @param dir 対象のカレントディレクトリを設定します.
     * @return String 実行可能なパスが返されます.
     * @exception Exception 例外.
     */
    public static final String searchFilterPath( String path,String dir )
        throws Exception {
        for( ;; ) {
            path = path.substring( 0,path.length() - FileUtil.getFileName( path ).length() ) ;
            String filterPath = new StringBuilder().
                append( path ).
                append( ScriptManager.FILTER_NAME ).
                append( ScriptManager.SCRIPT_PLUS ).
                toString() ;
            String fullPath = FileUtil.getFullPath(
                new StringBuilder().append( dir ).append( filterPath ).toString() ) ;
            if( FileUtil.isFileExists( fullPath ) == true && FileUtil.isRead( fullPath ) == true ) {
                return filterPath ;
            }
            if( path.length() <= 1 ) {
                return null ;
            }
            path = path.substring( 0,path.length() -1 ) ;
        }
    }
    
    /**
     * スクリプト組み込みHTMLを含む、スクリプトファイルタイプを取得.
     * <BR><BR>
     * スクリプト組み込みHTMLを含む、スクリプトファイルタイプが返されます.
     * <BR>
     * @param name 対象のスクリプト名を設定します.
     * @return int スクリプトファイルタイプが返されます.
     */
    public static final int getScriptByAllTyep( String name ) {
        if( name.endsWith( SCRIPT_HTML_PLUS ) == true ) {
            return SCRIPT_TYPE_BY_HTML ;
        }
        if( name.endsWith( SCRIPT_PLUS ) == false ) {
            name += SCRIPT_PLUS ;
        }
        return getScriptFileType( name ) ;
    }
    
    /**
     * スクリプトファイルタイプを取得.
     * <BR><BR>
     * スクリプトファイルタイプが返されます.
     * <BR>
     * @param name 対象のスクリプト名を設定します.
     * @return int スクリプトファイルタイプが返されます.
     */
    public static final int getScriptFileType( String name ) {
        if( name == null || ( name = name.trim() ).length() <= 0 ) {
            return SCRIPT_TYPE_BY_NOT ;
        }
        String check = null ;
        if( name.endsWith( SCRIPT_PLUS ) == false ) {
            return SCRIPT_TYPE_BY_NOT ;
        }
        else {
            check = name.substring( 0,name.length()-SCRIPT_PLUS.length() ) ;
        }
        if( check.endsWith( SCRIPT_BY_COMET ) ) {
            return SCRIPT_TYPE_BY_COMET ;
        }
        if( check.endsWith( SCRIPT_BY_TRIGGER ) ) {
            return SCRIPT_TYPE_BY_TRIGGER ;
        }
        if( check.endsWith( SCRIPT_BY_AJAX ) ) {
            return SCRIPT_TYPE_BY_AJAX ;
        }
        if( check.endsWith( SCRIPT_BY_REPORT ) ) {
            return SCRIPT_TYPE_BY_REPORT ;
        }
        if( check.endsWith( SCRIPT_BY_CONTROLLER ) ) {
            return SCRIPT_TYPE_BY_CONTROLLER ;
        }
        if( check.endsWith( SCRIPT_BY_RPC ) ) {
            return SCRIPT_TYPE_BY_RPC ;
        }
        if( check.endsWith( SCRIPT_BY_INNER ) ) {
            return SCRIPT_TYPE_BY_INNER ;
        }
        if( check.endsWith( SCRIPT_BY_FILTER ) ) {
            return SCRIPT_TYPE_BY_FILTER ;
        }
        return SCRIPT_TYPE_BY_NOT ;
    }
    
    /**
     * スクリプトタイプから、スクリプト名を取得.
     * <BR><BR>
     * スクリプトタイプから、スクリプト名を取得します.
     * <BR>
     * @param type 対象のタイプを設定します.
     * @return String スクリプトタイプが返されます.
     */
    public static final String getTypeByString( int type ) {
        switch( type ) {
            case SCRIPT_TYPE_BY_NOT: return "etc" ;
            case SCRIPT_TYPE_BY_COMET: return "comet" ;
            case SCRIPT_TYPE_BY_TRIGGER: return "trigger" ;
            case SCRIPT_TYPE_BY_AJAX: return "ajax" ;
            case SCRIPT_TYPE_BY_REPORT: return "report" ;
            case SCRIPT_TYPE_BY_CONTROLLER: return "controller" ;
            case SCRIPT_TYPE_BY_RPC: return "rpc" ;
            case SCRIPT_TYPE_BY_HTML: return "html" ;
            case SCRIPT_TYPE_BY_MODEL: return "model" ;
            case SCRIPT_TYPE_BY_LIB: return "lib" ;
            case SCRIPT_TYPE_BY_INNER: return "inner" ;
            case SCRIPT_TYPE_BY_ETC: return "etc" ;
        }
        return "etc" ;
    }
    
    /**
     * 拡張子が、スクリプト条件であるかチェック.
     * <BR><BR>
     * 拡張子が、スクリプト条件であるかチェックします.
     * <BR>
     * @param name チェック対象名を設定します.
     * @return boolean [true]の場合、スクリプト拡張子です.
     */
    public static final boolean isScript( String name ) {
        if( name == null || ( name = name.trim() ).length() <= 0 ) {
            return false ;
        }
        name = name.toLowerCase() ;
        if( name.endsWith( SCRIPT_PLUS ) == true ||
            name.endsWith( SCRIPT_HTML_PLUS ) == true ) {
            return true ;
        }
        return false ;
    }
    
    /**
     * Filter実行.
     */
    private static final boolean executionFilter( HttpdResponse response,String path,HttpdRequest request,String appDirectory )
        throws Exception {
        String filterPath = ScriptManager.searchFilterPath( path,appDirectory ) ;
        if( filterPath == null ) {
            return false ;
        }
        String fullPath = FileUtil.getFullPath(
            new StringBuilder().append( appDirectory ).append( filterPath ).toString() ) ;
        if( FileUtil.isFileExists( fullPath ) == true &&
            FileUtil.isRead( fullPath ) == true ) {
            HashMap<String,Object> params = new HashMap<String,Object>() ;
            response.setCookieSession( request ) ;
            response.getHeader().addHeader( HttpdDef.VALUE_LAST_UPDATE,
                HttpdTimestamp.getTimestamp( FileUtil.getLastTime( fullPath ) ) ) ;
            params.put( ScriptDef.SCRIPT_BY_REQUEST,request ) ;
            params.put( ScriptDef.SCRIPT_BY_RESPONSE,response ) ;
            Object res = TargetScript.executionScript( filterPath,request,params ) ;
            if( response.isOutputStream() == true ||
                ScriptDef.SCRIPT_RESULT_REDIRECT.equals( res ) ||
                ScriptDef.SCRIPT_RESULT_FORWARD.equals( res ) ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * スクリプト戻り値でエラーが発生したかチェック.
     */
    private static final void resultError( String path,Object res )
        throws Exception {
        if( res != null && res instanceof String &&
            ( ( String )res ).startsWith( ScriptDef.SCRIPT_RESULT_ERROR ) ) {
            String s = ( String )res ;
            s = s.substring( ScriptDef.SCRIPT_RESULT_ERROR.length() ) ;
            int state = -1 ;
            try {
                state = Integer.parseInt( s ) ;
            } catch( Exception e ) {
                state = 500 ;
            }
            throw new HttpdStateException( state,
                "指定パス[" + path + "]の処理でエラーが発生しました" ) ;
        }
    }
}
