package org.maachang.comet.net;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import org.maachang.comet.httpd.HttpdBinary;
import org.maachang.comet.httpd.HttpdHeaders;
import org.maachang.comet.httpd.HttpdRequest;
import org.maachang.comet.httpd.engine.HttpdDef;
import org.maachang.comet.httpd.engine.HttpdUtil;
import org.maachang.util.FileUtil;
import org.maachang.util.StringUtil;

/**
 * Http受信処理.
 * <BR>
 * Httpリクエスト情報を構成するための処理です.
 * 
 * @version 2008/02/08
 * @author masahito suzuki
 * @since MaachangComet 1.08
 */
public class ReadHttpdRequest {
    
    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 FORM_CONTENT_TYPE = "application/x-www-form-urlencoded" ;
    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 ReadHttpdRequest() {
        
    }
    
    /**
     * Httpリクエスト受信処理.
     * <BR><BR>
     * Httpリクエスト受信処理を実施します.
     * <BR>
     * @param stream Socket.getInputStreamを設定します.
     * @param seqId 対象のシーケンスIDを設定します.
     * @return HttpdRequest HttpdRequestオブジェクトが返されます.
     * @exception Exception 例外.
     */
    public static final HttpdRequest receiveHttpRequest( InputStream stream,int seqId )
        throws Exception {
        // ヘッダを取得.
        HttpdRequest ret = readHeader( stream ) ;
        
        // [POST]の場合は、コンテンツ長を取得して、Body情報を取得.
        if( HttpdDef.METHOD_POST.equals( ret.getMethod() ) ) {
            readResponseByBody( ret,stream ) ;
        }
        return ret ;
    }
    
