package org.maachang.comet.httpd.engine;

import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.comet.MaachangDef;
import org.maachang.comet.conf.ServiceDef;
import org.maachang.comet.httpd.HttpdErrorDef;
import org.maachang.comet.httpd.HttpdExecutionManager;
import org.maachang.comet.httpd.HttpdRequest;
import org.maachang.comet.httpd.HttpdResponse;
import org.maachang.comet.httpd.HttpdStateException;
import org.maachang.comet.httpd.engine.comet.Comet;
import org.maachang.comet.httpd.engine.comet.CometManager;
import org.maachang.comet.httpd.engine.script.BaseModel;
import org.maachang.comet.httpd.engine.script.BaseModelImpl;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.comet.httpd.engine.script.TargetScript;
import org.maachang.comet.httpd.engine.script.scripts.CometTriggerScript;
import org.maachang.manager.GlobalManager;
import org.maachang.util.FileUtil;

/**
 * HTTPDスクリプト実行管理.
 * 
 * @version 2007/08/30
 * @author masahito suzuki
 * @since MaachangComet 1.00
 */
class HttpdScriptManager implements HttpdExecutionManager {
    
    /**
     * LOG.
     */
    private static final Log LOG = LogFactory.getLog( HttpdScriptManager.class ) ;
    
    /**
     * Applicationディレクトリ.
     */
    private String appDirectory = null ;
    
