package org.maachang.connector ;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

import org.maachang.util.ConvertParam;

/**
 * １つのClientConnector.
 * 
 * @version 2008/05/25
 * @author masahito suzuki
 * @since MaachangBase 1.00
 */
class SingleClientConnector implements ClientConnector {
    
    /** I/Oバッファ長. */
    private static final int BUFFER = 131072 ;
    
    /** SoLinger値.*/
    private static final int LINGER_TIME = 15 ;
    
    /** 切断タイミング.*/
    private static final long SESSION_TIMEOUT = ConnectorSession.SESSION_TIMEOUT ;
    
    /** ローカルホストアドレス. */
    private static final String LOCAL_HOST = ConnectorSession.LOCAL_HOST ;
    
    /** 受信可能最大データ長. */
    private static final int MAX_BYTE = 32 * 0x00100000 ;
    
    /** 接続アドレス */
    private InetAddress addr = null ;
    
    /** 接続ポート */
    private int port = -1 ;
    
    /** 受信タイムアウト値. */
    private int timeout = -1 ;
    
    /** 通信認証用ヘッダ. */
    private byte[] headerBinary = null ;
    
    /** 通信用. */
    private Socket socket = null ;
    private InputStream inputStream = null ;
    private OutputStream outputStream = null ;
    
    /** 前回通信時間. */
    private long updateTime = -1L ;
    
    /**
     * コンストラクタ.
     */
    private SingleClientConnector() {
        
    }
    
    /**
     * コンストラクタ.
     * @param headerBinary 通信ヘッダバイナリを設定します.
     * @param addr 接続先のアドレスを設定します.
     * @param port 接続先のポート番号を設定します.
     * @param timeout 受信タイムアウト値を設定します.
     * @exception Exception 例外.
     */
    public SingleClientConnector( byte[] headerBinary,InetAddress addr,int port,int timeout )
        throws Exception {
        if( headerBinary == null || headerBinary.length <= 0 ) {
            throw new IllegalArgumentException( "ヘッダバイナリは不正です" ) ;
        }
        if( addr == null ) {
            throw new IllegalArgumentException( "接続先アドレスは不正です" ) ;
        }
        if( port <= -1 || port >= 65536 ) {
            throw new IllegalArgumentException( "接続先ポート番号は不正です" ) ;
        }
        if( timeout <= 0 ) {
            timeout = DEF_RECEIVE_TIMEOUT ;
        }
        this.headerBinary = headerBinary ;
        this.addr = addr ;
        this.port = port ;
        this.timeout = timeout ;
        //reconnect( false ) ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        close() ;
        
    }
    
    /**
     * コネクションクローズ.
     */
    public void close() {
        destroySocket() ;
        addr = null ;
        port = -1 ;
        timeout = -1 ;
        headerBinary = null ;
        socket = null ;
        inputStream = null ;
        outputStream = null ;
    }
    
    /**
     * 接続元のInetAddressを取得.
     * @return InetAddress 接続元のInetAddressが返されます.
     * @exception Exception 例外.
     */
    public InetAddress getInetAddress()
        throws Exception {
        return addr ;
    }
    
    /**
     * 接続元のポート番号を取得.
     * @return int 接続元のポート番号が返されます.
     * @exception Exception 例外.
     */
    public int getPort()
        throws Exception {
        return port ;
    }
    
    /**
     * サーバに送受信処理を行う.
     * @param binary 送信対象のバイナリを設定します.
     * @return byte[] 受信バイナリが返されます.
     * @exception Exception 例外.
     */
    public byte[] sendReceive( byte[] binary ) throws Exception {
        if( binary == null || binary.length <= 0 ) {
            throw new IllegalArgumentException( "引数は不正です" ) ;
        }
        ExpireConnectException e = null ;
        byte[] ret = null ;
        // 再接続回数は２回.
        for( int i = 0 ; i < 2 ; i ++ ) {
            try {
                // 再接続.
                //reconnect( true ) ;
                reconnect( false ) ;
                setUpdateTime( Long.MAX_VALUE ) ;
                // 送信処理.
                this.outputStream.write( headerBinary ) ;
                this.outputStream.write( ConvertParam.convertInt( binary.length + headerBinary.length + 4 ) ) ;
                this.outputStream.write( binary ) ;
                this.outputStream.flush() ;
                // 受信処理.
                ret = receive() ;
                updateTime() ;
                e = null ;
                break ;
            } catch( ExpireConnectException ec ) {
                destroySocket() ;
                e = ec ;
            } catch( Exception ex ) {
                destroySocket() ;
                throw e ;
            }
        }
        if( e != null ) {
            destroySocket() ;
            throw e ;
        }
        return ret ;
    }
    
    /**
     * オブジェクトがクローズされているかチェック.
     * @return boolean [true]の場合、オブジェクトはクローズされています.
     */
    public boolean isClosed() {
        return ( headerBinary == null ) ;
    }
    
    /**
     * 再コネクション.
     */
    private void reconnect( boolean mode ) throws Exception {
        if( mode == true ) {
            if( isDisconnection() == true ) {
                connectSocket() ;
            }
            else if( LOCAL_HOST.equals( addr.getHostAddress() ) ) {
                connectSocket() ;
            }
        }
        else {
            if( isDisconnection() == true ) {
                connectSocket() ;
            }
        }
    }
    
