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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.comet.conf.ServiceDef;
import org.maachang.comet.httpd.HttpdErrorDef;
import org.maachang.comet.httpd.HttpdRequest;
import org.maachang.comet.httpd.HttpdResponse;
import org.maachang.comet.httpd.engine.HttpdDef;
import org.maachang.comet.httpd.engine.HttpdResponseInstance;
import org.maachang.comet.httpd.engine.script.BaseModel;
import org.maachang.comet.httpd.engine.script.BaseModelImpl;
import org.maachang.comet.httpd.engine.script.EndScript;
import org.maachang.comet.httpd.engine.script.Script;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.comet.httpd.engine.script.WebAppScriptFactory;
import org.maachang.comet.httpd.engine.script.cache.CacheScriptManager;
import org.maachang.comet.httpd.engine.session.HttpdSession;
import org.maachang.comet.net.nio.ConnectionInfo;
import org.maachang.conf.ConvIniParam;
import org.maachang.manager.GlobalManager;

/**
 * 1つのコメット情報.
 *
 * @version 2007/08/24
 * @author  masahito suzuki
 * @since   MaachangComet 1.00
 */
public class Comet {
    
    /**
     * LOG.
     */
    private static final Log LOG = LogFactory.getLog( Comet.class ) ;
    
    /**
     * コメットタイムアウト情報.
     */
    private static final String COMET_TIMEOUT_PARAM = "CometTimeout" ;
    
    /**
     * グループID.
     */
    private String groupId = null ;
    
    /**
     * コメットスクリプトパス.
     */
    private String scriptPath = null ;
    
    /**
     * このコメットに接続している情報群.
     */
    private List<ConnectComet> connects = null ;
    
    /**
     * コメット生成時間.
     */
    private long createTime = -1L ;
    
    /**
     * コメット更新時間.
     */
    private long updateTime = -1L ;
    
    /**
     * 同期用.
     */
    private final Object sync = new Object() ;
    
