package org.maachang.session.engine ;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.maachang.dbm.MDbmIO;
import org.maachang.dbm.MDbmOp;
import org.maachang.util.ConvertParam;
import org.maachang.util.thread.LoopThread;

/**
 * セッションキャッシュ管理.
 *  
 * @version 2008/05/25
 * @author  masahito suzuki
 * @since  PersistenceSession 1.00
 */
class SessionCache {
    /**
     * 基本キー名.
     */
    protected static final String BASE_SESSION_KEY = "@session@base:" ;
    
    /**
     * データ格納要素名.
     */
    protected static final String VALUE_SESSION_KEY = "@session@value:" ;
    
    /**
     * キャッシュ削除時間 : デフォルト値.
     * 5分.
     */
    private static final long DEFAULT_DELETE_CACHE = 300000L ;
    
    /**
     * キャッシュ削除時間 : 最小値.
     * 1分.
     */
    private static final long MIN_DELETE_CACHE = 60000L ;
    
    /**
     * キャッシュ削除時間 : 最大値.
     * 30分.
     */
    private static final long MAX_DELETE_CACHE = 1800000L ;
    
    /**
     * セッション削除時間 : デフォルト値.
     * 1日.
     */
    private static final long DEFAULT_DELETE_SESSION = 86400000L ;
    
    /**
     * セッション削除時間 : 最小値.
     * 30分.
     */
    private static final long MIN_DELETE_SESSION = 1800000L ;
    
    /**
     * セッション削除時間 : 最大値.
     * 1週間.
     */
    private static final long MAX_DELETE_SESSION = 604800000L ;
    
    /**
     * セッション管理用MDBM.
     */
    private MDbmOp mdbm = null ;
    
    /**
     * キャッシュデータ格納オブジェクト.
     */
    private Map<String,CacheValue> cache = null ;
    
    /**
     * キャッシュタイムアウト.
     */
    private long cacheTimeout = -1L ;
    
    /**
     * セッションタイムアウト.
     */
    private long sessionTimeout = -1L ;
    
    /**
     * キャッシュ監視スレッド.
     */
    private CacheSessionMonThread cacheThread = null ;
    
    /**
     * セッション監視スレッド.
     */
    private MDbmSessionMonThread sessionThread = null ;
    
    /**
     * IDロック.
     */
    private final SessionIdLock idLock = new SessionIdLock() ;
    
    /**
     * コンストラクタ.
     */
    private SessionCache() {
        
    }
    
    /**
     * コンストラクタ.
     * @param mdbm セッション管理用MDBMを設定します.
     * @param cacheTimeout キャッシュタイムアウト値を設定します.
     * @param sessionTimeout セッションタイムアウト値を設定します.
     * @exception Exception 例外.
     */
    public SessionCache( MDbmOp mdbm,long cacheTimeout,long sessionTimeout )
        throws Exception {
        if( mdbm == null || mdbm.isUse() == false ) {
            throw new IllegalArgumentException( "無効なMDBMを設定しています" ) ;
        }
        if( cacheTimeout <= -1 ) {
            cacheTimeout = DEFAULT_DELETE_CACHE ;
        }
        else if( cacheTimeout <= MIN_DELETE_CACHE ) {
            cacheTimeout = MIN_DELETE_CACHE ;
        }
        else if( cacheTimeout >= MAX_DELETE_CACHE ) {
            cacheTimeout = MAX_DELETE_CACHE ;
        }
        
        if( sessionTimeout <= -1 ) {
            sessionTimeout = DEFAULT_DELETE_SESSION ;
        }
        else if( sessionTimeout <= MIN_DELETE_SESSION ) {
            sessionTimeout = MIN_DELETE_SESSION ;
        }
        else if( sessionTimeout >= MAX_DELETE_SESSION ) {
            sessionTimeout = MAX_DELETE_SESSION ;
        }
        this.mdbm = mdbm ;
        this.cacheTimeout = cacheTimeout ;
        this.sessionTimeout = sessionTimeout ;
        this.cache = Collections.synchronizedMap( new HashMap<String,CacheValue>() ) ;
        this.cacheThread = new CacheSessionMonThread( this.cache,this.idLock,this.cacheTimeout ) ;
        this.sessionThread = new MDbmSessionMonThread( this.mdbm,this.cache,
            this.idLock,this.sessionTimeout ) ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        destroy() ;
    }
    
