// ========================================================================
// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.server;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.eclipse.jetty.http.AbstractGenerator;
import org.eclipse.jetty.util.ByteArrayOutputStream2;
import org.eclipse.jetty.util.StringUtil;

/** OutputWriter.
 * A writer that can wrap a {@link HttpOutput} stream and provide
 * character encodings.
 *
 * The UTF-8 encoding is done by this class and no additional 
 * buffers or Writers are used.
 * The UTF-8 code was inspired by http://javolution.org
 */
public class HttpWriter extends Writer
{
    public static final int MAX_OUTPUT_CHARS = 512; 
    
    private static final int WRITE_CONV = 0;
    private static final int WRITE_ISO1 = 1;
    private static final int WRITE_UTF8 = 2;
    
    final HttpOutput _out;
    final AbstractGenerator _generator;
    int _writeMode;
    int _surrogate;

    /* ------------------------------------------------------------ */
    public HttpWriter(HttpOutput out)
    {
        _out=out;
        _generator=_out._generator;
        _surrogate=0; // AS lastUTF16CodePoint
    }

    /* ------------------------------------------------------------ */
    public void setCharacterEncoding(String encoding)
    {
        if (encoding == null || StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding))
        {
            _writeMode = WRITE_ISO1;
        }
        else if (StringUtil.__UTF8.equalsIgnoreCase(encoding))
        {
            _writeMode = WRITE_UTF8;
        }
        else
        {
            _writeMode = WRITE_CONV;
            if (_out._characterEncoding == null || !_out._characterEncoding.equalsIgnoreCase(encoding))
                _out._converter = null; // Set lazily in getConverter()
        }
        
        _out._characterEncoding = encoding;
        if (_out._bytes==null)
            _out._bytes = new ByteArrayOutputStream2(MAX_OUTPUT_CHARS);
    }

    /* ------------------------------------------------------------ */
    @Override
    public void close() throws IOException
    {
        _out.close();
    }

    /* ------------------------------------------------------------ */
    @Override
    public void flush() throws IOException
    {
        _out.flush();
    }

    /* ------------------------------------------------------------ */
    @Override
    public void write (String s,int offset, int length) throws IOException
    {   
        while (length > MAX_OUTPUT_CHARS)
        {
            write(s, offset, MAX_OUTPUT_CHARS);
            offset += MAX_OUTPUT_CHARS;
            length -= MAX_OUTPUT_CHARS;
        }

        if (_out._chars==null)
        {
            _out._chars = new char[MAX_OUTPUT_CHARS]; 
        }
        char[] chars = _out._chars;
        s.getChars(offset, offset + length, chars, 0);
        write(chars, 0, length);
    }

    /* ------------------------------------------------------------ */
    @Override
    public void write (char[] s,int offset, int length) throws IOException
    {              
        HttpOutput out = _out; 
        
        while (length > 0)
        {  
            out._bytes.reset();
            int chars = length>MAX_OUTPUT_CHARS?MAX_OUTPUT_CHARS:length;
            
            switch (_writeMode)
            {
                case WRITE_CONV:
                {
                    Writer converter=getConverter();
                    converter.write(s, offset, chars);
                    converter.flush();
                }
                break;

                case WRITE_ISO1:
                {
                    byte[] buffer=out._bytes.getBuf();
                    int bytes=out._bytes.getCount();
                    
                    if (chars>buffer.length-bytes)
                        chars=buffer.length-bytes;

                    for (int i = 0; i < chars; i++)
                    {
                        int c = s[offset+i];
                        buffer[bytes++]=(byte)(c<256?c:'?'); // ISO-1 and UTF-8 match for 0 - 255
                    }
                    if (bytes>=0)
                        out._bytes.setCount(bytes);

                    break;
                }

                case WRITE_UTF8:
                {
                    byte[] buffer=out._bytes.getBuf();
                    int bytes=out._bytes.getCount();

                    if (bytes+chars>buffer.length)
                        chars=buffer.length-bytes;

                    for (int i = 0; i < chars; i++)
                    {
                        int code = s[offset+i];

                        // Do we already have a surrogate?
                        if(_surrogate==0)
                        {
                            // No - is this char code a surrogate?
                            if(Character.isHighSurrogate((char)code))
                            {
                                _surrogate=code; // UCS-?
                                continue;
                            }                            
                        }
                        // else handle a low surrogate
                        else if(Character.isLowSurrogate((char)code))
                        {
                            code = Character.toCodePoint((char)_surrogate, (char)code); // UCS-4
                        }
                        // else UCS-2
                        else
                        {
                            code=_surrogate; // UCS-2
                            _surrogate=0; // USED
                            i--;
                        }

                        if ((code & 0xffffff80) == 0) 
                        {
                            // 1b
                            if (bytes>=buffer.length)
                            {
                                chars=i;
                                break;
                            }
                            buffer[bytes++]=(byte)(code);
                        }
                        else
                        {
                            if((code&0xfffff800)==0)
                            {
                                // 2b
                                if (bytes+2>buffer.length)
                                {
                                    chars=i;
                                    break;
                                }
                                buffer[bytes++]=(byte)(0xc0|(code>>6));
                                buffer[bytes++]=(byte)(0x80|(code&0x3f));
                            }
                            else if((code&0xffff0000)==0)
                            {
                                // 3b
                                if (bytes+3>buffer.length)
                                {
                                    chars=i;
                                    break;
                                }
                                buffer[bytes++]=(byte)(0xe0|(code>>12));
                                buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f));
                                buffer[bytes++]=(byte)(0x80|(code&0x3f));
                            }
                            else if((code&0xff200000)==0)
                            {
                                // 4b
                                if (bytes+4>buffer.length)
                                {
                                    chars=i;
                                    break;
                                }
                                buffer[bytes++]=(byte)(0xf0|(code>>18));
                                buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f));
                                buffer[bytes++]=(byte)(0x80|(code&0x3f));
                            }
                            else if((code&0xf4000000)==0)
                            {
                                // 5b
                                if (bytes+5>buffer.length)
                                {
                                    chars=i;
                                    break;
                                }
                                buffer[bytes++]=(byte)(0xf8|(code>>24));
                                buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f));
                                buffer[bytes++]=(byte)(0x80|(code&0x3f));
                            }
                            else if((code&0x80000000)==0)
                            {
                                // 6b
                                if (bytes+6>buffer.length)
                                {
                                    chars=i;
                                    break;
                                }
                                buffer[bytes++]=(byte)(0xfc|(code>>30));
                                buffer[bytes++]=(byte)(0x80|((code>>24)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f));
                                buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f));
                                buffer[bytes++]=(byte)(0x80|(code&0x3f));
                            }
                            else
                            {
                                buffer[bytes++]=(byte)('?');
                            } 

                            _surrogate=0; // USED

                            if (bytes==buffer.length)
                            {
                                chars=i+1;
                                break;
                            }
                        }
                    }
                    out._bytes.setCount(bytes);
                    break;
                }
                default:
                    throw new IllegalStateException();
            }
            
            out._bytes.writeTo(out);
            length-=chars;
            offset+=chars;
        }
    }
    
    
    /* ------------------------------------------------------------ */
    private Writer getConverter() throws IOException
    {
        if (_out._converter == null)
            _out._converter = new OutputStreamWriter(_out._bytes, _out._characterEncoding);
        return _out._converter;
    }   
}
