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

import java.util.Map;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.comet.MaachangDef;
import org.maachang.comet.ServiceDef;
import org.maachang.comet.httpd.engine.script.DummyBaseModel;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.comet.httpd.engine.script.SrcScript;
import org.maachang.comet.httpd.engine.script.SrcScriptList;
import org.maachang.comet.httpd.engine.script.dao.DaoFactory;
import org.maachang.comet.httpd.engine.script.js.JsDef;
import org.maachang.comet.httpd.engine.script.scripts.ScriptManager;
import org.maachang.jsr.script.api.ApiManager;
import org.maachang.jsr.script.api.ExternalBindings;
import org.maachang.manager.GlobalManager;
import org.maachang.util.FileUtil;

/**
 * モデルAPIマネージャ.
 * 
 * @version 2008/08/11
 * @author masahito suzuki
 * @since MaachangComet 1.23
 */
class ModelApiManager {
    
    /**
     * LOG.
     */
    private static final Log LOG = LogFactory.getLog( ModelApiManager.class ) ;
    
    /**
     * スクリプトエンジン名.
     */
    protected static final String ENGINE_NAME = "js" ;
    
    /**
     * スクリプト実行名.
     */
    protected static final String SCRIPT_NAME = "[model_api]" ;
    
    /**
     * モデルファイルフッダ名.
     */
    protected static final String MODEL_NAME_FOODER = "Model" ;
    
    /**
     * カスタムモデルスクリプト.
     */
    protected static final String MODEL_CUSTOM = "Custom" ;
    
    /**
     * 読み込み対象ディレクトリ.
     */
    private static final String READ_DIR = baseDir() ;
    
    /**
     * 再読み込み確認時間.
     */
    private static final long CHECK_TIME = 30000L ;
    
    /**
     * 前回再読み込み時間.
     */
    private long beforeTime = -1L ;
    
    /**
     * モデル管理コード.
     */
    private String modelCode = null ;
    
    /**
     * スクリプト内容.
     */
    private SrcScript srcScript = null ;
    
    /**
     * デフォルトパッケージ更新ID.
     */
    private int packageId = -1 ;
    
    /**
     * 再読み込みチェックフラグ.
     */
    private boolean reloadFlag = false ;
    
    /**
     * 同期オブジェクト.
     */
    private final Object sync = new Object() ;
    
    /**
     * コンストラクタ.
     */
    public ModelApiManager() throws Exception {
        try {
            if( GlobalManager.getValue( ServiceDef.MANAGER_BY_DBMS_POOL ) != null ) {
                synchronized( sync ) {
                    loadScript() ;
                }
            }
        } catch( Exception e ) {
            LOG.error( "## [modelApi] loadScriptError",e ) ;
            throw e ;
        }
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        this.clear() ;
    }
    
    /**
     * 基本ディレクトリを取得.
     */
    private static final String baseDir() {
        try {
            return FileUtil.getFullPath( MaachangDef.DIRECTORY_MODEL ) + FileUtil.FILE_SPACE ;
        } catch( Exception e ) {
        }
        return null ;
    }
    
    /**
     * 情報クリア.
     * <BR><BR>
     * 情報をクリアします.
     */
    protected void clear() {
        synchronized( sync ) {
            beforeTime = -1L ;
            modelCode = null ;
            srcScript = null ;
            packageId = -1 ;
            reloadFlag = false ;
        }
    }
    
    /**
     * 再読み込み処理.
     * @exception Exception 例外.
     */
    public void reload() throws Exception {
        synchronized( sync ) {
            loadScript() ;
        }
    }
    
    /**
     * スクリプトキャッシュを有効にする.
     * この処理は、スクリプト呼び出しの前に必ず実行する必要があります.
     * @exception Exception 例外.
     */
    public void useScript() throws Exception {
        boolean reloadCheck = false ;
        long time = System.currentTimeMillis() ;
        try {
            synchronized( sync ) {
                if( reloadFlag ) {
                    reloadCheck = true ;
                    reloadFlag = false ;
                    if( modelCode == null || modelCode.equals( createModelMonitor() ) == false ) {
                        loadScript() ;
                        return ;
                    }
                }
                if( GlobalManager.getValue( ServiceDef.MANAGER_BY_DBMS_POOL ) != null ) {
                    if( this.packageId != -1 && this.packageId != JsDef.getDefaultPackageId() ) {
                        loadScript() ;
                    }
                    else if( beforeTime + CHECK_TIME <= time ) {
                        if( reloadCheck == false &&
                            ( modelCode == null || modelCode.equals( createModelMonitor() ) == false ) ) {
                            loadScript() ;
                        }
                        else {
                            beforeTime = System.currentTimeMillis() ;
                        }
                    }
                }
            }
        } catch( Exception e ) {
            throw e ;
        }
    }
    