    /**
     * オブジェクトを破棄.
     */
    public void destroy() {
        if( sessionThread != null ) {
            sessionThread.stopThread() ;
        }
        sessionThread = null ;
        if( cacheThread != null ) {
            cacheThread.stopThread() ;
        }
        cacheThread = null ;
        
        if( mdbm != null ) {
            try {
                mdbm.maachangDbm().close() ;
            } catch( Exception e ) {
            }
        }
        mdbm = null ;
        if( cache != null ) {
            cache.clear() ;
        }
        cache = null ;
        cacheTimeout = -1L ;
        sessionTimeout = -1L ;
    }
    
    /**
     * セッションに１つの要素を設定.
     * @param sessionId 対象のセッションIDを設定します.
     * @param key 対象のKeyを設定します.
     * @param value 対象の要素を設定します.
     * @exception Exception 例外.
     */
    public void put( String sessionId,String key,byte[] value )
        throws Exception {
        if( isUse() == false ) {
            throw new IOException( "オブジェクトは既に破棄されています" ) ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ||
            key == null || ( key = key.trim() ).length() <= 0 ||
            value == null || value.length <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        sessionId = SessionCache.toSessionId( sessionId ) ;
        // セッションIDでロック.
        idLock.lock( sessionId ) ;
        try {
            String skey = convertSessionKey( sessionId ) ;
            String vKey = convertSessionValueKey( sessionId,key ) ;
            // 新しいセッションIDで登録する.
            if( mdbm.containsKey( skey ) == false ) {
                SessionChild ch = new SessionChild() ;
                ch.getChild().add( key.getBytes( "UTF8" ) ) ;
                byte[] b = ch.toBinary() ;
                ch = null ;
                mdbm.put( skey,b ) ;
                mdbm.put( vKey,value ) ;
            }
            // 既にセッションIDが存在する.
            else {
                // 指定セッションIDのキャッシュは削除.
                cache.remove( sessionId ) ;
                byte[] b = mdbm.get( skey ) ;
                SessionChild ch = new SessionChild( b ) ;
                b = null ;
                
                // 指定要素が新しい要素の場合.
                if( ch.containsKey( key ) == false ) {
                    ch.update() ;
                    ch.getChild().add( key.getBytes( "UTF8" ) ) ;
                    b = ch.toBinary() ;
                    mdbm.put( skey,b ) ;
                    b = null ;
                }
                // 新しい要素は既に存在する.
                else {
                    // セッション時間の更新.
                    setSessionTime( mdbm,sessionId ) ;
                }
                
                // 要素をセットする.
                mdbm.put( vKey,value ) ;
            }
        } finally {
            idLock.unlock( sessionId ) ;
        }
    }
    
    /**
     * セッションを削除.
     * @param sessionId 対象のセッションIDを設定します.
     * @exception Exception 例外.
     */
    public void remove( String sessionId ) throws Exception {
        if( isUse() == false ) {
            throw new IOException( "オブジェクトは既に破棄されています" ) ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        sessionId = SessionCache.toSessionId( sessionId ) ;
        // セッションIDでロック.
        idLock.lock( sessionId ) ;
        try {
            if( mdbm.containsKey( convertSessionKey( sessionId ) ) ) {
                removeSession( mdbm,cache,sessionId ) ;
            }
        } finally {
            idLock.unlock( sessionId ) ;
        }
    }
    
    /**
     * セッションから１つの要素を削除.
     * @param sessionId 対象のセッションIDを設定します.
     * @param key 対象のKeyを設定します.
     * @exception Exception 例外.
     */
    public void remove( String sessionId,String key )
        throws Exception {
        if( isUse() == false ) {
            throw new IOException( "オブジェクトは既に破棄されています" ) ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ||
            key == null || ( key = key.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        sessionId = SessionCache.toSessionId( sessionId ) ;
        // セッションIDでロック.
        idLock.lock( sessionId ) ;
        try {
            String vKey = convertSessionValueKey( sessionId,key ) ;
            if( mdbm.containsKey( vKey ) ) {
                cache.remove( sessionId ) ;
                setSessionTime( mdbm,sessionId ) ;
                mdbm.remove( vKey ) ;
            }
        } finally {
            idLock.unlock( sessionId ) ;
        }
    }
    
    /**
     * セッションから１つの要素を取得.
     * @param sessionId 対象のセッションIDを設定します.
     * @param key 対象のKeyを設定します.
     * @return byte[] １つの要素が返されます.
     * @exception Exception 例外.
     */
    public byte[] get( String sessionId,String key )
        throws Exception {
        if( isUse() == false ) {
            throw new IOException( "オブジェクトは既に破棄されています" ) ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ||
            key == null || ( key = key.trim() ).length() <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        sessionId = SessionCache.toSessionId( sessionId ) ;
        byte[] ret = null ;
        // セッションIDでロック.
        idLock.lock( sessionId ) ;
        try {
            boolean cacheSessionFlag = cache.containsKey( sessionId ) ;
            // セッション名に対してキャッシュ情報が存在する場合.
            if( cacheSessionFlag == true ) {
                CacheValue cacheVal = cache.get( sessionId ) ;
                // キャッシュから、Keyの位置を取得.
                int no = cacheVal.search( key ) ;
                if( no >= 0 ) {
                    // キャッシュにKeyが存在する場合.
                    setSessionTime( mdbm,sessionId ) ;
                    ret = cacheVal.get( no ) ;
                }
            }
            // キャッシュから情報を取得できない場合.
            if( ret == null ) {
                // MDBMから情報を取得.
                String vKey = convertSessionValueKey( sessionId,key ) ;
                if( mdbm.containsKey( vKey ) ) {
                    // MDBMにKey情報が存在する場合は、時間更新して、情報を取得し
                    // その内容をCache化する.
                    setSessionTime( mdbm,sessionId ) ;
                    ret = mdbm.get( vKey ) ;
                    if( ret != null ) {
                        // キャッシュにSessionID情報が存在しない場合は
                        // 生成して、格納する.
                        CacheValue cacheVal = null ;
                        if( cacheSessionFlag == false ) {
                            cacheVal = new CacheValue() ;
                            cache.put( sessionId,cacheVal ) ;
                        }
                        else {
                            cacheVal = cache.get( sessionId ) ;
                        }
                        cacheVal.put( key,ret ) ;
                    }
                }
            }
        } finally {
            idLock.unlock( sessionId ) ;
        }
        return ret ;
    }
    
    /**
     * 指定セッション更新時間を取得.
     * @param sessionId 対象のセッションIDを設定します.
     * @return long 対象の更新時間が返されます.
     * @exception Exception 例外.
     */
    public long getLastUpdateTime( String sessionId ) throws Exception {
        if( isUse() == false ) {
            throw new IOException( "オブジェクトは既に破棄されています" ) ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        long ret = -1L ;
        sessionId = SessionCache.toSessionId( sessionId ) ;
        // セッションIDでロック.
        idLock.lock( sessionId ) ;
        try {
            ret = getSessionTime( mdbm,sessionId ) ;
        } finally {
            idLock.unlock( sessionId ) ;
        }
        return ret ;
    }
    
    /**
     * 指定セッション情報が存在するかチェック.
     * @param sessionId 対象のセッションIDを設定します.
     * @return boolean [true]の場合、セッションIDは存在します.
     */
    public boolean containsKey( String sessionId ) {
        if( isUse() == false ) {
            return false ;
        }
        if( sessionId == null ||
            ( sessionId = sessionId.trim() ).length() != PersistenceDefine.SESSION_KEY_LENGTH ) {
            return false ;
        }
        sessionId = SessionCache.toSessionId( sessionId ) ;
        boolean ret = false ;
        try {
            // セッションIDでロック.
            idLock.lock( sessionId ) ;
            try {
                ret = mdbm.containsKey( convertSessionKey( sessionId ) ) ;
            } finally {
                idLock.unlock( sessionId ) ;
            }
            return ret ;
        } catch( Exception e ) {
        }
        return false ;
    }
    
    /**
     * MDBMオブジェクトを取得.
     * @return MDbmOp MDBMオブジェクトが返されます.
     */
    public MDbmOp getMDbm() {
        return ( isUse() ) ? mdbm : null ;
    }
    
    /**
     * セッション削除時間を取得.
     * @return int セッション削除時間が返されます.
     */
    public long getDeleteSessionTime() {
        return sessionTimeout ;
    }
    
    /**
     * このオブジェクトが有効かチェック.
     * @return boolean [true]が返された場合、オブジェクトは有効です.
     */
    public boolean isUse() {
        return ( mdbm == null || mdbm.isUse() == false ||
            cache == null || 
            cacheThread == null || cacheThread.isStop() == true ||
            sessionThread == null || sessionThread.isStop() == true ) ?
            false : true ;
    }
    
    /**
     * 指定Keyに対するセッションタイマーを取得.
     */
    protected static final long getSessionTime( MDbmOp op,String key ) throws Exception {
        if( key == null || ( key = key.trim() ).length() <= 0 ) {
            return -1L ;
        }
        if( key.startsWith( BASE_SESSION_KEY ) == false ) {
            key = convertSessionKey( key ) ;
        }
        MDbmIO io = ( MDbmIO )op.maachangDbm() ;
        byte[] b = new byte[ 8 ] ;
        io.read( key.getBytes( "UTF8" ),b,0,0,8 ) ;
        return ConvertParam.convertLong( 0,b ) ;
    }
    
    /**
     * 指定Keyに対するセッションタイマーを更新.
     */
    protected static final void setSessionTime( MDbmOp op,String key ) throws Exception {
        if( key == null || ( key = key.trim() ).length() <= 0 ) {
            return ;
        }
        if( key.startsWith( BASE_SESSION_KEY ) == false ) {
            key = convertSessionKey( key ) ;
        }
        MDbmIO io = ( MDbmIO )op.maachangDbm() ;
        byte[] b = ConvertParam.convertLong( System.currentTimeMillis() ) ;
        io.write( key.getBytes( "UTF8" ),b,0,0,8 ) ;
    }
    
    /**
     * 指定Keyを削除.
     */
    protected static final void removeSession( MDbmOp op,Map<String,CacheValue> cache,String key )
        throws Exception {
        if( key == null || ( key = key.trim() ).length() <= 0 ) {
            return ;
        }
        if( key.startsWith( BASE_SESSION_KEY ) ) {
            key = toSessionId( key ) ;
        }
        if( cache.containsKey( key ) ) {
            cache.remove( key ) ;
        }
        String baseKey = convertSessionKey( key ) ;
        if( op.containsKey( baseKey ) ) {
            byte[] b = op.get( baseKey ) ;
            SessionChild ch = new SessionChild( b ) ;
            b = null ;
            ArrayList<byte[]> lst = ch.getChild() ;
            int len = lst.size() ;
            for( int i = 0 ; i < len ; i ++ ) {
                String valKey = convertSessionValueKey(
                    key,new String( lst.get( i ),"UTF8" ) ) ;
                if( op.containsKey( valKey ) ) {
                    op.remove( valKey ) ;
                }
            }
            op.remove( baseKey ) ;
        }
    }
    
    /**
     * セッションキーに変換.
     */
    protected static final String convertSessionKey( String name ) {
        return BASE_SESSION_KEY+name ;
    }
    
    /**
     * MDBM格納のセッションKeyから、セッションIDを取得.
     */
    protected static final String toSessionId( String name ) {
        if( name.startsWith( BASE_SESSION_KEY ) ) {
            return name.substring( BASE_SESSION_KEY.length() ) ;
        }
        return name ;
    }
    
    /**
     * セッション要素キーに変換.
     */
    protected static final String convertSessionValueKey( String sessionId,String key ) {
        return new StringBuilder().
            append( VALUE_SESSION_KEY ).
            append( sessionId ).append( "." ).
            append( key ).toString() ;
    }
}

/**
 * SessionChild.
 */
class SessionChild {
    private long lastMod = -1L ;
    private ArrayList<byte[]> child = null ;
    
    public SessionChild() {
        this.lastMod = System.currentTimeMillis() ;
        this.child = new ArrayList<byte[]>() ;
    }
    
    public SessionChild( byte[] bin ) throws Exception {
        int p = 0 ;
        long time = ConvertParam.convertLong( p,bin ) ;
        p += 8 ;
        int len = ConvertParam.convertInt( p,bin ) ;
        p += 4 ;
        ArrayList<byte[]> lst = new ArrayList<byte[]>() ;
        for( int i = 0 ; i < len ; i ++ ) {
            int strLen = ConvertParam.convertInt( p,bin ) ;
            p += 4 ;
            if( strLen <= 0 ) {
                continue ;
            }
            byte[] b = new byte[ strLen ] ;
            System.arraycopy( bin,p,b,0,strLen ) ;
            p += strLen ;
            lst.add( b ) ;
        }
        this.lastMod = time ;
        this.child = lst ;
    }
    
    protected void finalize() {
        destroy() ;
    }
    
    public void destroy() {
        lastMod = -1L ;
        child = null ;
    }
    
    public long lastMod() {
        return lastMod ;
    }
    
    public void update() {
        lastMod = System.currentTimeMillis() ;
    }
    
    public boolean containsKey( String key ) {
        try {
            return containsKey( key.getBytes( "UTF8" ) ) ;
        } catch( Exception e ) {
        }
        return false ;
    }
    
    public boolean containsKey( byte[] key ) {
        return ( search( key ) >= 0 ) ;
    }
    
    public int search( String key ) {
        try {
            return search( key.getBytes( "UTF8" ) ) ;
        } catch( Exception e ) {
        }
        return -1 ;
    }
    
    public int search( byte[] key ) {
        int klen = key.length ;
        int len = child.size() ;
        for( int i = 0 ; i < len ; i ++ ) {
            byte[] x = child.get( i ) ;
            if( x.length == klen ) {
                boolean flg = true ;
                for( int j = 0 ; j < klen ; j ++ ) {
                    if( x[ j ] != key[ j ] ) {
                        flg = false ;
                        break ;
                    }
                }
                if( flg == true ) {
                    return i ;
                }
            }
        }
        return -1 ;
    }
    
    public ArrayList<byte[]> getChild() {
        return child ;
    }
    
    public byte[] toBinary() throws Exception {
        int len = child.size() ;
        int max = 8 + 4 ;
        for( int i = 0 ; i < len ; i ++ ) {
            byte[] b = child.get( i ) ;
            max += 4 + b.length ;
        }
        byte[] ret = new byte[ max ] ;
        int p = 0 ; 
        ConvertParam.convertLong( ret,p,lastMod ) ;
        p += 8 ;
        ConvertParam.convertInt( ret,p,len ) ;
        p += 4 ;
        for( int i = 0 ; i < len ; i ++ ) {
            byte[] b = child.get( i ) ;
            int strLen = b.length ;
            ConvertParam.convertInt( ret,p,strLen ) ;
            p += 4 ;
            System.arraycopy( b,0,ret,p,strLen ) ;
            p += strLen ;
        }
        return ret ;
    }
    
}

/**
 * キャッシュ監視スレッド.
 */
class CacheSessionMonThread extends LoopThread {
    private static final Log LOG = LogFactory.getLog( CacheSessionMonThread.class ) ;
    private long timeout = -1L ;
    private Map<String,CacheValue> map = null ;
    private SessionIdLock idLock = null ;
    
    private CacheSessionMonThread() {
        
    }
    
    public CacheSessionMonThread( Map<String,CacheValue> map,SessionIdLock idLock,long timeout )
        throws Exception {
        this.map = map ;
        this.timeout = timeout ;
        this.idLock = idLock ;
        startThread() ;
    }
    
    protected void clear() {
        this.map = null ;
    }
    
    protected boolean execution() throws Exception {
        if( map.size() <= 0 ) {
            Thread.sleep( 1000 ) ;
        }
        else {
            for( Iterator<String> keys = map.keySet().iterator() ; keys.hasNext() ; ) {
                Thread.sleep( 250 ) ;
                String key = keys.next() ;
                
                // セッションIDでロック.
                idLock.lock( key ) ;
                try {
                    CacheValue val = map.get( key ) ;
                    if( val == null || val.size() <= 0 ||
                        val.getLastTime() + timeout <= System.currentTimeMillis() ) {
                        if( LOG.isDebugEnabled() ) {
                            LOG.debug( "## キャッシュセッション[" + key + "]を削除しました" ) ;
                        }
                        keys.remove() ;
                    }
                } finally {
                    idLock.unlock( key ) ;
                }
            }
            Thread.sleep( 250 ) ;
        }
        return false ;
    }
    
}

/**
 * Session監視スレッド.
 */
class MDbmSessionMonThread extends LoopThread {
    private static final Log LOG = LogFactory.getLog( MDbmSessionMonThread.class ) ;
    private long timeout = -1L ;
    private MDbmOp mdbm = null ;
    private Map<String,CacheValue> cache = null ;
    private SessionIdLock idLock = null ;
    
    private MDbmSessionMonThread() {
        
    }
    
    public MDbmSessionMonThread( MDbmOp mdbm,Map<String,CacheValue> cache,SessionIdLock idLock,long timeout )
        throws Exception {
        this.mdbm = mdbm ;
        this.cache = cache ;
        this.timeout = timeout ;
        this.idLock = idLock ;
        startThread() ;
    }
    
    protected void clear() {
        this.mdbm = null ;
        this.cache = null ;
    }
    
    protected void toException( Exception e ) {
        LOG.warn( "## sessionMonitor-error",e ) ;
    }
    
    protected boolean execution() throws Exception {
        if( mdbm.size() <= 0 ) {
            Thread.sleep( 1000 ) ;
        }
        else {
            for( Enumeration<byte[]> keys = mdbm.elements() ; keys.hasMoreElements() ; ) {
                Thread.sleep( 250 ) ;
                byte[] keyBin = keys.nextElement() ;
                if( keyBin == null ) {
                    continue ;
                }
                String key = new String( keyBin,"UTF8" ) ;
                keyBin = null ;
                if( key.startsWith( SessionCache.BASE_SESSION_KEY ) ) {
                    String sessionId = SessionCache.toSessionId( key ) ;
                    
                    // セッションIDでロック.
                    idLock.lock( sessionId ) ;
                    try {
                        if( mdbm.containsKey( key ) ) {
                            long time = SessionCache.getSessionTime( mdbm,key ) ;
                            if( time <= -1 || time + timeout <= System.currentTimeMillis() ) {
                                SessionCache.removeSession( mdbm,cache,sessionId ) ;
                                LOG.info( "## 永続化セッション["+ sessionId + "]を削除しました" ) ;
                            }
                        }
                    } finally {
                        idLock.unlock( sessionId ) ;
                    }
                    
                }
            }
            Thread.sleep( 250 ) ;
        }
        return false ;
    }
    
}