    /**
     * コンストラクタ.
     */
    private Comet() {
        
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * 条件を設定してコメット接続状態を確立します.
     * <BR>
     * @param groupId 対象のグループIDを設定します.
     * @param scriptPath コメットスクリプトパスを設定します.
     * @exception Exception 例外.
     */
    public Comet( String groupId,String scriptPath )
        throws Exception {
        if( groupId == null || ( groupId = groupId.trim() ).length() <= 0 ||
            scriptPath == null || ( scriptPath = scriptPath.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        this.groupId = groupId ;
        this.scriptPath = scriptPath ;
        this.createTime = System.currentTimeMillis() ;
        this.updateTime = System.currentTimeMillis() ;
        this.connects = Collections.synchronizedList( new ArrayList<ConnectComet>() ) ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        this.clear() ;
    }
    
    /**
     * 情報クリア.
     * <BR><BR>
     * 情報をクリアします.
     */
    public void clear() {
        destroyConnection( connects ) ;
        this.groupId = null ;
        this.scriptPath = null ;
        this.createTime = -1L ;
        this.updateTime = -1L ;
        this.connects = null ;
    }
    
    /**
     * 新しいコネクションを追加.
     * <BR><BR>
     * 新しいコネクションを追加します.
     * <BR>
     * @param request リクエストを追加します.
     * @exception Exception 例外.
     */
    public void putConnection( HttpdRequest request )
        throws Exception {
        if( request == null || request.isUse() == false ) {
            return ;
        }
        if( this.connects == null ) {
            return ;
        }
        ConnectComet conn = new ConnectComet( request ) ;
        this.connects.add( conn ) ;
        synchronized( sync ) {
            this.updateTime = System.currentTimeMillis() ;
        }
    }
    
    /**
     * 無効なコネクション情報や、タイムアウト状態のコネクションを検知して削除.
     */
    protected void cleanConnection() {
        if( connects == null ) {
            return ;
        }
        int len = connects.size() ;
        for( int i = len-1 ; i >= 0 ; i -- ) {
            try {
                // コネクションが既にクローズされている.
                ConnectComet conn = connects.get( i ) ;
                if( conn == null || conn.isConnection() == false ) {
                    if( conn != null ) {
                        conn.destroy() ;
                    }
                    connects.remove( i ) ;
                }
                // コネクションタイムアウト.
                String timeout = conn.getRequest().getQuery().getParam( COMET_TIMEOUT_PARAM ) ;
                if( timeout != null ) {
                    long tm = ConvIniParam.getLong( timeout ) ;
                    timeout =  null ;
                    if( tm > 0L && conn.getConnectionTime() + tm <= System.currentTimeMillis() ) {
                        
                        conn.destroy() ;
                        connects.remove( i ) ;
                    }
                }
            } catch( Exception e ) {
            }
        }
    }
    
    /**
     * 接続先に情報を送信.
     * <BR><BR>
     * 接続先に情報を送信します.
     * <BR>
     * @param args コメット実行処理に渡したい、パラメータを設定します.
     * @param groupId 対象のグループIDを設定します.
     * @exception Exception 例外.
     */
    public void send( Object args,String groupId )
        throws Exception {
        try {
            List<ConnectComet> conns = getCometConnects() ;
            if( conns == null || conns.size() <= 0 ) {
                return ;
            }
            // Comet実行.
            sendAll( conns,this.scriptPath,args,groupId ) ;
            if( LOG.isDebugEnabled() ) {
                LOG.debug( "... exit - comet:" + groupId ) ;
            }
        } catch( Exception e ) {
            throw e ;
        }
    }
    
    /**
     * このコメットに接続しているセッションID群を取得.
     * <BR><BR>
     * このコメットに接続しているセッションID群を取得します.
     * <BR>
     * @return ArrayList<String> 接続されているセッションID群が返されます.
     */
    public ArrayList<String> getConnectSessionIds() {
        if( connects == null ) {
            return null ;
        }
        ArrayList<String> ret = new ArrayList<String>() ;
        int len = connects.size() ;
        for( int i = len-1 ; i >= 0 ; i -- ) {
            try {
                ConnectComet conn = connects.get( i ) ;
                if( conn != null && conn.isConnection() == true ) {
                    HttpdSession session = conn.getRequest().getSession() ;
                    if( session != null && session.getSessionId() != null ) {
                        ret.add( session.getSessionId() ) ;
                    }
                }
                else {
                    if( conn != null ) {
                        conn.destroy() ;
                    }
                    connects.remove( i ) ;
                }
            } catch( Exception e ) {
            }
        }
        if( ret.size() <= 0 ) {
            return null ;
        }
        return ret ;
    }
    
    /**
     * このコメットの接続数を取得.
     * <BR><BR>
     * このコメットの接続数を取得します.
     * <BR>
     * @return int 接続されている接続数が返されます.
     */
    public int getConnectSize() {
        if( connects != null ) {
            return connects.size() ;
        }
        return 0 ;
    }
    
    /**
     * グループIDを取得.
     * <BR><BR>
     * グループIDを取得します.
     * <BR>
     * @return String グループIDが返されます.
     */
    public String getGroupId() {
        return this.groupId ;
    }
    
    /**
     * スクリプトパスを取得.
     * <BR><BR>
     * スクリプトパスを取得します.
     * <BR>
     * @return String スクリプトパスが返されます.
     */
    public String getScriptPath() {
        return this.scriptPath ;
    }
    
    /**
     * コメット生成時間を取得.
     * <BR><BR>
     * コメット生成時間を取得します.
     * <BR>
     * @return long コメット生成時間が返されます.
     */
    public long getCreateTime() {
        return this.createTime ;
    }
    
    /**
     * コメット更新時間を取得.
     * <BR><BR>
     * コメット更新時間を取得します.
     * <BR>
     * @return long コメット更新時間が返されます.
     */
    public long getUpdateTime() {
        long ret = -1L ;
        synchronized( sync ) {
            ret = this.updateTime ;
        }
        return ret ;
    }
    
    /**
     * １つのリクエスト情報を処理.
     * <BR><BR>
     * １つのリクエスト情報を処理します.
     * <BR>
     * @param res 出力先を設定します.
     * @param request 対象のリクエスト情報を設定します.
     * @param scriptPath 対象のスクリプトパスを設定します.
     * @param groupId 対象のグループIDを設定します.
     * @return Object 戻り情報が返されます.
     * @exception Exception 例外.
     */
    public static final Object oneSend( HttpdResponse res,BaseModel baseModel,
        HttpdRequest request,String scriptPath,String groupId )
        throws Exception {
        return oneSend( res,baseModel,request,scriptPath,null,groupId ) ;
    }
    
    /**
     * １つのリクエスト情報を処理.
     * <BR><BR>
     * １つのリクエスト情報を処理します.
     * <BR>
     * @param res 出力先を設定します.
     * @param request 対象のリクエスト情報を設定します.
     * @param scriptPath 対象のスクリプトパスを設定します.
     * @param args コメット実行処理に渡したい、パラメータを設定します.
     * @param groupId 対象のグループIDを設定します.
     * @return Object 戻り情報が返されます.
     * @exception Exception 例外.
     */
    public static final Object oneSend( HttpdResponse res,BaseModel baseModel,
        HttpdRequest request,String scriptPath,Object args,String groupId )
        throws Exception {
        WebAppScriptFactory webapp = ( WebAppScriptFactory )GlobalManager.getValue(
            ServiceDef.MANAGER_BY_WEB_APP_FACTORY ) ;
        if( webapp == null ) {
            throw new IOException( "WebAppScriptFactoryの取得に失敗しました" ) ;
        }
        // コメットスクリプトを取得.
        Script comet = webapp.getApplication( scriptPath ) ;
        if( comet == null ) {
            throw new IOException( "["+scriptPath+"]の取得に失敗しました" ) ;
        }
        Object ret = null ;
        boolean libFlag = false ;
        SimpleScriptContext ctx = null ;
        boolean errorFlag = false ;
        try {
            // 実行コンテキスト生成.
            ctx = new SimpleScriptContext() ;
            
            // ライブラリスクリプトを読み込む.
            CacheScriptManager.getInstance().script( ctx ) ;
            libFlag = true ;
            ScriptDef.setDirectoryByBindings( ctx ) ;
            
            // 各パラメータを設定.
            Bindings bindings = ctx.getBindings( ScriptContext.ENGINE_SCOPE ) ;
            bindings.put( ScriptDef.SCRIPT_BY_QUERY,ScriptDef.getQuery( request.getQuery() ) ) ;
            bindings.put( ScriptDef.SCRIPT_MODE,ScriptDef.MODE_COMET ) ;
            bindings.put( ScriptDef.SCRIPT_BY_MODEL,baseModel ) ;
            bindings.put( ScriptDef.SCRIPT_BY_PATH,scriptPath ) ;
            bindings.put( ScriptDef.SCRIPT_BY_HEADER,request.getHeader() ) ;
            bindings.put( ScriptDef.SCRIPT_BY_COMET_ARGS,args ) ;
            bindings.put( ScriptDef.SCRIPT_BY_GROUP_ID,groupId ) ;
            HttpdSession session = request.getSession() ;
            if( session != null ) {
                bindings.put( ScriptDef.SCRIPT_BY_SESSION,session ) ;
            }
            if( res != null ) {
                // HTTPレスポンスを生成.
                res.setHttpCache( true ) ;
                res.setHttpClose( false ) ;//keep-alive.
                res.setCookieSession( request ) ;
                res.getHeader().setHeader( HttpdDef.VALUE_CONTENT_TYPE,HttpdDef.AJAX_MIME_TYPE ) ;
                res.setHttpCache( true ) ;
                
                // リクエスト/レスポンスを登録.
                bindings.put( ScriptDef.SCRIPT_BY_REQUEST,request ) ;
                bindings.put( ScriptDef.SCRIPT_BY_RESPONSE,res ) ;
            }
            
            // 実行.
            comet.getScript().execution( ctx,null ) ;
            if( res != null ) {
                res.flush() ;
            }
            
        } catch( ScriptException sc ) {
            if( ctx != null && EndScript.isEndScript( sc ) == true ) {
                ret = EndScript.getEndByResult( ctx ) ;
            }
            else {
                errorFlag = true ;
                throw sc ;
            }
        } catch( Exception e ) {
            errorFlag = true ;
            throw e ;
        } finally {
            if( baseModel.isCreate() == true ) {
                try {
                    if( errorFlag == true ) {
                        baseModel.rollback() ;
                    }
                    else {
                        baseModel.commit() ;
                    }
                } catch( Exception ee ) {
                }
            }
            if( libFlag == true ) {
                try {
                    CacheScriptManager.getInstance().executionByExitRequest() ;
                } catch( Exception ee ) {
                }
            }
        }
        return ret ;
    }
    
    /**
     * コネクションオブジェクト群を取得.
     */
    private List<ConnectComet> getCometConnects() {
        if( this.connects != null ) {
            List<ConnectComet> ret = this.connects ;
            this.connects = Collections.synchronizedList( new ArrayList<ConnectComet>() ) ;
            synchronized( sync ) {
                this.updateTime = System.currentTimeMillis() ;
            }
            return ret ;
        }
        return null ;
    }
    
    /**
     * １つの情報送信後の待機時間.
     */
    //private static final long WAIT = 50L ;
    
    /**
     * 全ての接続先に情報を送信.
     */
    private void sendAll( List<ConnectComet> conns,String scriptPath,Object args,String groupId )
        throws Exception {
        BaseModel baseModel = new BaseModelImpl() ;
        int len = conns.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            ConnectComet conn = conns.remove( 0 ) ;
            if( conn == null || conn.isConnection() == false ) {
                if( conn != null ) {
                    conn.destroy() ;
                }
                continue ;
            }
            //LOG.info( "send (" + i + "):" + conn.getRequest().toIpPort() + " [" + conn.getRequest().getHeader().getHeader( "User-Agent" ) + "]" ) ;
            try {
                // レスポンス情報を生成.
                HttpdResponse res = HttpdResponseInstance.createResponse(
                    conn.getRequest(),conn.getRequest().getUrlPath(),
                    HttpdErrorDef.HTTP11_200,conn.getRequest().getKeepAliveTimeout(),
                    conn.getRequest().getKeepAliveCount() ) ;
                // 送信処理.
                oneSend( res,baseModel,conn.getRequest(),scriptPath,args,groupId ) ;
                // 送信結果を反映.
                res.flush() ;
                res.destroy() ;
                ConnectionInfo info = conn.getConnectionInfo() ;
                try {
                    if( info != null && info.isUse() == true ) {
                        if( info.isCloseFlag() == true ) {
                            info.destroy() ;
                        }
                        else {
                            if( info.getCount() > 0 ) {
                                if( info.recyclingConnection() == true ) {
                                    conn.cancel() ;
                                }
                            }
                        }
                    }
                    else {
                        if( info != null ) {
                            info.destroy() ;
                        }
                    }
                } catch( Exception ee ) {
                    ee.printStackTrace() ;
                }
                conn.destroy() ;
                //Thread.sleep( WAIT ) ;
            } catch( Exception e ) {
                if( conn != null ) {
                    conn.destroy() ;
                }
                LOG.error( "comet-error",e ) ;
            }
        }
    }
    
    /**
     * 各接続先をクローズ.
     */
    private static final void destroyConnection( List<ConnectComet> conns ) {
        if( conns != null ) {
            int len = conns.size() ;
            for( int i = 0 ; i < len ; i ++ ) {
                ConnectComet con = conns.get( i ) ;
                if( con != null ) {
                    con.destroy() ;
                }
            }
            conns.clear() ;
        }
    }
}

