package org.maachang.mimdb.server ;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Map;

import org.maachang.mimdb.Mimdb;
import org.maachang.mimdb.core.impl.MimdbUtils;
import org.maachang.mimdb.core.util.AtomicNumber32;
import org.maachang.mimdb.core.util.Config;

/**
 * Mimdbサーバー.
 * 
 * @version 2014/01/16
 * @author  masahito suzuki
 * @since MasterInMemDB 1.02
 */
public final class MimdbServer extends Thread {
    
    /** 送信バッファ長. **/
    protected static final int SEND_BYTE_ARRAY_LENGTH = 4096 ;
    
    /** 受信バッファ長. **/
    protected static final int RECV_BYTE_ARRAY_LENGTH = 1024 ;
    
    /** サーバーコンフィグファイル名. **/
    private static final String SERVER_CONF = "mimdb.conf" ;
    
    /** デフォルトbacklog. **/
    private static final int DEF_BACK_LOG = 50 ;
    
    /** デフォルトスレッド数. **/
    private static final int DEF_THREAD = 1 ;
    
    /** 最大スレッド数. **/
    private static final int MAX_THREAD = 100 ;
    
    /** Selectorタイムアウト. **/
    private static final int SELECTOR_TIMEOUT = 1000 ;
    
    /** スレッド停止フラグ. **/
    private final AtomicNumber32 stopFlag = new AtomicNumber32( 0 ) ;
    
    /** セレクタ. **/
    private NioSelector selector ;
    
    /** サーバソケット. **/
    private ServerSocketChannel channel ;
    
    /** mimdb. **/
    private Mimdb mimdb ;
    
    /** 環境設定フォルダー. **/
    protected String envFolder ;
    
    /** IOスレッド管理. **/
    private MimdbIOThread[] ioList ;
    
    /** バインドアドレス. **/
    private String bindAddr ;
    
    /** バインドポート番号. **/
    private int bindPort ;
    
    /** バックログ. **/
    private int backlog ;
    
    /** 圧縮転送モード. **/
    private boolean compress ;
    
    
    
    /**
     * コンストラクタ.
     */
    private MimdbServer() {
        
    }
    
    /**
     * コンストラクタ.
     * @param folder 対象の環境定義ファイル(mimdb.conf)が存在する
     *               フォルダ名を設定します.
     * @exception Exception 例外.
     */
    public MimdbServer( final String folder ) throws Exception {
        _init( folder ) ;
    }
    
    /** 情報出力. **/
    public static final void print( final String s ) {
        System.out.print( s ) ;
    }
    /** 情報出力. **/
    public static final void println( final String s ) {
        System.out.println( s ) ;
    }
    
    /** 初期処理. **/
    private final void _init( String folder ) throws Exception {
        if( folder == null || folder.length() == 0 ) {
            throw new IllegalArgumentException( "環境定義フォルダー名が設定されていません" ) ;
        }
        
        long allTime = System.currentTimeMillis() ;
        
        folder = MimdbUtils.getFullPath( folder ) ;
        if( !folder.endsWith( "/" ) ) {
            folder += "/" ;
        }
        String file = folder + SERVER_CONF ;
        Config conf = Config.read( file ) ;
        
        /** サーバー定義を取得. **/
        String bindAddr = conf.getString( "server","addr",0 ) ;
        int bindPort = conf.getInt( "server","port",0 ) ;
        int backlog = conf.getInt( "server","backlog",0 ) ;
        int threadLength = conf.getInt( "server","thread",0 ) ;
        boolean compress = conf.getBoolean( "server","compress",0 ) ;
        
        /** 各条件チェック. **/
        if( bindAddr == null ||
            ( bindAddr = bindAddr.trim() ).length() <= 0 ||
            "0.0.0.0".equals( bindAddr ) ) {
            bindAddr = null ;
        }
        if( bindPort <= 0 || bindPort > 65535 ) {
            bindPort = ConnectionDefine.DEF_PORT ;
        }
        if( backlog <= 0 ) {
            backlog = DEF_BACK_LOG ;
        }
        if( threadLength <= 0 ) {
            threadLength = DEF_THREAD ;
        }
        else if( threadLength > MAX_THREAD ) {
            threadLength = MAX_THREAD ;
        }
        
        println( "## start mimdb server " ) ;
        println( "   address :" + ( ( bindAddr == null ) ? "0.0.0.0" : bindAddr ) ) ;
        println( "   port    :" + bindPort ) ;
        println( "   backlog :" + backlog ) ;
        println( "   thread  :" + threadLength ) ;
        println( "   compress:" + compress ) ;
        println( "" ) ;
        
        /** mimdbの初期化. **/
        Mimdb mimdb = Mimdb.getInstance() ;
        mimdb.init( conf ) ;
        
        /** Alias定義されている全データベースを起動. **/
        long time ;
        String name ;
        Map<String,String> alias = mimdb.getAlias() ;
        Iterator<String> it = alias.keySet().iterator() ;
        while( it.hasNext() ) {
            name = it.next() ;
            time = System.currentTimeMillis() ;
            print( "## load database " + name + " " ) ;
            
            // データベース起動.
            mimdb.getDatabase( name ) ;
            
            time = System.currentTimeMillis() - time ;
            println( " ... ok   " + time + "msec" ) ;
        }
        println( "" ) ;
        
        /** サーバー起動. **/
        println( "## start server." ) ;
        
        // IPV4で処理.
        System.setProperty( "java.net.preferIPv4Stack","true" ) ;
        // DNSキャッシュは300秒.
        System.setProperty( "networkaddress.cache.ttl","300" ) ;
        // DNS解決失敗した場合のキャッシュ保持しない.
        System.setProperty( "networkaddress.cache.negative.ttl","0" ) ;
        
        // サーバーソケット作成.
        ServerSocketChannel ch = ServerSocketChannel.open() ;
        ch.configureBlocking( false ) ;
        ServerSocket soc = ch.socket() ;
        soc.setReuseAddress( true ) ;
        
        // サーバーソケットBind.
        if( bindAddr == null ) {
            soc.bind( new InetSocketAddress( bindPort ),backlog ) ;
        }
        else {
            soc.bind( new InetSocketAddress( bindAddr,bindPort ),backlog ) ;
        }
        soc = null ;
        
        // セレクターを生成.
        NioSelector sc = new NioSelector() ;
        
        // サーバーソケットの登録.
        sc.register( ch,SelectionKey.OP_ACCEPT ) ;
        
        // 受信スレッドの作成.
        MimdbIOThread[] io = new MimdbIOThread[ threadLength ] ;
        for( int i = 0 ; i < threadLength ; i ++ ) {
            io[ i ] = new MimdbIOThread( i,compress ) ;
        }
        
        /** 処理完了. **/
        allTime = System.currentTimeMillis() - allTime ;
        println( "" ) ;
        println( "## success " + allTime + "msec." ) ;
        
        // 条件セット.
        this.selector = sc ;
        this.channel = ch ;
        this.mimdb = mimdb ;
        this.envFolder = folder ;
        this.ioList = io ;
        this.bindAddr = ( bindAddr == null ) ? "0.0.0.0" : bindAddr  ;
        this.bindPort = bindPort ;
        this.backlog = backlog ;
        this.compress = compress ;
    }
    