    /**
     * ヘッダ受信処理.
     */
    private static final HttpdRequest readHeader( InputStream stream )
        throws Exception {
        int endPoint = 0 ;
        int endHeaderLen = END_HEADER.length ;
        ArrayBinary buf = new ArrayBinary() ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                if( buf.getLength() <= 0 ) {
                    return null ;
                }
                return convertHttpdHeader( buf.getBinary(),buf.getLength() ) ;
            }
            else {
                d = d & 0x000000ff ;
                buf.write( d ) ;
                if( d == ( int )( END_HEADER[ endPoint ] & 0x000000ff ) ) {
                    endPoint ++ ;
                    if( endPoint >= endHeaderLen ) {
                        if( buf.getLength() <= 0 ) {
                            return null ;
                        }
                        return convertHttpdHeader( buf.getBinary(),buf.getLength() ) ;
                    }
                }
                else {
                    endPoint = 0 ;
                }
            }
        }
    }
    
    /**
     * Body受信処理.
     */
    private static final boolean readResponseByBody( HttpdRequest req,InputStream stream )
        throws Exception {
        ArrayBinary buf = new ArrayBinary() ;
        // chunkedでの受信.
        if( req.getHeader().size( TRANSFER_ENCODING ) == 1 ) {
            String s = req.getHeader().getHeader( TRANSFER_ENCODING,0 ) ;
            if( CHUNKED.equals( s ) == false ) {
                throw new IOException( "不正な["+TRANSFER_ENCODING+"="+s+"]を検出しました" ) ;
            }
            readBodyByChunked( buf,stream ) ;
        }
        // コンテンツ長での受信.
        else if( req.getHeader().size( CONTENT_LENGTH ) == 1 ) {
            String s = req.getHeader().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 false ;
            }
            readBodyByLength( buf,contentLength,stream ) ;
        }
        // 未指定の場合の受信.
        else {
            readBodyByNoSetting( buf,stream ) ;
        }
        // 取得されたバイナリをセット.
        if( buf.getLength() > 0 ) {
            convertBody( req,buf.getBinary( buf.getLength() ) ) ;
            return true ;
        }
        return false ;
    }
    
    /**
     * chunked受信.
     */
    private static final void readBodyByChunked( ArrayBinary buf,InputStream stream )
        throws Exception {
        int len = -1 ;
        int enterPos = 0 ;
        int pos = 0 ;
        int cnt = 0 ;
        byte[] headBuf = 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( headBuf,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 {
                	headBuf[ pos ] = ( byte )d ;
                    pos ++ ;
                }
            }
            // データ受信.
            else {
                buf.write( d ) ;
                pos ++ ;
                if( len <= pos ) {
                    len = -1 ;
                    pos = 0 ;
                    enterPos = 0 ;
                }
            }
        }
    }
    
    /**
     * ContentLength指定の受信.
     */
    private static final void readBodyByLength( ArrayBinary buf,int contentLength,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            buf.write( d ) ;
            pnt ++ ;
            if( contentLength <= pnt ) {
                return ;
            }
        }
    }
    
    /**
     * 受信サイズ未指定の受信.
     */
    private static final void readBodyByNoSetting( ArrayBinary buf,InputStream stream )
        throws Exception {
        int pnt = 0 ;
        for( ;; ) {
            int d = stream.read() ;
            if( d <= -1 ) {
                return ;
            }
            buf.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ヘッダ数値["+s+"]を検出しました" ) ;
        }
    }
    
    /**
     * バイナリ検索.
     */
    private static int binaryIndexOf( byte[] binary,String data,int off,int len )
        throws Exception {
        if( binary == null || binary.length <= 0 ||
            data == null || data.length() <= 0 ) {
            return -1 ;
        }
        return binaryIndexOf( binary,data.getBytes( CHARSET ),off,len ) ;
    }
    
    /**
     * バイナリ検索.
     */
    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 HttpdRequest convertHttpdHeader( byte[] binary,int endPos )
        throws Exception {
        HttpdBaseRequestImpl ret = new HttpdBaseRequestImpl() ;
        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 ) {
                convertHttpExecutionAndVersion( ret,one ) ;
                if( ret.getHttpVersion().endsWith( HttpdDef.VERSION_09 ) == true ) {
                    // HTTPバージョンが0.9の場合.
                    convertAnalysisQuery( ret ) ;// パラメータを解析して取得.
                    return ret ;
                }
            }
            // ヘッダ解析.
            else {
                convertHttpHeader( ret,one ) ;
            }
            // ヘッダ終端を検出.
            if( p >= endPos ) {
                break ;
            }
            line ++ ;
        }
        convertAnalysisQuery( ret ) ;// URLパラメータ解析.
        return ret ;
    }
    /**
     * 処理タイプと、HTTPバージョンを取得.
     */
    private static final void convertHttpExecutionAndVersion( HttpdRequest out,String oneLine ) {
        int pos1 = oneLine.indexOf( " " ) ;
        int pos2 = oneLine.indexOf( " ",pos1+1 ) ;
        String exec = oneLine.substring( 0,pos1 ) ;
        String url = oneLine.substring( pos1+1,pos2 ) ;
        String version = oneLine.substring( pos2+1,oneLine.length() ) ;
        out.setMethod( exec.trim() ) ;
        out.setUrlPath( url.trim() ) ;
        out.setHttpVersion( version.trim() ) ;
    }
    
    /**
     * パラメータを解析.
     */
    private static final void convertHttpHeader( HttpdRequest 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.getHeader().addHeader( key,null ) ;
        }
        else if( key.indexOf( "User-Agent" ) != -1 ||
            key.indexOf( "Modified" ) != -1 ||
            key.indexOf( "Date" ) != -1 ) {
            out.getHeader().addHeader( key,oneLine ) ;
        }
        else {
            if( oneLine.indexOf( "," ) != -1 ) {
                ArrayList<String> lst = StringUtil.cutString( oneLine,"," ) ;
                int len = lst.size() ;
                HttpdHeaders header = out.getHeader() ;
                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 ) {
                                    header.addHeader( key,o ) ;
                                }
                                else {
                                    header.addHeader( key,o.substring( 0,sp ).trim() ) ;
                                    header.addHeader( key,o.substring( sp+1 ).trim() ) ;
                                }
                            }
                            else {
                                header.addHeader( key,o ) ;
                            }
                        }
                    }
                    else {
                        String o = lst.get( i ).trim() ;
                        if( o.length() > 0 ) {
                            header.addHeader( key,o ) ;
                        }
                    }
                }
            }
            else {
                oneLine = oneLine.trim() ;
                if( oneLine.length() > 0 ) {
                    out.getHeader().addHeader( key,oneLine ) ;
                }
            }
        }
    }
    
    /**
     * URLクエリパラメータを解析して、パラメータとして取得.
     * <BR>
     * @param out 対象のリクエスト情報を設定します.
     */
    public static final void convertAnalysisQuery( HttpdRequest out ) {
        String query = HttpdUtil.getQueryValue( out.getUrlPath() ) ;
        if( query != null && ( query = query.trim() ).length() > 0) {
            out.setUrlPath( HttpdUtil.getReadName( out.getUrlPath() ) ) ;
            HttpdUtil.convertQueryByParams( out.getQuery(),query,"UTF8" ) ;
        }
    }
    
    /**
     * 解析されたBodyデータをパラメータとして取得.
     */
    private static final void convertAnalysisBody( HttpdRequest out,String body ) {
        if( body != null && ( body = body.trim() ).length() > 0 ) {
            HttpdUtil.convertQueryByParams( out.getQuery(),body,"UTF8" ) ;
        }
    }
    
    /**
     * POST情報での、Body内容を解析.
     * <BR>
     * @param out 対象のHTTPリクエストを設定します.
     * @param binary 対象のバイナリを設定します.
     * @exception Exception 例外.
     */
    public static final void convertBody( HttpdRequest out,byte[] binary )
        throws Exception {
        int p = 0 ;
        int b = 0 ;
        int enterLen = ENTER.length ;
        int len = out.getHeader().size( "Content-Type" ) ;
        String multiPartForm = null ;
        String dataType = null ;
        for( int i = 0 ; i < len ; i ++ ) {
            String contentType = out.getHeader().getHeader( "Content-Type",i ) ;
            if( multiPartForm == null ) {
                int boundary = -1 ;
                if( ( boundary = contentType.indexOf( "multipart/form-data" ) ) != -1 &&
                    ( boundary = contentType.indexOf( "boundary=",boundary ) ) != -1 ) {
                    multiPartForm = contentType.substring( boundary+"boundary=".length(),contentType.length() ) ;
                }
            }
            if( dataType == null || contentType.startsWith( FORM_CONTENT_TYPE ) == false ) {
                dataType = contentType ;
            }
        }
        // POSTに[Content-Type]が存在しない場合.
        if( dataType == null ) {
            // [application/x-www-form-urlencoded]であることにする(IEのAjax対応).
            dataType = FORM_CONTENT_TYPE ;
        }
        // HTTPがMultipartFormの場合.
        if( multiPartForm != null ) {
            out.setBody( null ) ;// body情報は保持しない.
            String target = "--" + multiPartForm ;
            String end = target + "--" ;
            String keyName = null ;
            String fileName = null ;
            String mimeType = null ;
            boolean dataFlag = false ;
            boolean keyFlag = false ;
            int filenameLen = "; filename=\"".length() ;
            int nameLen = "; name=\"".length() ;
            for( ;; ) {
                // データ条件で無い場合.
                if( dataFlag == false ) {
                    p = binaryIndexOf( binary,ENTER,b,binary.length ) ;
                    if( p == -1 ) {
                        break ;
                    }
                    else if( p == b ) {
                        b += enterLen ;
                        continue ;
                    }
                    String one = new String( binary,b,p-b,CHARSET ) ;
                    b = p + enterLen ;
                    // データ開始値.
                    if( keyFlag == false ) {
                        // データの開始値の場合.
                        if( one.equals( target ) == true ) {
                            keyFlag = true ;
                        }
                        // 全てのデータ終了値の場合.
                        else if( one.equals( end ) == true ) {
                            break ;
                        }
                        // その他.
                        else {
                            throw new IllegalArgumentException( "不正な条件を検出しました" ) ;
                        }
                    }
                    // データキー名など.
                    else {
                        // データ情報の場合.
                        if( one.length() <= 0 ) {
                            dataFlag = true ;
                        }
                        // データキー名などの情報.
                        else {
                            // アップロードファイル名.
                            if( one.startsWith( "Content-Disposition:" ) ) {
                                int filePnt = one.indexOf( "; filename=\"" ) ;
                                if( filePnt != -1 ) {
                                    filePnt+= filenameLen ;
                                    String fullPath = one.substring( filePnt,one.indexOf( "\"",filePnt ) ).trim() ;
                                    fileName = FileUtil.getFileName( fullPath ).trim() ;
                                }
                            }
                            // データキー名.
                            int namePnt = one.indexOf( "; name=\"" ) ;
                            if( namePnt != -1 ) {
                                namePnt+=nameLen ;
                                keyName = one.substring( namePnt,one.indexOf( "\"",namePnt ) ).trim() ;
                            }
                            // MimeType.
                            else if( one.startsWith( "Content-Type:" ) ) {
                                mimeType = one.substring( "Content-Type:".length(),one.length() ).trim() ;
                            }
                            // その他.
                            else {
                                throw new IllegalArgumentException( "不正な条件を検出しました" ) ;
                            }
                        }
                    }
                }
                // データ情報.
                else {
                    // uploadBinary情報.
                    if( fileName != null ) {
                        p = binaryIndexOf( binary,target,b,binary.length ) ;
                        if( p == -1 ) {
                            throw new IllegalArgumentException( "不正な条件を検出しました" ) ;
                        }
                        if( fileName == null || ( fileName = fileName.trim() ).length() <= 0 ) {
                            b = p ;
                        }
                        else {
                            len = p-(b+enterLen) ;
                            byte[] uploadBinary = new byte[ len ] ;
                            System.arraycopy( binary,b,uploadBinary,0,len ) ;
                            b = p ;
                            HttpdBinary bin = new HttpdBinary( fileName,mimeType,uploadBinary ) ;
                            out.getQuery().addParam( keyName,bin ) ;
                        }
                    }
                    else {
                        p = binaryIndexOf( binary,target,b,binary.length ) ;
                        if( p == -1 ) {
                            throw new IllegalArgumentException( "不正な条件を検出しました" ) ;
                        }
                        String one = new String( binary,b,p-(b+enterLen),CHARSET ) ;
                        b = p ;
                        out.getQuery().addParam( keyName,one ) ;
                    }
                    keyName = null ;
                    fileName = null ;
                    mimeType = null ;
                    dataFlag = false ;
                    keyFlag = false ;
                }
            }
        }
        // 通常のForm情報の場合.
        else if( dataType != null && dataType.startsWith( FORM_CONTENT_TYPE ) == true ) {
            String charset = getCharset( dataType ) ;
            // body情報を保持.
            out.setBody( binary ) ;
            boolean end = false ;
            StringBuilder buf = new StringBuilder() ;
            for( ;; ) {
                if( end == true ) {
                    break ;
                }
                p = binaryIndexOf( binary,ENTER,b,binary.length ) ;
                if( p <= -1 ) {
                    p = binary.length ;
                    end = true ;
                }
                if( p == b ) {
                    b += enterLen ;
                    continue ;
                }
                String one = new String( binary,b,p-b,charset ) ;
                one = StringUtil.changeString( one,"\r","" ) ;
                one = StringUtil.changeString( one,"\n","" ) ;
                b = p + enterLen ;
                
                // データ終端を検出.
                if( one.length() <= 0 ) {
                    break ;
                }
                buf.append( one ) ;
            }
            String bodys = buf.toString() ;
            buf = null ;
            convertAnalysisBody( out,bodys ) ;
            bodys = null ;
        }
        // その他のForm情報の場合.
        else {
            // データは区切らない.
            out.setBody( binary ) ;
        }
    }
    
    private static final String getCharset( String contentType ) {
        int p = contentType.indexOf( "charset=" ) ;
        if( p <= -1 ) {
            return CHARSET ;
        }
        else {
            return contentType.substring( p+"charset=".length() ) ;
        }
    }
}

