package org.maachang.mimdb ;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
 * Mimdbメンテナンスクライアント処理.
 * 
 * @version 2013/11/08
 * @author masahito suzuki
 * @since MasterInMemDB 1.00
 */
public class MimdbMaintenanceClient {
    
    private MimdbMaintenanceClient() {}
    
    /** 受信バッファ. **/
    private static final int MAX_READ = 4096 ;
    
    /** 文字コード. **/
    private static final String CHARSET = "UTF8" ;
    
    /** 基本ポート番号. **/
    private static final int BASE_PORT = 3444 ;
    
    /** 受信バッファ. **/
    private byte[] buf = new byte[ MAX_READ ] ;
    
    /** クローズ状態. **/
    private boolean closeFlag = true ;
    
    /** 接続Socket. **/
    private Socket socket ;
    
    /** 接続条件. **/
    private String addr ;
    private String bindAddr ;
    private int port ;
    private int bindPort ;
    private int timeout ;
    
    /**
     * コンストラクタ.
     * @param addr 接続先のIPアドレスを設定します.
     *             [null]を設定した場合は、ローカルIPで接続します.
     * @param port 接続先のポート番号を設定します.
     *             [-1]の場合は、規定のポート番号で設定します.
     * @exception Exception 例外.
     */
    public MimdbMaintenanceClient( String addr,int port ) throws Exception {
        this( addr,port,null,-1,-1 ) ;
    }
    
    /**
     * コンストラクタ.
     * @param addr 接続先のIPアドレスを設定します.
     *             [null]を設定した場合は、ローカルIPで接続します.
     * @param port 接続先のポート番号を設定します.
     *             [-1]の場合は、規定のポート番号で設定します.
     * @param timeout 受信タイムアウト値を設定します.
     * @exception Exception 例外.
     */
    public MimdbMaintenanceClient( String addr,int port,int timeout ) throws Exception {
        this( addr,port,null,-1,timeout ) ;
    }
    
    /**
     * コンストラクタ.
     * @param addr 接続先のIPアドレスを設定します.
     *             [null]を設定した場合は、ローカルIPで接続します.
     * @param port 接続先のポート番号を設定します.
     *             [-1]の場合は、規定のポート番号で設定します.
     * @param bindAddr バインド先のIPアドレスを設定します.
     *             [null]を設定した場合は、バインドIPアドレスは設定されません
     * @param bindPort バインド先のポート番号を設定します.
     *             [-1]の場合は、バインドポートは設定されません.
     * @param timeout 受信タイムアウト値を設定します.
     * @exception Exception 例外.
     */
    public MimdbMaintenanceClient( String addr,int port,String bindAddr,int bindPort,int timeout ) throws Exception {
        if( addr == null || ( addr = addr.trim() ).length() <= 0 || "null".equals( addr ) ) {
            addr = "127.0.0.1" ;
        }
        if( port > 65535 || port < 0 ) {
            port = BASE_PORT ;
        }
        if( timeout <= -1 ) {
            timeout = 30000 ; // デフォルトタイムアウトは30秒.
        }
        if( bindPort > 65535 || bindPort < 0 ) {
            bindPort = -1 ;
            bindAddr = null ;
        }
        // 接続条件をセット.
        this.addr = addr ;
        this.port = port ;
        this.bindAddr = bindAddr ;
        this.bindPort = bindPort ;
        this.timeout = timeout ;
        
        // 接続処理.
        reConnection() ;
    }
    
    /** 通信再接続. **/
    private final void reConnection() throws Exception {
        close() ;
        Socket s ;
        System.setProperty( "networkaddress.cache.ttl","300" ) ;
        System.setProperty( "networkaddress.cache.negative.ttl","0" ) ;
        // バインド用.
        if( bindPort != -1 ) {
            s = new Socket() ;
            if( bindAddr != null ) {
                s.bind( new InetSocketAddress( InetAddress.getByName( bindAddr ),bindPort ) ) ;
            }
            else {
                s.bind( new InetSocketAddress( bindPort ) ) ;
            }
            s.connect( new InetSocketAddress( InetAddress.getByName( addr ),port ) ) ;
        }
        // 非バインド用.
        else {
            s = new Socket() ;
            s.connect( new InetSocketAddress( InetAddress.getByName( addr ),port ) ) ;
        }
        s.setReuseAddress( true ) ;
        s.setSoLinger( true,5 ) ;
        s.setSendBufferSize( 32768 ) ;
        s.setReceiveBufferSize( 32768 ) ;
        s.setKeepAlive( false ) ;
        s.setTcpNoDelay( true ) ;
        s.setOOBInline( true ) ;
        s.setSoTimeout( timeout ) ;
        socket = s ;
        closeFlag = false ;
    }
    
