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

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleBindings;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.comet.httpd.engine.script.SrcScript;
import org.maachang.comet.httpd.engine.script.js.JsDef;
import org.maachang.util.Digest;
import org.maachang.util.FileUtil;
import org.maachang.util.StringUtil;

/**
 * 指定ディレクトリ以下のディレクトリ内の
 * スクリプト群をキャッシュ化.
 * 
 * @version 2007/08/29
 * @author masahito suzuki
 * @since MaachangComet 1.00
 */
public class CacheScriptByDirectorys {
    
    /**
     * LOG.
     */
    private static final Log LOG = LogFactory.getLog( CacheScriptByDirectorys.class ) ;
    
    /**
     * スクリプトエンジン名.
     */
    protected static final String ENGINE_NAME = "js" ;
    
    /**
     * 再読み込み確認時間.
     */
    private static final long CHECK_TIME = 30000L ;
    
    /**
     * キャッシュ名.
     */
    private String name = null ;
    
    /**
     * キャッシュ参照ディレクトリ名.
     */
    private String cacheDir = null ;
    
    /**
     * デフォルトパッケージ更新ID.
     */
    protected int packageId = -1 ;
    
    /**
     * 次回チェック用時間.
     */
    private long checkTime = -1L ;
    
    /**
     * 状態管理.
     */
    private String check = null ;
    
    /**
     * 同期オブジェクト.
     */
    private final Object sync = new Object() ;
    
