package org.maachang.comet.proxy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;

import org.maachang.comet.conf.MimeConfig;
import org.maachang.comet.conf.ServiceDef;
import org.maachang.comet.httpd.HttpConnectionInfo;
import org.maachang.comet.httpd.HttpdErrorDef;
import org.maachang.comet.httpd.HttpdHeaders;
import org.maachang.comet.httpd.HttpdRequest;
import org.maachang.comet.httpd.HttpdResponse;
import org.maachang.comet.httpd.HttpdVersionDef;
import org.maachang.comet.httpd.engine.HttpdCookie;
import org.maachang.comet.httpd.engine.HttpdDef;
import org.maachang.comet.httpd.engine.HttpdTimestamp;
import org.maachang.comet.httpd.engine.script.ScriptDef;
import org.maachang.comet.httpd.engine.session.HttpdSession;
import org.maachang.comet.httpd.engine.session.HttpdSessionManager;
import org.maachang.conf.ConvIniParam;
import org.maachang.manager.GlobalManager;
import org.maachang.util.ArrayBinary;
import org.maachang.util.ConvertParam;
import org.maachang.util.FileUtil;
import org.maachang.util.GZIPBinary;

/**
 * HTTPD基本レスポンス実装.
 * 
 * @version 2007/08/26
 * @author masahito suzuki
 * @since MaachangComet 1.00
 */
public class HttpdProxyResponseImpl implements HttpdResponse {
    
    /**
     * HTTPコード.
     */
    protected int state = HttpdErrorDef.HTTP11_200 ;
    
    /**
     * 戻りHTTPバージョン.
     */
    protected String version = HttpdDef.VERSION_11 ;
    
    /**
     * コネクションオブジェクト.
     */
    private HttpConnectionInfo connectionInfo = null ;
    
    /**
     * 戻りヘッダ.
     */
    protected HttpdHeaders header = null ;
    
    /**
     * HTTPキャッシュ.
     */
    protected boolean cacheMode = false ;
    
    /**
     * gzip圧縮フラグ.
     */
    protected boolean gzipFlag = true ;
    
    /**
     * リクエストGZIP名.
     */
    protected String gzipName = null ;
    
    /**
     * OutputStream.
     */
    private OutputStream outputStream = null ;
    
    /**
     * printWriter.
     */
    private PrintWriter printWriter = null ;
    
    /**
     * 出力対象バイナリ.
     */
    private ByteArrayOutputStream binaryStream = null ;
    private byte[] outputBinary = null ;
    