    /**
     * スクリプト内容を取得.
     * <BR><BR>
     * スクリプト内容を取得します.
     * <BR>
     * @return SrcScript スクリプト内容が返されます.
     */
    public SrcScript getSrcScript() {
        SrcScript ret = null ;
        synchronized( sync ) {
            ret = srcScript ;
        }
        return ret ;
    }
    
    /**
     * 指定パスがこのキャッシュ内の条件であるかチェック.
     * <BR><BR>
     * 指定パスがこのキャッシュ内の条件であるかチェックします.
     * <BR>
     * @param path チェック対象のパスを設定します.
     * @return boolean [true]の場合はこのキャッシュ内の条件です.
     */
    public boolean isCache( String path ) {
        if( path == null || ( path = path.trim() ).length() <= 0 ||
            path.equals( SCRIPT_NAME ) == false ) {
            return false ;
        }
        return true ;
    }
    
    /**
     * リロードフラグを設定.
     * @param reloadFlag [true]の場合、再読み込みチェックを行います.
     */
    public void setReloadFlag( boolean reloadFlag ) {
        synchronized( sync ) {
            this.reloadFlag = reloadFlag ;
        }
    }
    
    /**
     * ディレクトリ名を取得.
     * @return String ディレクトリ名が返されます.
     */
    public String getDirectory() {
        return MaachangDef.DIRECTORY_MODEL ;
    }
    
    /**
     * スクリプト内容を読み込む.
     */
    private void loadScript() throws Exception {
        int cnt = 0 ;
        String beforeManagerCode = this.modelCode ;
        int beforePackageId = this.packageId ;
        long beforeBeforeTime = this.beforeTime ;
        //ScriptContext ctx = null ;
        try {
            // Dao初期化.
            DaoFactory daoFactory = getDaoFactory() ;
            if( daoFactory != null ) {
                daoFactory.clear() ;
            }
            // モデル内容を読み込み.
            String[] models = ScriptDef.getUseScript( READ_DIR ) ;
            int startPos = 0 ;
            // 対象となるモデル内容が存在する場合.
            if( models != null && models.length > 0 ) {
                StringBuilder buf = new StringBuilder() ;
                // 更新された内容を一時保存.
                int[] outLine = new int[ 1 ] ;
                this.modelCode = ScriptDef.getScriptManagerCode( models,READ_DIR ) ;
                this.packageId = JsDef.pushDefaultPackage( outLine,buf ) ;
                this.beforeTime = System.currentTimeMillis() ;
                
                startPos = outLine[ 0 ] ;
                outLine = null ;
                
                // モデル内容を読み込む.
                int len = models.length ;
                if( this.srcScript == null ) {
                    this.srcScript = new SrcScript() ;
                }
                else {
                    this.srcScript.clear() ;
                }
                // モデル群情報を確保.
                buf.append( "\nvar Models = {};\n" ) ;
                startPos += 2 ;
                for( int i = 0 ; i < len ; i ++ ) {
                    if( models[i].endsWith( ScriptManager.SCRIPT_PLUS ) ) {
                        models[i] = models[i].substring( 0,models[i].length()-ScriptManager.SCRIPT_PLUS.length() ) ;
                    }
                    if( models[i].endsWith( MODEL_CUSTOM ) ||
                        models[i].endsWith( MODEL_NAME_FOODER ) == false ) {
                        continue ;
                    }
                    else {
                        cnt ++ ;
                    }
                    // 本来のモデル名を取得して、付加処理.
                    String modelName = models[i].substring( 0,models[i].length()-MODEL_NAME_FOODER.length() ) ;
                    if( LOG.isDebugEnabled() ) {
                        LOG.debug( ">read[model] - " + modelName ) ;
                    }
                    startPos = putModel( modelName,buf,this.srcScript,startPos ) ;
                }
                String script = buf.toString() ;
                buf = null ;
                this.srcScript.create( script ) ;
                
                // 処理実行.
                Bindings bindings = ExternalBindings.getInstance() ;
                // dummyデータ設定.
                bindings.put( ScriptDef.SCRIPT_BY_MODEL,new DummyBaseModel() ) ;
                
                // エンジン実行.
                ScriptEngine engine = ApiManager.getInstance().getScriptEngine() ;
                //ctx = new SimpleScriptContext() ;
                //ctx.setBindings( bindings,ScriptContext.ENGINE_SCOPE ) ;
                //engine.setContext( ctx ) ;
                //engine.put( ScriptEngine.FILENAME,SCRIPT_NAME ) ;
                ApiManager.setScriptName( SCRIPT_NAME ) ;
                CompiledScript cs = ( ( Compilable )engine ).compile( script ) ;
                //cs.eval( ctx ) ;
                cs.eval( bindings ) ;
                cs = null ;
                
                // APIマネージャに登録.
                Map<String,Object> modelManager = ApiManager.getLocal() ;
                bindings.clear() ;
                ApiManager.getInstance().setModelMap( modelManager ) ;
                if( LOG.isDebugEnabled() ) {
                    LOG.debug( "** model-cacheを読み込み" ) ;
                }
            }
            // 対象となるモデル内容が存在しない場合は、内容をクリアする.
            if( cnt <= 0 ) {
                this.modelCode = null ;
                this.packageId = -1 ;
                ApiManager.getInstance().setModelMap( null ) ;
                this.beforeTime = System.currentTimeMillis() ;
            }
            this.reloadFlag = false ;
        } catch( Exception e ) {
            // 例外の場合は、ロールバック処理.
            this.modelCode = beforeManagerCode ;
            this.packageId = beforePackageId ;
            this.beforeTime = beforeBeforeTime ;
            throw e ;
        } finally {
            ApiManager.removeLocal() ;
        }
    }
    