class ArrayBinary {
    protected static final int MAX_LENGTH = 32 * 0x00100000 ;
    private static final int BUFFER = 4096 ;
    private byte[] binary = null ;
    private int length = 0 ;
    
    public ArrayBinary() {
        this.binary = new byte[ BUFFER ] ;
        this.length = 0 ;
    }
    
    protected void finalize() throws Exception {
        this.destroy() ;
    }
    
    public void destroy() {
        this.binary = null ;
        this.length = 0 ;
    }
    
    public void reset() {
        if( this.binary.length == BUFFER ) {
            this.length = 0 ;
        }
        else {
            this.binary = new byte[ BUFFER ] ;
            this.length = 0 ;
        }
    }
    
    public void write( int b ) throws Exception {
        if( length >= MAX_LENGTH ) {
            this.destroy() ;
            throw new IOException( "受信データ長が最大値["+MAX_LENGTH+"]を越しています" ) ;
        }
        if( binary.length <= length ) {
            byte[] t = binary ;
            int iLen = t.length * 2 ;
            if( iLen >= MAX_LENGTH ) {
                iLen = MAX_LENGTH ;
            }
            binary = new byte[ iLen ] ;
            System.arraycopy( t,0,binary,0,t.length ) ;
            t = null ;
        }
        binary[ length ] = ( byte )( b & 0x000000ff ) ;
        length ++ ;
    }
    
    public byte getByte( int no ) {
        return binary[ no ] ;
    }
    
    public byte[] getBinary() {
        return binary ;
    }
    
    public byte[] getBinary( int length ) {
        if( length <= 0 ) {
            length = this.length ;
        }
        if( this.length < length ) {
            length = this.length ;
        }
        byte[] ret = new byte[ length ] ;
        System.arraycopy( this.binary,0,ret,0,length ) ;
        return ret ;
    }
    
    public int getLength() {
        return length ;
    }
}