    /**
     * コネクションが切断されているかチェック.
     * @return boolean [true]の場合、オブジェクトはクローズされています.
     */
    protected boolean isDisconnection() {
        boolean ret = false ;
        try {
            long time = getUpdateTime() ;
            if( time <= -1L ) {
                destroySocket() ;
                ret = true ;
            }
            else if( this.socket == null || this.socket.isBound() == false ||
                this.socket.isClosed() == true ) {
                destroySocket() ;
                ret = true ;
            }
            else if( time != Long.MAX_VALUE &&
                System.currentTimeMillis() >= SESSION_TIMEOUT + time ) {
                destroySocket() ;
                ret = true ;
            }
        } catch( Exception e ) {
            destroySocket() ;
            ret = true ;
        }
        return ret ;
    }
    
    /**
     * 受信処理.
     */
    private byte[] receive() throws Exception {
        if( socket == null || socket.isClosed() == true ) {
            destroySocket() ;
            throw new IOException( "接続は既に破棄されています" ) ;
        }
        int headerLen = headerBinary.length ;
        int len = -1 ;
        ByteArrayOutputStream bs = new ByteArrayOutputStream() ;
        InputStream in = this.inputStream ;
        try {
            if( socket.isClosed() == true ) {
                bs.close() ;
                bs = null ;
                return null ;
            }
            byte[] ret = null ;
            int cnt = 0 ;
            for( ;; ) {
                int n = in.read() ;
                if( n <= -1 ) {
                    if( len <= -1 ) {
                        throw new ExpireConnectException( "通信は切断されています" ) ;
                    }
                    break ;
                }
                bs.write( n ) ;
                cnt ++ ;
                if( len <= -1 ) {
                    if( cnt >= headerLen + 4 ) {
                        byte[] bin = bs.toByteArray() ;
                        for( int i = 0 ; i < headerLen ; i ++ ) {
                            if( headerBinary[ i ] != bin[ i ] ) {
                                throw new ConnectorNotHeaderException( "通信ヘッダは不正です" ) ;
                            }
                        }
                        len = ConvertParam.convertInt( headerLen,bin ) ;
                        // 不正な受信サイズ.
                        if( len <= 0 || len >= MAX_BYTE ) {
                            destroySocket() ;
                            bs.close() ;
                            bs = null ;
                            throw new IOException( "最大サイズを越した受信データを検知しました" ) ;
                        }
                    }
                }
                else if( cnt >= len ) {
                    ret = bs.toByteArray() ;
                    break ;
                }
            }
            bs.close() ;
            bs = null ;
            return ret ;
        } catch( ConnectorNotHeaderException ch ) {
            destroySocket() ;
            throw ch ;
        } catch( ExpireConnectException ec ) {
            destroySocket() ;
            throw ec ;
        } catch( Exception e ) {
            destroySocket() ;
            throw e ;
        } finally {
            if( bs != null ) {
                try {
                    bs.close() ;
                } catch( Exception e ) {
                }
            }
            bs = null ;
        }
    }
    
    /**
     * コネクション処理.
     * @param timeout 対象のタイムアウト値を設定します.
     * @exception Exception 例外.
     */
    private void connectSocket() throws Exception {
        if( isClosed() == true ) {
            throw new IOException( "オブジェクトは既にクローズされています" ) ;
        }
        destroySocket() ;
        Socket socket = null ;
        socket = new Socket( addr,port ) ;
        socket.setSendBufferSize( BUFFER ) ;
        socket.setReceiveBufferSize( BUFFER ) ;
        socket.setKeepAlive( true ) ;
        socket.setTcpNoDelay( true ) ;
        socket.setReuseAddress( true ) ;
        socket.setSoLinger( true,LINGER_TIME ) ;
        socket.setSoTimeout( timeout ) ;
        this.socket = socket ;
        this.inputStream = new BufferedInputStream( socket.getInputStream() ) ;
        this.outputStream = new BufferedOutputStream( socket.getOutputStream() ) ;
        updateTime() ;
    }
    
    /**
     * コネクション破棄.
     */
    protected void destroySocket() {
        if( this.inputStream != null ) {
            try {
                this.inputStream.close() ;
            } catch( Exception e ) {
            }
        }
        if( this.outputStream != null ) {
            try {
                this.outputStream.flush() ;
            } catch( Exception e ) {
            }
            try {
                this.outputStream.close() ;
            } catch( Exception e ) {
            }
        }
        if( this.socket != null ) {
            try {
                this.socket.close() ;
            } catch( Exception e ) {
            }
        }
        this.socket = null ;
        this.inputStream = null ;
        this.outputStream = null ;
        setUpdateTime( -1L ) ;
    }
    
    private synchronized void setUpdateTime( long t ) {
        this.updateTime = t ;
    }
    
    private synchronized void updateTime() {
        this.updateTime = System.currentTimeMillis() ;
    }
    
    private synchronized long getUpdateTime() {
        return this.updateTime ;
    }
}