    /**
     * Model管理コードを生成.
     */
    private String createModelMonitor()
        throws Exception {
        if( LOG.isDebugEnabled() ) {
            LOG.debug( "## read ModelCode" ) ;
        }
        String[] models = ScriptDef.getUseScript( READ_DIR ) ;
        return ScriptDef.getScriptManagerCode( models,READ_DIR ) ;
    }
    
    /**
     * Model内容を追加.
     */
    private int putModel( String model,StringBuilder buf,SrcScript srcScript,int startPos )
        throws Exception {
        buf.append( "\n\n" ) ;
        buf.append( "/** modelName << " ).append( model ).append( MODEL_NAME_FOODER ).append( ScriptManager.SCRIPT_PLUS ).append( " >> **/\n" ) ;
        buf.append( "var " ).append( model ).append( " = baseModel(\"" ).append( model ).append( "\");\n" ) ;
        buf.append( "Models[\"" ).append( model ).append( "\"] = " ).append( model ).append( ";\n" ) ;
        startPos += 5 ;
        String modelScript = FileUtil.getFileByString( READ_DIR+"/"+model+MODEL_NAME_FOODER+ScriptManager.SCRIPT_PLUS,"UTF8" ) ;
        if( modelScript != null && ( modelScript = modelScript.trim() ).length() > 0 ) {
            int targetSize = SrcScript.getScriptList( modelScript ) ;
            SrcScriptList srcLst = new SrcScriptList( model,startPos,targetSize ) ;
            this.srcScript.setSrcScriptList( srcLst ) ;
            buf.append( modelScript ).append( "\n" ) ;
            startPos += ( targetSize + 1 ) ;
        }
        modelScript = null ;
        // カスタムモデルスクリプトが存在する場合、それも含めて取り込む.
        String customName = new StringBuilder().append( model ).append( MODEL_CUSTOM ).append( ScriptManager.SCRIPT_PLUS ).toString() ;
        String custom = READ_DIR+"/"+customName ;
        if( FileUtil.isFileExists( custom ) == true ) {
            modelScript = FileUtil.getFileByString( custom,"UTF8" ) ;
            if( modelScript != null && ( modelScript = modelScript.trim() ).length() > 0 ) {
                buf.append( "/** modelName(Custom) << " ).append( customName ).append( " >> **/\n" ) ;
                startPos ++ ;
                int targetSize = SrcScript.getScriptList( modelScript ) ;
                SrcScriptList srcLst = new SrcScriptList( customName,startPos,targetSize ) ;
                this.srcScript.setSrcScriptList( srcLst ) ;
                buf.append( modelScript ).append( "\n" ) ;
                startPos += ( targetSize + 1 ) ;
            }
        }
        return startPos ;
    }
    
    /**
     * DaoFactoryを取得.
     */
    private DaoFactory getDaoFactory() {
        return ( DaoFactory )GlobalManager.getInstance().get( ServiceDef.DAO_FACTORY ) ;
    }
}