    /**
     * スレッド開始.
     * @exception Exception 例外.
     */
    public void startThread() throws Exception {
        // スレッド起動.
        this.stopFlag.set( 1 ) ;
        this.setDaemon( true ) ;
        this.start() ;
        
        // 子スレッドもそれぞれ実行.
        int len = ioList.length ;
        for( int i = 0 ; i < len ; i ++ ) {
            ioList[ i ].startThread() ;
        }
    }
    
    /**
     * スレッド停止.
     */
    public void stopThread() {
        this.stopFlag.set( 0 ) ;
    }
    
    /**
     * スレッドが停止しているかチェック.
     * @return boolean [true]の場合、スレッド停止しています.
     */
    public boolean isStop() {
        return ( stopFlag.get() == 0 ) ;
    }
    
    /**
     * 環境フォルダーを取得.
     * @return String 環境フォルダが返却されます.
     */
    public String getEnvFolder() {
        return envFolder ;
    }
    
    /**
     * バインドアドレスを取得.
     * @return String バインドアドレスが返却されます.
     */
    public String getBindAddress() {
        return bindAddr ;
    }
    
    /**
     * バインドポート番号を取得.
     * @return int バインドポート番号が返却されます.
     */
    public int getBindPort() {
        return bindPort ;
    }
    
    /**
     * バックログを取得.
     * @return int バックログが返却されます.
     */
    public int getBackLog() {
        return backlog ;
    }
    
    /**
     * 通信圧縮が有効か取得.
     * @return boolean [true]の場合、圧縮は有効です.
     */
    public boolean isCompress() {
        return compress ;
    }
    
    /**
     * mimdbオブジェクトを取得.
     * @return Mimdb mimdbオブジェクトが返却されます.
     */
    public Mimdb getMimdb() {
        return mimdb ;
    }
    
    /**
     * ワークスレッド実行.
     */
    public void run() {
        ThreadDeath tdObject = null ;
        boolean endFlag = false ;
        
        int no,threadLen ;
        SelectionKey key = null ;
        SocketChannel ch ;
        Iterator<SelectionKey> it ;
        
        no = 0 ;
        threadLen = ioList.length ;
        
        // スレッド開始.
        println( "****** mimdbServer [開始] *****" ) ;
        NioSelector s = selector ;
        
        while( stopFlag.get() == 1 ) {
            try {
                
                // スレッド停止.
                if( endFlag ) {
                    break ;
                }
                
                // 情報がゼロ件の場合.
                if( s.select( SELECTOR_TIMEOUT ) == 0 ) {
                    continue ;
                }
                
                it = s.iterator() ;
                while( it!=null && it.hasNext() && stopFlag.get() == 1 ) {
                    
                    // スレッド停止.
                    if( endFlag ) {
                        break ;
                    }
                    
                    // Socket処理実行.
                    key = it.next() ;
                    it.remove() ;
                    
                    // 対象Keyが存在しない場合は処理しない.
                    if( key == null ) {
                        continue ;
                    }
                    
                    // acceptが検知された場合.
                    if( key.isAcceptable() ) {
                        // IOスレッドに登録.
                        if( ( ch = channel.accept() ) != null ) {
                            no = ( no >= threadLen ) ? 0 : no ;
                            ioList[ no ++ ].register( ch ) ;
                        }
                    }
                }
                
            } catch( ThreadDeath td ) {
                tdObject = td ;
                endFlag = true ;
            } catch( Throwable t ) {
                // InterruptedException.
                // ThreadDeath
                // これ以外の例外は無視.
            }
        }
        // 後処理.
        stopFlag.set( 0 ) ;
        println( "****** mimdbServer [停止] *****" ) ;
        
        // セレクタ情報のクリア.
        s.close() ;
        selector = null ;
        try { channel.close() ; } catch( Throwable e ) {}
        channel = null ;
        
        // 子スレッドもそれぞれ停止.
        try { 
            for( int i = 0 ; i < threadLen ; i ++ ) {
                try { ioList[ i ].stopThread() ; } catch( Throwable e ) {}
            }
        } catch( Throwable t ) {}
        
        // スレッドデスを返却.
        if( tdObject != null ) {
            throw tdObject ;
        }
        
    }
}