    /**
     * オブジェクトの破棄.
     */
    public void close() {
        closeFlag = true ;
        try {
            socket.getInputStream().close() ;
        } catch( Exception e ) {}
        try {
            socket.getOutputStream().close() ;
        } catch( Exception e ) {}
        try {
            socket.close() ;
        } catch( Exception e ) {}
    }
    
    /**
     * 接続状態を取得.
     * @return boolean [true]の場合、接続されていません
     */
    public boolean isClose() {
        return closeFlag ;
    }
    
    /**
     * 接続先IPアドレスを取得.
     * @return String 接続先のIPアドレスが返却されます.
     */
    public String getAddress() {
        return addr ;
    }
    
    /**
     * 接続先ポート番号を取得.
     * @return int 接続先ポート番号をが返却されます.
     */
    public int getPort() {
        return port ;
    }
    
    /**
     * バインドIPアドレスを取得.
     * @return String 接続先のIPアドレスが返却されます.
     */
    public String getBindAddress() {
        return bindAddr ;
    }
    
    /**
     * バインドポート番号を取得.
     * @return int 接続先ポート番号をが返却されます.
     */
    public int getBindPort() {
        return bindPort ;
    }
    
    /**
     * コネクションタイムアウト値を取得.
     * @return int コネクションタイムアウト値が返却されます.
     */
    public int getTimeout() {
        return timeout ;
    }
    
    /**
     * コマンド実行.
     * @param cmd 対象のコマンドを設定します.
     * @return String コマンド結果が返却されます.
     * @exception Exception 例外.
     */
    public String execution( String cmd ) throws Exception {
        if( cmd == null || ( cmd = cmd.trim() ).length() <= 0 ) {
            return "" ;
        }
        StringBuilder sBuf = null ;
        try {
            // コマンド送信.
            byte[] b = cmd.getBytes( CHARSET ) ;
            if( b.length+2 > MAX_READ ) {
                throw new MimdbException( "コマンド長が長すぎます["+b.length+"]" ) ;
            }
            int len = b.length ;
            buf[ 0 ] = (byte)(len & 0x000000ff) ;
            buf[ 1 ] = (byte)( (len & 0x0000ff00) >> 8 ) ;
            System.arraycopy( b,0,buf,2,len ) ;
            
            socket.getOutputStream().write( buf,0,len+2 ) ;
            socket.getOutputStream().flush() ;
            
            InputStream in = socket.getInputStream() ;
            
            // 受信処理.
            sBuf = new StringBuilder() ;
            int bOff = 0 ;
            int bLen = 0 ;
            while( true ) {
                // socket受信.
                if( ( len = in.read( buf,bOff,MAX_READ-bOff ) ) <= 0 ) {
                    // データ終端の場合.
                    if( len <= -1 ) {
                        // ソケットのクローズ.
                        close() ;
                        break ;
                    }
                    // 受信ゼロに対して、オフセット値がヘッダバイト(2)以下の場合は再読み込み.
                    if( bOff < 2 ) {
                        continue ;
                    }
                }
                // ヘッダ取得処理の条件.
                if( bOff == 0 ) {
                    bLen = ( buf[ 0 ] & 0x000000ff ) | ( ( buf[ 1 ] & 0x000000ff ) << 8 ) ;
                    if( bLen <= -1 || bLen + 2 >= MAX_READ ) {
                        throw new MimdbException( "不正な受信サイズを受信しました[" + bLen + "]" ) ;
                    }
                    // 受信終了.
                    if( bLen == 0 ) {
                        break ;
                    }
                }
                bOff += len ;
                // 一次的な受信データがなくなるまで処理.
                while( true ) {
                    // １つの受信条件が終了した場合.
                    if( bOff >= bLen + 2 ) {
                        // バイナリを文字列化して、残りのバイナリをオフセット分移動.
                        sBuf.append( new String( buf,2,bLen,CHARSET ) ) ;
                        // １データの受信が丁度の場合.
                        if( bOff == bLen + 2 ) {
                            bOff = 0 ;
                        }
                        // 次の情報をTOPに移動させる.
                        else {
                            topBinary( buf,bLen+2,bOff-(bLen+2) ) ;
                            bOff = bOff-(bLen+2) ;
                            // 次の情報長を取得.
                            bLen = ( buf[ 0 ] & 0x000000ff ) | ( ( buf[ 1 ] & 0x000000ff ) << 8 ) ;
                            if( bLen <= -1 || bLen + 2 >= MAX_READ ) {
                                throw new MimdbException( "不正な受信サイズを受信しました[" + bLen + "]" ) ;
                            }
                            // 受信終了.
                            if( bLen == 0 ) {
                                break ;
                            }
                        }
                    }
                    else {
                        break ;
                    }
                }
                // 受信データが存在しない場合.
                if( bLen == 0 ) {
                    break ;
                }
            }
        } catch( Exception e ) {
            close() ;
            throw e ;
        }
        return sBuf.toString() ;
    }
    