    /**
     * コンストラクタ.
     */
    private HttpdScriptManager() {
        
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * カレントディレクトリを指定してオブジェクトを生成します.
     * <BR>
     * @param currentDirectory 対象のカレントディレクトリを設定します.
     * @exception Exception 例外.
     */
    public HttpdScriptManager( String currentDirectory )
        throws Exception {
        this.appDirectory = new StringBuilder().
            append( ScriptDef.trimCurrentDirectory( currentDirectory ) ).
            append( MaachangDef.DIRECTORY_APPLICATION ).toString() ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        appDirectory = null ;
    }
    
    /**
     * 指定内容を取得.
     * <BR><BR>
     * 指定された内容を取得します.
     * <BR>
     * @param request 対象のリクエストを設定します.
     * @param timeout 対象のタイムアウト値を設定します.
     * @param count 対象のカウント値を設定します.
     * @return response 対象のレスポンスを設定します.
     * @exception Exception 例外.
     */
    public HttpdResponse get( HttpdRequest request )
        throws Exception {
        if( request == null ) {
            throw new HttpdStateException( HttpdErrorDef.HTTP11_400,
                "リクエストが不正です" ) ;
        }
        String path = trimPath( request.getUrlPath() ) ;
        if( path == null ) {
            throw new HttpdStateException( HttpdErrorDef.HTTP11_404,
                "指定パス[" + request.getUrlPath() + "]の内容は存在しません" ) ;
        }
        HttpdResponse ret = null ;
        try {
            // 指定ファイルが存在するかチェック.
            String fullPath = appDirectory+path.substring( 1,path.length() ) ;
            if( FileUtil.isFileExists( fullPath+ScriptDef.SCRIPT_PLUS ) == false ) {
                
                // 先にコントローラ名として変換して再度チェック.
                String controllerPath = path + ScriptDef.SCRIPT_BY_CONTROLLER ;
                fullPath = appDirectory+controllerPath.substring( 1,controllerPath.length() ) ;
                
                // コントローラー情報が存在しない場合.
                if( FileUtil.isFileExists( fullPath+ScriptDef.SCRIPT_PLUS ) == false ) {
                    // mhtml名を探す.
                    path = path + ScriptDef.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 + ScriptDef.SCRIPT_PLUS ;
                    fullPath += ScriptDef.SCRIPT_PLUS ;
                }
            }
            else {
                path += ScriptDef.SCRIPT_PLUS ;
                fullPath += ScriptDef.SCRIPT_PLUS ;
            }
            // 指定ファイルの読み込み権限が存在しない場合.
            if( FileUtil.isRead( fullPath ) == false ) {
                throw new HttpdStateException( HttpdErrorDef.HTTP11_403,
                    "指定パス[" + request.getUrlPath() + "]は読み込み権限がありません" ) ;
            }
            long time = FileUtil.getLastTime( fullPath ) ;
            fullPath = null ;
            TargetScript script = null ;
            int type = ScriptDef.getScriptFileType( path ) ;
            switch( type ) {
                // URL指定で実行可能なコメットオブジェクトタイプ.
                case ScriptDef.SCRIPT_TYPE_BY_COMET : {
                    
                    // コメットトリガーが存在する場合は、その条件で処理.
                    String triggerPath = CometTriggerScript.getTriggerName( path ) ;
                    if( triggerPath != null ) {
                        // コメットトリガーを先に実行する.
                        ret = executionCometTriger( request,triggerPath,path ) ;
                    }
                    // コメットトリガーが実行されていない場合は、コメット登録.
                    if( ret == null ) {
                        // コメットの場合は、Body情報は使えない.
                        request.setBody( null ) ;
                        // コメットの場合は、コメットマネージャで処理.
                        CometManager man = ( CometManager )GlobalManager.getValue(
                            ServiceDef.MANAGER_BY_COMET ) ;
                        man.cometRequest( request ) ;
                        
                        // コネクションを維持する.
                        request.setKeepAliveTimeout( -1L ) ;
                    }
                }
                break ;
                // URL指定で実行可能オブジェクトタイプ.
                case ScriptDef.SCRIPT_TYPE_BY_AJAX :
                case ScriptDef.SCRIPT_TYPE_BY_CONTROLLER :
                case ScriptDef.SCRIPT_TYPE_BY_JSONP :
                case ScriptDef.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 ) == false ) {
                        if( type == ScriptDef.SCRIPT_TYPE_BY_AJAX ||
                            type == ScriptDef.SCRIPT_TYPE_BY_JSONP ||
                            type == ScriptDef.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 ) ;
                        script = new TargetScript( path,request ) ;
                        Object res = script.execution( params ) ;
                        resultError( path,res ) ;
                    }
                }
                break ;
                // URL指定で実行不可能オブジェクトタイプ.
                case ScriptDef.SCRIPT_TYPE_BY_COMET_TRIGGER :
                case ScriptDef.SCRIPT_TYPE_BY_INNER :
                case ScriptDef.SCRIPT_TYPE_BY_FILTER : {
                    // エラー403.
                    throw new HttpdStateException( HttpdErrorDef.HTTP11_403,
                        "指定パス[" + path + "]の内容はアクセスできません" ) ;
                }
                default : {
                    
                    // MHTMLファイルの場合.
                    if( path.endsWith( ScriptDef.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 ) == 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 ) ;
                            script = new TargetScript( path,request ) ;
                            Object res = script.execution( params ) ;
                            resultError( path,res ) ;
                        }
                    }
                    // その他では、404エラーとする.
                    else {
                        throw new HttpdStateException( HttpdErrorDef.HTTP11_404,
                            "指定パス[" + path + "]の内容は存在しません" ) ;
                    }
                }
                break ;
            }
            return ret ;
        } catch( Exception e ) {
            if( ret != null ) {
                ret.cancel() ;
            }
            throw e ;
        } catch( Error e ) {
            if( ret != null ) {
                ret.cancel() ;
            }
            throw e ;
        }
    }
    
    /**
     * 指定パスのファイル日付を取得.
     * <BR><BR>
     * 指定パスのファイル日付を取得します.
     * <BR>
     * @parma path 対象のパスを設定します.
     * @return long ファイル日付が返されます.
     */
    public long getFileTime( String path ) {
        path = trimPath( path ) ;
        if( path == null ) {
            return -1L ;
        }
        String fullPath = appDirectory+path.substring( 1,path.length() ) ;
        long ret = -1L ;
        // 通常名でチェック.
        if( ( ret = FileUtil.getLastTime( fullPath+ScriptDef.SCRIPT_PLUS ) ) != -1L ) {
            return ret ;
        }
        // コントローラ名でチェック.
        if( ( ret = FileUtil.getLastTime( fullPath+ScriptDef.SCRIPT_BY_CONTROLLER+ScriptDef.SCRIPT_PLUS ) ) != -1L ) {
            return ret ;
        }
        // MHTML名でチェック.
        if( ( ret = FileUtil.getLastTime( fullPath+ScriptDef.SCRIPT_HTML_PLUS ) ) != -1L ) {
            return ret ;
        }
        return -1L ;
    }
    
    /**
     * 指定パスの内容が存在するかチェック.
     * <BR><BR>
     * 指定パスの内容が存在するかチェックします.
     * <BR>
     * @param path 対象のパスを設定します.
     * @return boolean [true]の場合、存在します.
     */
    public boolean isPath( String path ) {
        path = trimPath( path ) ;
        if( path == null ) {
            return false ;
        }
        String fullPath = appDirectory+path.substring( 1,path.length() ) ;
        // 通常名でチェック.
        if( FileUtil.isFileExists( fullPath+ScriptDef.SCRIPT_PLUS ) == true ) {
            return true ;
        }
        // コントローラ名でチェック.
        if( FileUtil.isFileExists( fullPath+ScriptDef.SCRIPT_BY_CONTROLLER+ScriptDef.SCRIPT_PLUS ) == true ) {
            return true ;
        }
        // MHTML名でチェック.
        if( FileUtil.isFileExists( fullPath+ScriptDef.SCRIPT_HTML_PLUS ) == true ) {
            return true ;
        }
        return false ;
    }
    
    /**
     * 指定パスの内容が読み込み可能かチェック.
     * <BR><BR>
     * 指定パスの内容が読み込み可能かチェックします.
     * <BR>
     * @param path 対象のパスを設定します.
     * @return boolean [true]の場合、読み込み可能です.
     */
    public boolean isRead( String path ) {
        path = trimPath( path ) ;
        if( path == null ) {
            return false ;
        }
        String fullPath = appDirectory+path.substring( 1,path.length() ) ;
        // 通常名でチェック.
        if( FileUtil.isRead( fullPath+ScriptDef.SCRIPT_PLUS ) == true ) {
            return true ;
        }
        // コントローラ名でチェック.
        if( FileUtil.isRead( fullPath+ScriptDef.SCRIPT_BY_CONTROLLER+ScriptDef.SCRIPT_PLUS ) == true ) {
            return true ;
        }
        // MHTML名でチェック.
        if( FileUtil.isRead( fullPath+ScriptDef.SCRIPT_HTML_PLUS ) == true ) {
            return true ;
        }
        return false ;
    }
    
    /**
     * Path名を整形.
     */
    private static final String trimPath( String path ) {
        String lower = path.toLowerCase() ;
        if( path.endsWith( "/" ) == true ) {
            return path + "index" ;
        }
        //if( path.equals( "/" ) == true ) {
        //    return null ;
        //}
        if( lower.endsWith( ScriptDef.SCRIPT_PLUS ) == true ) {
            path = path.substring( 0,path.length()-ScriptDef.SCRIPT_PLUS.length() ) ;
        }
        else if( lower.endsWith( ScriptDef.SCRIPT_HTML_PLUS ) == true ) {
            path = path.substring( 0,path.length()-ScriptDef.SCRIPT_HTML_PLUS.length() ) ;
        }
        return path ;
    }
    
    /**
     * コメットトリガー正常終了条件.
     */
    private static final String SUCCESS_TRIGGER = "success" ;
    
    /**
     * コメットトリガー実行.
     */
    private HttpdResponse executionCometTriger( HttpdRequest request,String trigerPath,String path )
        throws Exception {
        HttpdResponse ret = null ;
        BaseModel baseModel = new BaseModelImpl() ;
        try {
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "... execution Trigger[" + trigerPath + "]:" +
                    request.getQuery().getParam( "groupId" ) ) ;
            }
            Object res = Comet.oneSend( null,baseModel,request,
                trigerPath,request.getQuery().getParam( "groupId" ) ) ;
            if( baseModel.isCreate() ) {
                try {
                    baseModel.commit() ;
                } catch( Exception ee ) {
                }
            }
            if( res != null && res instanceof String && SUCCESS_TRIGGER.equals( res ) ) {
                if( LOG.isDebugEnabled() ) {
                    LOG.debug( "... exit[trigger] - comet:" +
                        request.getQuery().getParam( "groupId" ) ) ;
                }
                ret = HttpdResponseInstance.createResponse( request,request.getUrlPath(),
                    HttpdErrorDef.HTTP11_200,request.getKeepAliveTimeout(),
                    request.getKeepAliveCount() ) ;
                ret.setHttpCache( true ) ;
                ret.setHttpClose( false ) ;
                request.setBody( null ) ;
                Comet.oneSend( ret,baseModel,request,
                    path,request.getQuery().getParam( "groupId" ) ) ;
            }
            return ret ;
        } catch( Exception e ) {
            if( ret != null ) {
                ret.cancel() ;
            }
            throw e ;
        } catch( Error e ) {
            if( ret != null ) {
                ret.cancel() ;
            }
            throw e ;
        } finally {
            baseModel.clear() ;
        }
    }
    
    /**
     * Filter実行.
     */
    private final boolean executionFilter( HttpdResponse response,String path,HttpdRequest request )
        throws Exception {
        String filterPath = getExecutionFilterPath( path ) ;
        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 ) ;
            TargetScript script = new TargetScript( filterPath,request ) ;
            Object res = script.execution( params ) ;
            if( response.isOutputStream() == true ||
                ScriptDef.SCRIPT_RESULT_REDIRECT.equals( res ) ||
                ScriptDef.SCRIPT_RESULT_FORWARD.equals( res ) ) {
                return true ;
            }
        }
        return false ;
    }
    
    /**
     * 実行可能なフィルタファイルパスを探す.
     */
    private final String getExecutionFilterPath( String path )
        throws Exception {
        for( ;; ) {
            path = path.substring( 0,path.length() - FileUtil.getFileName( path ).length() ) ;
            String filterPath = new StringBuilder().
                append( path ).
                append( ScriptDef.FILTER_NAME ).
                append( ScriptDef.SCRIPT_PLUS ).
                toString() ;
            String fullPath = FileUtil.getFullPath(
                new StringBuilder().append( appDirectory ).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 ) ;
        }
    }
    
    /**
     * スクリプト戻り値でエラーが発生したかチェック.
     */
    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 + "]の処理でエラーが発生しました" ) ;
        }
    }
    
}