    /**
     * コンストラクタ.
     */
    private HttpdProxyResponseImpl() {
        
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * ステータスを設定してオブジェクトを生成します.
     * <BR>
     * @param request 対象のリクエストを設定します.
     * @param url 対象のリクエストURLを設定します.
     * @param timeout 対象のタイムアウト値を設定します.
     * @param count 対象のカウント値を設定します.
     * @exception Exception 例外.
     */
    public HttpdProxyResponseImpl( HttpdRequest request,String url,long timeout,int count )
        throws Exception {
        this( request,url,HttpdErrorDef.HTTP10_200,timeout,count ) ;
    }
    
    /**
     * コンストラクタ.
     * <BR><BR>
     * ステータスを設定してオブジェクトを生成します.
     * <BR>
     * @param request 対象のリクエストを設定します.
     * @param url 対象のリクエストURLを設定します.
     * @param state 対象のHTTPコードを設定します.
     * @param timeout 対象のタイムアウト値を設定します.
     * @param count 対象のカウント値を設定します.
     * @exception Exception 例外.
     */
    public HttpdProxyResponseImpl( HttpdRequest request,String url,int state,long timeout,int count )
        throws Exception {
        if( request == null || request.getConnectionInfo() == null ||
            request.getConnectionInfo().isUse() == false ) {
            throw new IOException( "引数は不正です" ) ;
        }
        this.state = state ;
        this.connectionInfo = request.getConnectionInfo() ;
        this.header = new HttpdHeaders() ;
        // 拡張子から、コンテンツタイプを設定.
        noSetByContentType( this.header,url ) ;
        // GZIP圧縮可能かチェック.
        if( request.getHeader().getHeader( HttpdDef.VALUE_ENCODE ) != null ) {
            HttpdHeaders requestHeader = request.getHeader() ;
            int len = requestHeader.size( HttpdDef.VALUE_ENCODE ) ;
            boolean chkFlag = false ;
            if( request.isGzip() == true ) {
                for( int i = 0 ; i < len ; i ++ ) {
                    String encode = requestHeader.getHeader( HttpdDef.VALUE_ENCODE,i ) ;
                    if( encode != null && encode.indexOf( "gzip" ) != -1 ) {
                        if( ( encode = encode.trim() ).length() <= 0 ) {
                            this.gzipName = null ;
                            chkFlag = false ;
                        }
                        else {
                            this.gzipName = encode ;
                            chkFlag = true ;
                        }
                        break ;
                    }
                }
            }
            if( chkFlag == false ) {
                this.gzipName = null ;
            }
            // 指定拡張子が、非圧縮対象の場合かチェック.
            else {
                MimeConfig mimeType = ( MimeConfig )GlobalManager.getValue( ServiceDef.MANAGER_BY_MIME_TYPE ) ;
                int p = url.lastIndexOf( "." ) ;
                String plus = null ;
                if( p != -1 ) {
                    plus = url.substring( p+1 ) ;
                }
                if( plus == null || ( plus = plus.trim() ).length() <= 0 ) {
                    plus = "" ;
                }
                String type = mimeType.get( MimeConfig.OPTION_GZIP+plus ) ;
                if( ConvIniParam.getBoolean( type ) == false ) {
                    this.gzipName = null ;
                }
            }
        }
        else {
            this.gzipName = null ;
        }
        this.version = request.getVersion() ;
    }
    
    /**
     * デストラクタ.
     */
    protected void finalize() throws Exception {
        destroy() ;
    }
    
    /**
     * オブジェクトを破棄.
     * <BR><BR>
     * オブジェクトを破棄します.
     */
    public void destroy() {
        this.flush() ;
        destroyByNoOutputStream() ;
        if( printWriter != null ) {
            try {
                printWriter.close() ;
            } catch( Exception e ) {
            }
        }
        if( outputStream != null ) {
            outputBinary = binaryStream.toByteArray() ;
            try {
                outputStream.close() ;
            } catch( Exception e ) {
            }
            try {
                binaryStream = null ;
                Object[] o = new Object[ 1 ] ;
                o[ 0 ] = outputBinary ;
                sendProxyServer( o ) ;
            } catch( Exception e ) {
            }
        }
        if( connectionInfo != null ) {
            connectionInfo.destroy() ;
        }
        header = null ;
        cacheMode = false ;
        gzipFlag = true ;
        gzipName = null ;
        connectionInfo = null ;
        outputStream = null ;
        printWriter = null ;
        binaryStream = null ;
        outputBinary = null ;
    }
    
    /**
     * キャンセル処理.
     */
    public void cancel() {
        if( connectionInfo != null ) {
            connectionInfo.cancel() ;
        }
        connectionInfo = null ;
        header = null ;
        cacheMode = false ;
        gzipFlag = true ;
        gzipName = null ;
        outputStream = null ;
        printWriter = null ;
    }
    
    /**
     * 出力完了処理.
     * <BR><BR>
     * 出力を完了する処理を実施します.
     */
    public void flush() {
        if( printWriter != null ) {
            try {
                printWriter.flush() ;
            } catch( Exception e ) {
            }
        }
        if( outputStream != null ) {
            try {
                outputStream.flush() ;
            } catch( Exception e ) {
            }
            if( outputStream instanceof GZIPOutputStream ) {
                try {
                    ( ( GZIPOutputStream )outputStream ).finish() ;
                } catch( Exception e ) {
                }
            }
        }
    }
    
    /**
     * HTTPコードを設定.
     * <BR><BR>
     * HTTPコードを設定します.
     * <BR>
     * @param state 対象のHTTPコードを設定します.
     */
    public void setState( int state ) {
        this.state = state ;
    }
    
    /**
     * HTTPコードを取得.
     * <BR><BR>
     * HTTPコードを取得します.
     * <BR>
     * @return int HTTPコードが返されます.
     */
    public int getState() {
        return state ;
    }
    
    /**
     * HTTPバージョン情報を設定.
     * <BR><BR>
     * HTTPバージョン情報を設定します.
     * <BR>
     * @param version 対象のHTTPバージョンを設定します.
     */
    public void setVersion( String version ) {
        this.version = version ;
    }
    
    /**
     * HTTPバージョン情報を取得.
     * <BR><BR>
     * HTTPバージョン情報を取得します.
     * <BR>
     * @return String 対象のHTTPバージョンが返されます.
     */
    public String getVersion() {
        return version ;
    }
    
    /**
     * GZIP圧縮条件を設定.
     * <BR><BR>
     * GZIP圧縮条件を設定します.
     * <BR>
     * @param flag GZIP圧縮条件を設定します.
     */
    public void setGzip( boolean flag ) {
        gzipFlag = flag ;
    }
    
    /**
     * GZIP圧縮条件を取得.
     * <BR><BR>
     * GZIP圧縮条件を取得します.
     * <BR>
     * @return boolean GZIP圧縮条件が返されます.
     */
    public boolean getGzip() {
        if( gzipName == null ) {
            return false ;
        }
        return gzipFlag ;
    }
    
    /**
     * HTTPキャッシュ条件を設定.
     * <BR><BR>
     * HTTPキャッシュ条件を設定します.
     * <BR>
     * @param cache HTTPキャッシュ条件を設定します.
     */
    public void setHttpCache( boolean cache ) {
        this.cacheMode = cache ;
    }
    
    /**
     * HTTPキャッシュ条件を取得.
     * <BR><BR>
     * HTTPキャッシュ条件を取得します.
     * <BR>
     * @return boolean HTTPキャッシュ条件が返されます.
     */
    public boolean getHttpCache() {
        return cacheMode ;
    }
    
    /**
     * リクエストに設定されているSessionCookie条件を設定.
     * <BR><BR>
     * リクエストに設定されているSessionCookie条件を設定します.
     * <BR>
     * @param request 対象のリクエストを設定します.
     * @exception Exception 例外.
     */
    public void setCookieSession( HttpdRequest request )
        throws Exception {
        if( request == null ) {
            return ;
        }
        HttpdSession session = request.getSession() ;
        if( session != null ) {
            int len = 0 ;
            if( ( len = header.size( HttpdCookie.SET_COOKIE ) ) > 0 ) {
                for( int i = len-1 ; i >= 0 ; i -- ) {
                    String value = header.getHeader( HttpdCookie.SET_COOKIE,i ) ;
                    if( value != null ) {
                        if( value.indexOf( HttpdSession.SESSION_NAME+"=" ) != -1 ) {
                            header.removeHeader( HttpdCookie.SET_COOKIE,i ) ;
                        }
                    }
                }
            }
            HttpdSessionManager sessionManager = ( HttpdSessionManager )GlobalManager.getValue(
                ServiceDef.MANAGER_BY_SESSION ) ;
            long time = System.currentTimeMillis() + sessionManager.getDeleteTime() ;
            String value = HttpdCookie.createCookie(
                HttpdSession.SESSION_NAME,
                session.getSessionId(),
                "/",time ) ;
            header.addHeader( HttpdCookie.SET_COOKIE,value ) ;
        }
    }
    
    /**
     * ヘッダオブジェクトを取得.
     * <BR><BR>
     * ヘッダオブジェクトを取得します.
     * <BR>
     * @return HttpdHeaders ヘッダオブジェクトが返されます.
     */
    public HttpdHeaders getHeader() {
        return header ;
    }
    
    /**
     * ソケットクローズフラグを設定.
     * <BR><BR>
     * 送信結果と同時に、ソケットをクローズする場合は、[true]を設定します.
     * <BR>
     * @param mode [true]の場合、このオブジェクトが破棄されると同時に、コネクションが切断されます.
     */
    public void setDestroyByClose( boolean mode ) {
        // 何もしない.
    }
    
    /**
     * リクエストKeepAliveモードを取得.
     * <BR><BR>
     * リクエストKeepAliveモードを取得します.
     * <BR>
     * @return boolean [true]の場合リクエストのKeepAliveは有効です.
     */
    public boolean isRequestKeepAlive() {
        return false ;
    }
    
    /**
     * HTTPクローズ条件を設定.
     * <BR><BR>
     * HTTPクローズ条件を設定します.
     * <BR>
     * @param closeFlag HTTPクローズ条件を設定します.
     */
    public void setHttpClose( boolean closeFlag ) {
        // 何もしない.
    }
    
    /**
     * HTTPクローズ条件を取得.
     * <BR><BR>
     * HTTPクローズ条件を取得します.
     * <BR>
     * @return boolean HTTPクローズ条件が返されます.
     */
    public boolean getHttpClose() {
        return true ;
    }
    
    /**
     * 戻り情報を設定するオブジェクトを取得.
     * <BR><BR>
     * 戻り情報を設定するオブジェクトを取得します.
     * <BR>
     * @return OutputStream 戻り情報を設定するオブジェクトが返されます.
     * @exception Exception 例外
     */
    public OutputStream getOutput()
        throws Exception {
        if( printWriter != null ) {
            return null ;
        }
        if( outputStream == null ) {
            // 基本ヘッダ群を設定.
            header.setHeader( "Server",HttpdVersionDef.getPrintServerName() ) ;
            header.setHeader( "Date",HttpdTimestamp.getNowTimestamp() ) ;
            
            // 非キャッシュ状態の場合.
            if( cacheMode == true ) {
                header.setHeader( "Expires",HttpdTimestamp.getTimestamp( 0L ) );
                header.setHeader( "Pragma","no-cache" ) ;
                header.setHeader( HttpdDef.VALUE_LAST_UPDATE,HttpdTimestamp.getNowTimestamp() ) ;
                header.setHeader( "Cache-Control","private" ) ;
                header.addHeader( "Cache-Control","no-cache" ) ;
                header.addHeader( "Cache-Control","no-store" ) ;
                header.addHeader( "Cache-Control","max-age=0" ) ;
            }
            // GZIP圧縮が有効の場合.
            if( HttpdDef.VERSION_11.equals( version ) == true && getGzip() == true ) {
                header.setHeader( "Content-Encoding",gzipName ) ;
            }
            // 出力用OutputStreamを生成.
            this.binaryStream = new ByteArrayOutputStream() ;
            // GIZPの場合.
            if( getGzip() == true ) {
                outputStream = new GZIPOutputStream( binaryStream ) ;
            }
            // GZIP以外の場合.
            else {
                outputStream = binaryStream ;
            }
        }
        return outputStream ;
    }
    
    /**
     * 戻り情報を設定するPrintWriterを取得.
     * <BR><BR>
     * 戻り情報を設定するPrintWriterを取得します.
     * <BR>
     * @return PrintWriter 戻り情報を設定するオブジェクトが返されます.
     * @exception Exception 例外
     */
    public PrintWriter getPrint()
        throws Exception {
        if( printWriter == null ) {
            OutputStream os = getOutput() ;
            PrintWriter p = new PrintWriter(
                new OutputStreamWriter( os,HttpdDef.DEF_CHARSET ) ) ;
            printWriter = p ; 
        }
        return printWriter ;
    }
    
    /**
     * OutputStreamが展開されているかチェック.
     * <BR><BR>
     * OutputStreamが展開されているかチェックします.
     * <BR>
     * @return boolean [true]の場合、展開されています.
     */
    public boolean isOutputStream() {
        return ( outputStream != null ) ? true : false ;
    }
    
    /**
     * Body情報を取得.
     * <BR>
     * @return byte[] Body情報が返されます.
     */
    public byte[] getBody() {
        byte[] ret = outputBinary ;
        outputBinary = null ;
        return ret ;
    }
    
    /**
     * destroyメソッドで、OutputStreamをオープンしていない場合.
     */
    private boolean destroyByNoOutputStream() {
        if( isOutputStream() == false ) {
            try {
                this.getOutput() ;
                // コンテンツ長を0バイトに設定.
                header.setHeader( HttpdDef.VALUE_CONTENT_LENGTH,"0" ) ;
            } catch( Exception e ) {
            }
            return true ;
        }
        return false ;
    }
    
    /**
     * Proxyサーバにデータ送信.
     */
    private void sendProxyServer( Object[] o )
        throws Exception {
        byte[] body = ( byte[] )o[ 0 ] ;
        o[ 0 ] = null ;
        byte[] headerBin = createHeaderBinary() ;
        headerBin = GZIPBinary.getInstance().convertBinaryByGZIP( headerBin ) ;
        int len = headerBin.length + 12 ;
        if( body != null ) {
            len += body.length ;
        }
        OutputStream outputStream = connectionInfo.getOutputStream() ;
        outputStream.write( ConvertParam.convertInt( len ) ) ;// 全体長.
        outputStream.write( ConvertParam.convertInt( headerBin.length ) ) ;// ヘッダ長.
        outputStream.write( headerBin ) ;// ヘッダ.
        headerBin = null ;
        if( body != null ) {
            outputStream.write( ConvertParam.convertInt( body.length ) ) ;// body長.
            outputStream.write( body ) ;// body
        }
        else {
            outputStream.write( ConvertParam.convertInt( 0 ) ) ;// body長(なし).
        }
        body = null ;
        outputStream.flush() ;
    }
    
    /**
     * Headerバイナリを生成.
     */
    private byte[] createHeaderBinary()
        throws Exception {
        String[] keys = header.getKeys() ;
        int headerLen = keys.length ;
        ArrayBinary ret = new ArrayBinary() ;
        putValue( ret,version ) ;// HTTPバージョン.
        putValue( ret,String.valueOf( state ) ) ;// ステータス.
        putValue( ret,HttpdErrorDef.convertErrorMessage( state ) ) ;// ステータス(メッセージ).
        // ヘッダ情報出力.
        ret.write( ConvertParam.convertInt( headerLen ) ) ;// ヘッダ長.
        for( int i = 0 ; i < headerLen ; i ++ ) {
            putValue( ret,keys[ i ] ) ;// キー名.
            int lenJ = header.size( keys[ i ] ) ;
            ret.write( ConvertParam.convertInt( lenJ ) ) ;// キーに対する要素数.
            for( int j = 0 ; j < lenJ ; j ++ ) {
                String val = header.getHeader( keys[ i ],j ) ;
                putValue( ret,val ) ;
            }
        }
        return ret.getBinary() ;
    }
    
    /**
     * １つの要素をセット.
     */
    private static final void putValue( ArrayBinary out,String value )
        throws Exception {
        if( value == null || ( value = value.trim() ).length() <= 0 ) {
            out.write( ConvertParam.convertInt( 0 ) ) ;
        }
        else {
            byte[] tmp = value.getBytes( "ISO-8859-1" ) ;
            out.write( ConvertParam.convertInt( tmp.length ) ) ;
            out.write( tmp ) ;
        }
    }
    
    /*
     * コンテンツタイプが設定されていない場合.
     */
    private static final void noSetByContentType( HttpdHeaders header,String url ) {
        url = FileUtil.getFileName( url ) ;
        if( url.indexOf( "." ) == -1 && url.endsWith( ScriptDef.SCRIPT_PLUS ) == true ) {
            header.setHeader( HttpdDef.VALUE_CONTENT_TYPE,HttpdDef.AJAX_MIME_TYPE ) ;
        }
        else if( url.endsWith( ScriptDef.SCRIPT_HTML_PLUS ) == true ) {
            header.setHeader( HttpdDef.VALUE_CONTENT_TYPE,HttpdDef.MHTML_MIME_TYPE ) ;
        }
        else {
            MimeConfig conf = ( MimeConfig )GlobalManager.getValue(
                ServiceDef.MANAGER_BY_MIME_TYPE ) ;
            String mime = conf.getNameByMimeType( url ) ;
            if( mime.indexOf( "xml" ) != -1 || mime.indexOf( "html" ) != -1 ) {
                header.setHeader( HttpdDef.VALUE_CONTENT_TYPE,mime+HttpdDef.MIME_CHARSET_UTF8 ) ;
            }
            else {
                header.setHeader( HttpdDef.VALUE_CONTENT_TYPE,mime ) ;
            }
        }
    }
}