    /** バイナリの残オフセット分を先頭にずらす. **/
    private static final void topBinary( byte[] b,int off,int len ) {
        for( int i = 0 ; i < len ; i ++ ) {
            b[ i ] = b[ i+off ] ;
        }
    }
    
    /** コマンド実行エラーメッセージ出力. **/
    private static final void errorCommand( String msg ) {
        System.out.println( "-ip [ipAddress or domain] -port [port] -bip [bindIpAddress] -bport [bindPort]" ) ;
        System.out.println( " -time [timeout] [command]" ) ;
        if( msg != null ) {
            System.out.println( msg ) ;
        }
        System.exit( -1 ) ;
    }
    
    /**
     * メイン実行.
     * @param args 対象のパラメータを設定します.
     * @exception Exception 例外.
     */
    public static final void main( String[] args ) throws Exception {
        String addr = null ;
        int port = -1 ;
        String bindAddr = null ;
        int bindPort = -1 ;
        int timeout = -1 ;
        String cmd = null ;
        
        if( args != null && args.length > 0 ) {
            int len = args.length ;
            
            // 接続条件を解析.
            int type = -1 ;
            String n ;
            for( int i = 0 ; i < len ; i ++ ) {
                n = args[ i ].toLowerCase() ;
                if( "-help".equals( n ) ) {
                    errorCommand( null ) ;
                }
                else if( "-?".equals( n ) ) {
                    errorCommand( null ) ;
                }
                else if( "-ip".equals( n ) ) {
                    type = 1 ;
                }
                else if( "-addr".equals( n ) ) {
                    type = 1 ;
                }
                else if( "-port".equals( n ) ) {
                    type = 2 ;
                }
                else if( "-bip".equals( n ) ) {
                    type = 3 ;
                }
                else if( "-baddr".equals( n ) ) {
                    type = 3 ;
                }
                else if( "-bport".equals( n ) ) {
                    type = 4 ;
                }
                else if( "-time".equals( n ) ) {
                    type = 5 ;
                }
                else if( "-timeout".equals( n ) ) {
                    type = 5 ;
                }
                else {
                    // コマンド内容.
                    switch( type ) {
                        case 1 :
                            if( addr != null ) errorCommand( "接続先IPアドレスが多重定義" ) ;
                            addr = args[ i ] ; break ;
                        case 2 :
                            if( port != -1 ) errorCommand( "接続先Portが多重定義" ) ;
                            port = Integer.parseInt( args[ i ] ) ; break ;
                        case 3 :
                            if( bindAddr != null ) errorCommand( "バインドIPアドレスが多重定義" ) ;
                            bindAddr = args[ i ] ; break ;
                        case 4 :
                            if( bindPort != -1 ) errorCommand( "バインドポートが多重定義" ) ;
                            bindPort = Integer.parseInt( args[ i ] ) ; break ;
                        case 5 :
                            if( timeout != -1 ) errorCommand( "受信タイムアウトが多重定義" ) ;
                            timeout = Integer.parseInt( args[ i ] ) ; break ;
                        default :
                            if( i + 1 == len ) {
                                // command.
                                cmd = args[ i ] ;
                                if( cmd.startsWith( "\"" ) && cmd.endsWith( "\"" ) ) {
                                    cmd = cmd.substring( 1,cmd.length()-1 ).trim() ;
                                }
                                break ;
                            }
                            errorCommand( "コマンドの構成が不正" ) ;
                    }
                    type = -1 ;
                }
            }
        }
        // クライアント接続.
        MimdbMaintenanceClient cl = new MimdbMaintenanceClient( addr,port,bindAddr,bindPort,timeout ) ;
        if( cmd != null ) {
            String res = cl.execution( cmd ) ;
            System.out.println( res ) ;
            System.exit( 0 ) ;
        }
        String res ;
        BufferedReader bw = new BufferedReader( new InputStreamReader( System.in ) ) ;
        while( true ) {
            System.out.print( ">" ) ;
            cmd = bw.readLine() ;
            if( cmd == null || cmd.length() <= 0 ) {
                continue ;
            }
            else if( "exit".equals( cmd ) ) {
                System.exit( 0 ) ;
            }
            // 接続処理.
            try {
                res = cl.execution( cmd ) ;
                System.out.println( res ) ;
            } catch( Exception e ) {
                // エラーの場合、１度だけ再接続処理.
                cl.reConnection() ;
                res = cl.execution( cmd ) ;
                System.out.println( res ) ;
                // ここでエラーの場合は、終了.
            }
        }
    }
    
}
