package org.maachang.proxy.engine.net.server;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.util.ArrayList;

import org.maachang.proxy.engine.net.ResponseInfo;
import org.maachang.util.AnalysisUtil;
import org.maachang.util.ArrayBinary;
import org.maachang.util.StringUtil;

/**
 * HTTPサーバにリクエストを行う処理.
 * 
 * @version 2008/04/08
 * @author masahito suzuki
 * @since MaachangProxy 1.01
 */
public class ReadHttpResponse {
    
    //private static final int WAIT_COUNT = 16 ;
    private static final String CONTENT_LENGTH = "Content-Length" ;
    private static final String TRANSFER_ENCODING = "Transfer-Encoding" ;
    private static final String CHUNKED = "chunked" ;
    private static final String CHARSET = "UTF8" ;
    private static final byte[] ENTER = new byte[] {(byte) 13, (byte) 10} ;
    private static final byte[] END_HEADER = new byte[] {
        (byte) 0x0d, (byte) 0x0a,(byte) 0x0d, (byte) 0x0a} ;
    
    private ReadHttpResponse() {
        
    }
    
    /**
     * ヘッダ受信処理.
     * @param stream 受信用streamを設定します.
     * @return ResponseInfo ヘッダ情報が格納されたResponse情報が返されます.
     * @exception Exception 例外.
     */
    public static final ResponseInfo readResponseByHeader( InputStream stream )
        throws Exception {
        try {
            int endPoint = 0 ;
            int endHeaderLen = END_HEADER.length ;
            ArrayBinary buf = new ArrayBinary() ;
            for( ;; ) {
                int d = stream.read() ;
                if( d <= -1 ) {
                    if( buf.length() <= 0 ) {
                        return null ;
                    }
                    return convertHttpdHeader( buf.getRawBinary(),buf.length() ) ;
                }
                else {
                    d = d & 0x000000ff ;
                    buf.write( d ) ;
                    if( d == ( int )( END_HEADER[ endPoint ] & 0x000000ff ) ) {
                        endPoint ++ ;
                        if( endPoint >= endHeaderLen ) {
                            if( buf.length() <= 0 ) {
                                return null ;
                            }
                            return convertHttpdHeader( buf.getRawBinary(),buf.length() ) ;
                        }
                    }
                    else {
                        endPoint = 0 ;
                    }
                }
            }
        } catch( SocketException se ) {
            // この例外の場合、Expireエラーが濃厚なので、それを返す.
            throw new ExpireServerConnectException( "ヘッダ受信時にエラーが発生しました" ) ;
        }
    }
    
    /**
     * Body受信処理.
     * @param out 情報取得用OutputStreamを設定します.<BR>
     *            ByteArrayOutputStreamの場合、取得した情報は、レスポンスのBodyに直接格納されます.
     * @param res 対象のレスポンス情報を設定します.
     * @param stream 受信用streamを設定します.
     * @exception Exception 例外.
     */
    public static final void readResponseByBody( OutputStream out,ResponseInfo res,InputStream stream )
        throws Exception {
        boolean rawMode = true ;
        if( out instanceof ByteArrayOutputStream ) {
            rawMode = false ;
        }
        // chunkedでの受信.
        if( res.headerSize( TRANSFER_ENCODING ) == 1 ) {
            String s = res.getHeader( TRANSFER_ENCODING,0 ) ;
            if( CHUNKED.equals( s ) == false ) {
                throw new IOException( "不正な["+TRANSFER_ENCODING+"="+s+"]を検出しました" ) ;
            }
            if( rawMode == true ) {
                readBodyByChunkedToRaw( out,stream ) ;
            }
            else {
                readBodyByChunked( out,stream ) ;
            }
        }
        // コンテンツ長での受信.
        else if( res.headerSize( CONTENT_LENGTH ) == 1 ) {
            String s = res.getHeader( CONTENT_LENGTH,0 ) ;
            if( s == null || s.length() <= 0 ) {
                throw new IOException( "不正な["+CONTENT_LENGTH+"]を検出しました" ) ;
            }
            // コンテンツ長を取得.
            int contentLength = Integer.parseInt( s ) ;
            if( contentLength <= -1 ) {
                throw new IOException( "受信データ長["+contentLength+"]は不正です" ) ;
            }
            else if( contentLength == 0 ) {
                return ;
            }
            readBodyByLength( out,contentLength,stream ) ;
        }
        // 未指定の場合の受信.
        else {
            readBodyByNoSetting( out,stream ) ;
        }
        if( rawMode == false ) {
            byte[] b = ( ( ByteArrayOutputStream )out ).toByteArray() ;
            res.setBody( b ) ;
            out.close() ;
            out = null ;
        }
    }
    