    /**
     * コンストラクタ.
     */
    private CacheScriptByDirectorys() {
        
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * キャッシュ条件を設定します.
     * <BR>
     * @param name 対象のキャッシュ名を設定します.
     * @exception Exception 例外.
     */
    public CacheScriptByDirectorys( String name )
        throws Exception {
        if( name == null || ( name = name.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        if( name.indexOf( "/" ) != -1 || name.indexOf( "\\" ) != -1 ) {
            throw new IllegalArgumentException(
                "指定条件は、ディレクトリ階層設定を行えません" ) ;
        }
        this.name = name ;
        this.cacheDir = FileUtil.getFullPath( name ) ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        this.clear() ;
    }
    
    /**
     * 情報クリア.
     * <BR><BR>
     * 情報をクリアします.
     */
    protected void clear() {
        synchronized( sync ) {
            checkTime = -1 ;
            check = null ;
            packageId = -1 ;
        }
    }
    
    /**
     * 再読み込み処理.
     * <BR><BR>
     * 再読み込み処理を行います.
     * <BR>
     * @param wrapper 取得対象のスクリプトWrapperを設定します.
     * @exception Exception 例外.
     */
    public void reload( CacheTableWrapper wrapper )
        throws Exception {
        if( wrapper == null ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        try {
            synchronized( sync ) {
                loadScripts( wrapper ) ;
            }
        } catch( Exception e ) {
            this.clear() ;
            throw e ;
        }
    }
    
    /**
     * スクリプトキャッシュを取得.
     * <BR><BR>
     * スクリプトキャッシュを取得します.
     * <BR>
     * @param wrapper 取得対象のスクリプトWrapperを設定します.
     * @exception Exception 例外.
     */
    public void script( CacheTableWrapper wrapper ) throws Exception {
        if( wrapper == null ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        try {
            synchronized( sync ) {
                if( this.packageId != -1 &&
                    this.packageId != JsDef.getDefaultPackageId() ) {
                    loadScripts( wrapper ) ;
                }
                else if( checkTime + CHECK_TIME <= System.currentTimeMillis() ) {
                    if( wrapper.getCacheTable().isParent( this.name ) == false ) {
                        loadScripts( wrapper ) ;
                    }
                    else {
                        String chk = createCheckLib( getLibFileNames( cacheDir ) ) ;
                        if( chk == null || chk.equals( check ) == false ) {
                            loadScripts( wrapper ) ;
                        }
                    }
                }
            }
        } catch( Exception e ) {
            this.clear() ;
            throw e ;
        }
    }
    
    /**
     * 指定パスのソーススクリプトを取得.
     * <BR><BR>
     * 指定パスのソーススクリプトを取得します.
     * <BR>
     * @param path 対象のパス名を設定します.
     * @return SrcScript 対象のソーススクリプトが返されます.
     */
    public SrcScript getSrcScript( String path ) {
        if( isCache( path ) == false ) {
            return null ;
        }
        try {
            String script = FileUtil.getFileByString( path ) ;
            if( script != null ) {
                return new SrcScript( script ) ;
            }
        } catch( Exception e ) {
        }
        return null ;
    }
    
    /**
     * 対象名を取得.
     * <BR><BR>
     * 対象名を取得します.
     * <BR>
     * @return String 対象名が返されます.
     */
    public String getName() {
        return this.name ;
    }
    
    /**
     * 指定パスがこのキャッシュ内の条件であるかチェック.
     * <BR><BR>
     * 指定パスがこのキャッシュ内の条件であるかチェックします.
     * <BR>
     * @param path チェック対象のパスを設定します.
     * @return boolean [true]の場合はこのキャッシュ内の条件です.
     */
    public boolean isCache( String path ) {
        if( path == null || ( path = path.trim() ).length() <= 0 ||
            path.startsWith( this.name ) == false ) {
            return false ;
        }
        return true ;
    }
    
    /**
     * スクリプトをロードして実行.
     */
    private void loadScripts( CacheTableWrapper wrapper ) throws Exception {
        String[] libNames = getLibFileNames( cacheDir ) ;
        if( libNames != null && libNames.length > 0 ) {
            int len = libNames.length ;
            ScriptEngineManager manager = new ScriptEngineManager() ;
            ScriptEngine engine = manager.getEngineByName( ENGINE_NAME ) ;
            // 更新待機.
            CacheTable cacheTable = wrapper.getCacheTable() ;
            cacheTable.waitByStartUpdate() ;
            // 実行処理.
            try {
                wrapper.setCacheAppend( true ) ;
                Bindings bindings = new SimpleBindings( wrapper ) ;
                for( int i = 0 ; i < len ; i ++ ) {
                    String script = FileUtil.getFileByString( this.name+libNames[ i ],"UTF8" ) ;
                    if( script != null ) {
                        if( LOG.isDebugEnabled() ) {
                            LOG.debug( ">read["+this.name+"] - " + (this.name+libNames[ i ]) ) ;
                        }
                        StringBuilder buf = new StringBuilder() ;
                        this.packageId = JsDef.pushDefaultPackage( buf ) ;
                        script = buf.append( script ).toString() ;
                        buf = null ;
                        engine.put( ScriptEngine.FILENAME,this.name+libNames[ i ] ) ;
                        engine.eval( script,bindings ) ;
                    }
                }
                // 登録.
                bindings = cacheTable.getBindings() ;
                cacheTable.putParent( this.name,bindings ) ;
            } finally {
                wrapper.setCacheAppend( false ) ;
                cacheTable.exitUpdate() ;
            }
            this.check = createCheckLib( libNames ) ;
            this.checkTime = System.currentTimeMillis() ;
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "** "+this.name+"-cacheを読み込み" ) ;
            }
        }
        else {
            check = null ;
            packageId = -1 ;
            this.checkTime = System.currentTimeMillis() ;
        }
    }
    
    /**
     * ライブラリ更新チェック用情報を生成.
     */
    private final String createCheckLib( String[] libNames )
        throws Exception {
        if( libNames == null || libNames.length <= 0 ) {
            return null ;
        }
        StringBuilder buf = new StringBuilder() ;
        int len = libNames.length ;
        buf.append( len ) ;
        for( int i = 0 ; i < len ; i ++ ) {
            buf.append( "*" ) ;
            long time = -1L ;
            String fname = cacheDir+libNames[ i ] ;
            fname = StringUtil.changeString( fname,"\\","/" ) ;
            if( FileUtil.isFileExists( fname ) == true ) {
                time = FileUtil.getLastTime( fname ) ;
            }
            buf.append( libNames[ i ] ).append( "*" ).append( time ) ;
        }
        byte[] bin = buf.toString().getBytes( "UTF8" ) ;
        buf = null ;
        return Digest.convert( "SHA1",bin ) ;
    }
    
    /**
     * 指定ディレクトリ以下のディレクトリ名一覧を取得.
     */
    private final String[] getLibFileNames( String dir )
        throws Exception {
        ArrayList<String> lst = new ArrayList<String>() ;
        list( lst,dir,dir ) ;
        if( lst.size() > 0 ) {
            int len = lst.size() ;
            String[] ret = new String[ len ] ;
            for( int i = 0 ; i < len ; i ++ ) {
                ret[ i ] = lst.get( i ) ;
            }
            lst = null ;
            Arrays.sort( ret ) ;
            return ret ;
        }
        return null ;
    }
    
    /**
     * 指定ディレクトリ以下のディレクトリ名を取得.
     */
    private final void list( ArrayList<String> out,String base,String dir )
        throws Exception {
        File fp = new File( dir ) ;
        String[] names = fp.list() ;
        fp = null ;
        if( names != null && names.length > 0 ) {
            int len = names.length ;
            for( int i = 0 ; i < len ; i ++ ) {
                if( names[ i ] == null || ( names[ i ] = names[ i ].trim() ).length() <= 0 ) {
                    continue ;
                }
                String name = new StringBuilder().append( dir ).
                    append( FileUtil.FILE_SPACE ).append( names[ i ] ).toString() ;
                if( FileUtil.isDirExists( name ) ) {
                    list( out,base,name ) ;
                }
                else if( name.toLowerCase().endsWith( ScriptDef.SCRIPT_PLUS ) ) {
                    name = name.substring( base.length(),name.length() ) ;
                    if( name.indexOf( "\\" ) != -1 ) {
                        name = StringUtil.changeString( name,"\\","/" ) ;
                    }
                    if( name.startsWith( "/" ) == true ) {
                        name.substring( 1,name.length() ) ;
                    }
                    out.add( name ) ;
                }
            }
        }
    }
}