    /**
     * chunked受信.
     */
    private static final void readBodyByChunked( OutputStream out,InputStream stream )
        throws Exception {
        int len = -1 ;
        int enterPos = 0 ;
        int pos = 0 ;
        int cnt = 0 ;
        byte[] buf = new byte[ 32 ] ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                throw new IOException( "不正なchunked終端を検出しました("+len+")" ) ;
            }
            d = d & 0x000000ff ;
            cnt ++ ;
            // chunkedデータ長を取得.
            if( len == -1 ) {
                // 改行を検出.
                if( d == ( int )( ENTER[ enterPos ] & 0x000000ff ) ) {
                    enterPos ++ ;
                    if( enterPos >= ENTER.length ) {
                        if( pos > 0 ) {
                            String chLen = new String( buf,0,pos,CHARSET ) ;
                            len = convertChunkedDataLength( chLen ) ;
                            // chunked終端.
                            if( len == 0 ) {
                                // 残りのデータが無くなるまで取得.
                                int exitCnt = 0 ;
                                for( ;; ) {
                                    if( stream.available() > 0 ) {
                                        stream.read() ;
                                        exitCnt ++ ;
                                        if( exitCnt >= ENTER.length ) {
                                            break ;
                                        }
                                    }
                                }
                                return ;
                            }
                            pos = 0 ;
                            enterPos = 0 ;
                        }
                        else {
                            enterPos = 0 ;
                        }
                    }
                }
                else if( enterPos >= 1 ) {
                    throw new IOException( "不正なchunkedデータを検出しました" ) ;
                }
                else {
                    buf[ pos ] = ( byte )d ;
                    pos ++ ;
                }
            }
            // データ受信.
            else {
                out.write( d ) ;
                pos ++ ;
                if( len <= pos ) {
                    len = -1 ;
                    pos = 0 ;
                    enterPos = 0 ;
                }
            }
        }
    }
    
    /**
     * 直接サーバに送信するChunked処理.
     */
    private static final void readBodyByChunkedToRaw( OutputStream out,InputStream stream )
        throws Exception {
        int len = -1 ;
        int enterPos = 0 ;
        int pos = 0 ;
        int cnt = 0 ;
        byte[] buf = new byte[ 32 ] ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                throw new IOException( "不正なchunked終端を検出しました("+len+")" ) ;
            }
            d = d & 0x000000ff ;
            cnt ++ ;
            out.write( d ) ;
            // chunkedデータ長を取得.
            if( len == -1 ) {
                // 改行を検出.
                if( d == ( int )( ENTER[ enterPos ] & 0x000000ff ) ) {
                    enterPos ++ ;
                    if( enterPos >= ENTER.length ) {
                        if( pos > 0 ) {
                            String chLen = new String( buf,0,pos,CHARSET ) ;
                            len = convertChunkedDataLength( chLen ) ;
                            // chunked終端.
                            if( len == 0 ) {
                                // 残りのデータが無くなるまで取得.
                                int exitCnt = 0 ;
                                for( ;; ) {
                                    if( stream.available() > 0 ) {
                                        d = stream.read() ;
                                        d = d & 0x000000ff ;
                                        exitCnt ++ ;
                                        out.write( d ) ;
                                        if( exitCnt >= ENTER.length ) {
                                            break ;
                                        }
                                    }
                                }
                                return ;
                            }
                            pos = 0 ;
                            enterPos = 0 ;
                        }
                        else {
                            enterPos = 0 ;
                        }
                    }
                }
                else if( enterPos >= 1 ) {
                    throw new IOException( "不正なchunkedデータを検出しました" ) ;
                }
                else {
                    buf[ pos ] = ( byte )d ;
                    pos ++ ;
                }
            }
            // データ受信.
            else {
                pos ++ ;
                if( len <= pos ) {
                    len = -1 ;
                    pos = 0 ;
                    enterPos = 0 ;
                }
            }
        }
    }
    
    /**
     * ContentLength指定の受信.
     */
    private static final void readBodyByLength( OutputStream out,int contentLength,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            out.write( d ) ;
            pnt ++ ;
            if( contentLength <= pnt ) {
                // 残りのデータが無くなるまで取得.
                /*int exitCnt = 0 ;
                for( ;; ) {
                    if( stream.available() > 0 ) {
                        stream.read() ;
                        exitCnt = 0 ;
                    }
                    else if( exitCnt >= WAIT_COUNT ) {
                        break ;
                    }
                    else {
                        exitCnt ++ ;
                        Thread.sleep( 1 ) ;
                    }
                }*/
                return ;
            }
        }
    }
    
    /**
     * 受信サイズ未指定の受信.
     */
    private static final void readBodyByNoSetting( OutputStream out,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            out.write( d ) ;
            pnt ++ ;
        }
    }
    
    /**
     * Chunked長を計算します.
     */
    private static final int convertChunkedDataLength( String s )
        throws Exception {
        try {
            return Integer.parseInt( s,16 ) ;
        } catch( Exception e ) {
            if( s == null || s.length() <= 0 ) {
                throw new IOException( "不正なchunkedヘッダ数値を検出しました" ) ;
            }
            throw new IOException( "不正なchunkedヘッダ数値["+AnalysisUtil.toStringByBinary( s.getBytes() )+"-("+s+")]を検出しました" ) ;
        }
    }
    
    /**
     * バイナリ検索.
     */
    private static int binaryIndexOf( byte[] binary,byte[] data,int off,int len )
        throws Exception {
        if( binary == null || binary.length <= 0 ||
            data == null || data.length <= 0 ) {
            return -1 ;
        }
        int dataLen = data.length ;
        if( len <= 0 || len >= binary.length ) {
            len = binary.length ;
        }
        int ret = -1 ;
        for( int i = off ; i < len ; i ++ ) {
            if( binary[ i ] == data[ 0 ] && i+dataLen <= len ) {
                ret = i ;
                for( int j = 1 ; j < dataLen ; j ++ ) {
                    if( binary[ i+j ] != data[ j ] ) {
                        ret = -1 ;
                        break ;
                    }
                }
                if( ret != -1 ) {
                    return ret ;
                }
            }
        }
        return -1 ;
    }
    
    /**
     * HTTPヘッダを解析.
     */
    private static final ResponseInfo convertHttpdHeader( byte[] binary,int endPos )
        throws Exception {
        ResponseInfo ret = new ResponseInfo() ;
        int p = 0 ;
        int b = 0 ;
        int line = 0 ;
        int enterLen = ENTER.length ;
        // ヘッダ情報を読み込む.
        for( ;; ) {
            p = binaryIndexOf( binary,ENTER,b,endPos ) ;
            if( p == -1 ) {
                if( b >= endPos ) {
                    break ;
                }
                p = endPos ;
            }
            String one = new String( binary,b,p-b,CHARSET ) ;
            b = p + enterLen ;
            
            // Method情報を取得.
            if( line == 0 ) {
                convertResultStateAndVersion( ret,one ) ;
            }
            // ヘッダ解析.
            else {
                convertHttpHeader( ret,one ) ;
            }
            // ヘッダ終端を検出.
            if( p >= endPos ) {
                break ;
            }
            line ++ ;
        }
        return ret ;
    }
    /**
     * レスポンスステータスと、HTTPバージョンを取得.
     */
    private static final void convertResultStateAndVersion( ResponseInfo out,String oneLine ) {
        int pos1 = oneLine.indexOf( " " ) ;
        int pos2 = oneLine.indexOf( " ",pos1+1 ) ;
        String version = oneLine.substring( 0,pos1 ) ;
        String state = oneLine.substring( pos1+1,pos2 ) ;
        String stateMessage = oneLine.substring( pos2+1,oneLine.length() ) ;
        out.setVersion( version.trim() ) ;
        out.setState( state.trim() ) ;
        out.setStateMessage( stateMessage.trim() ) ;
    }
    
    /**
     * Httpヘッダを解析.
     */
    private static final void convertHttpHeader( ResponseInfo out,String oneLine ) {
        int p = oneLine.indexOf( ":" ) ;
        if( p == -1 ) {
            return ;
        }
        String key = oneLine.substring( 0,p ).trim() ;
        p += 1 ;
        oneLine = oneLine.substring( p+1,oneLine.length() ) ;
        if( oneLine == null || ( oneLine = oneLine.trim() ).length() <= 0 ) {
            out.addHeader( key,null ) ;
        }
        else if( key.indexOf( "User-Agent" ) != -1 ||
            key.indexOf( "Modified" ) != -1 ||
            key.indexOf( "Date" ) != -1 ) {
            out.putHeader( key,oneLine ) ;
        }
        else {
            if( oneLine.indexOf( "," ) != -1 ) {
                ArrayList<String> lst = StringUtil.cutString( oneLine,"," ) ;
                int len = lst.size() ;
                for( int i = 0 ; i < len ; i ++ ) {
                    if( i == 0 ) {
                        String o = lst.get( i ).trim() ;
                        if( o.length() > 0 ) {
                            int sp = o.indexOf( " " ) ;
                            if( sp != -1 ) {
                                boolean flg = false ;
                                for( int j = 0 ; j < sp ; j ++ ) {
                                    char c = o.charAt( j ) ;
                                    if( c == '\"' || c == '\'' ) {
                                        flg = true ;
                                        break ;
                                    }
                                }
                                if( flg == true ) {
                                    out.addHeader( key,o ) ;
                                }
                                else {
                                    out.addHeader( key,o.substring( 0,sp ).trim() ) ;
                                    out.addHeader( key,o.substring( sp+1 ).trim() ) ;
                                }
                            }
                            else {
                                out.addHeader( key,o ) ;
                            }
                        }
                    }
                    else {
                        String o = lst.get( i ).trim() ;
                        if( o.length() > 0 ) {
                            out.addHeader( key,o ) ;
                        }
                    }
                }
            }
            else {
                oneLine = oneLine.trim() ;
                if( oneLine.length() > 0 ) {
                    out.addHeader( key,oneLine ) ;
                }
            }
        }
    }
}
