//
//  ========================================================================
//  Copyright (c) 1995-2015 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.websocket;

import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;


/**
 * WebSocketGenerator.
 * This class generates websocket packets.
 * It is fully synchronized because it is likely that async
 * threads will call the addMessage methods while other
 * threads are flushing the generator.
 *
 * modified by KNOWLEDGECODE
 */
public class WebSocketGeneratorRFC6455 implements WebSocketGenerator
{
    private final Lock _lock = new ReentrantLock();
    private final WebSocketBuffers _buffers;
    private final EndPoint _endp;
    private final byte[] _mask = new byte[4];
    private final MaskGen _maskGen;
    private Buffer _buffer;
    private int _m;
    private boolean _opsent;
    private boolean _closed;

    public WebSocketGeneratorRFC6455(WebSocketBuffers buffers, EndPoint endp)
    {
        this(buffers, endp, null);
    }

    public WebSocketGeneratorRFC6455(WebSocketBuffers buffers, EndPoint endp, MaskGen maskGen)
    {
        _buffers = buffers;
        _endp = endp;
        _maskGen = maskGen;
    }

    public Buffer getBuffer()
    {
        _lock.lock();
        try
        {
            return _buffer;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public void addFrame(byte flags, byte opcode, byte[] content, int offset, int length) throws IOException
    {
        _lock.lock();
        try
        {
            if (_closed)
                throw new EofException("Closed");
            if (opcode == WebSocketConnectionRFC6455.OP_CLOSE)
                _closed = true;

            boolean mask = _maskGen != null;

            if (_buffer == null)
                _buffer = mask ? _buffers.getBuffer() : _buffers.getDirectBuffer();

            boolean last = WebSocketConnectionRFC6455.isLastFrame(flags);

            int space = mask ? 14 : 10;

            do
            {
                if (_opsent)
                {
                    flags &= 0x0b;
                    opcode = WebSocketConnectionRFC6455.OP_CONTINUATION;
                }
                opcode = (byte)(((0xf & flags) << 4) + (0xf & opcode));
                _opsent = true;

                int payload = length;
                if (payload + space > _buffer.capacity())
                {
                    // We must fragement, so clear FIN bit
                    opcode = (byte)(opcode & 0x7F); // Clear the FIN bit
                    payload = _buffer.capacity() - space;
                }
                else if (last)
                    opcode = (byte)(opcode | 0x80); // Set the FIN bit

                // ensure there is space for header
                if (_buffer.space() <= space)
                {
                    flushBuffer();
                    if (_buffer.space() <= space)
                        flush();
                }

                // write the opcode and length
                if (payload > 0xffff)
                {
                    _buffer.put(new byte[]{
                            opcode,
                            mask ? (byte)0xff : (byte)0x7f,
                            (byte)0,
                            (byte)0,
                            (byte)0,
                            (byte)0,
                            (byte)((payload >> 24) & 0xff),
                            (byte)((payload >> 16) & 0xff),
                            (byte)((payload >> 8) & 0xff),
                            (byte)(payload & 0xff)});
                }
                else if (payload >= 0x7e)
                {
                    _buffer.put(new byte[]{
                            opcode,
                            mask ? (byte)0xfe : (byte)0x7e,
                            (byte)(payload >> 8),
                            (byte)(payload & 0xff)});
                }
                else
                {
                    _buffer.put(new byte[]{
                            opcode,
                            (byte)(mask ? (0x80 | payload) : payload)});
                }

                // write mask
                if (mask)
                {
                    _maskGen.genMask(_mask);
                    _m = 0;
                    _buffer.put(_mask);
                }

                // write payload
                int remaining = payload;
                while (remaining > 0)
                {
                    _buffer.compact();
                    int chunk = remaining < _buffer.space() ? remaining : _buffer.space();

                    if (mask)
                    {
                        for (int i = 0; i < chunk; i++)
                            _buffer.put((byte)(content[offset + (payload - remaining) + i] ^ _mask[+_m++ % 4]));
                    }
                    else
                        _buffer.put(content, offset + (payload - remaining), chunk);

                    remaining -= chunk;
                    if (_buffer.space() > 0)
                    {
                        // Gently flush the data, issuing a non-blocking write
                        flushBuffer();
                    }
                    else
                    {
                        // Forcibly flush the data, issuing a blocking write
                        flush();
                        if (remaining == 0)
                        {
                            // Gently flush the data, issuing a non-blocking write
                            flushBuffer();
                        }
                    }
                }
                offset += payload;
                length -= payload;
            }
            while (length > 0);
            _opsent = !last;

            if (_buffer != null && _buffer.length() == 0)
            {
                _buffers.returnBuffer(_buffer);
                _buffer = null;
            }
        }
        finally
        {
            _lock.unlock();
        }
    }

    public int flushBuffer() throws IOException
    {
        if (!_lock.tryLock())
            return 0;

        try
        {
            if (!_endp.isOpen())
                throw new EofException();

            if (_buffer != null)
            {
                int flushed = _buffer.hasContent() ? _endp.flush(_buffer) : 0;
                if (_closed && _buffer.length() == 0)
                    _endp.shutdownOutput();
                return flushed;
            }

            return 0;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public int flush() throws IOException
    {
        if (!_lock.tryLock())
            return 0;

        try
        {
            if (_buffer == null)
                return 0;

            int result = flushBuffer();
            if (!_endp.isBlocking())
            {
                long now = System.currentTimeMillis();
                long end = now + _endp.getMaxIdleTime();
                while (_buffer.length() > 0)
                {
                    boolean ready = _endp.blockWritable(end - now);
                    if (!ready)
                    {
                        now = System.currentTimeMillis();
                        if (now < end)
                            continue;
                        throw new IOException("Write timeout");
                    }

                    result += flushBuffer();
                }
            }
            _buffer.compact();
            return result;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public boolean isBufferEmpty()
    {
        _lock.lock();
        try
        {
            return _buffer == null || _buffer.length() == 0;
        }
        finally
        {
            _lock.unlock();
        }
    }

    public void returnBuffer()
    {
        _lock.lock();
        try
        {
            if (_buffer != null && _buffer.length() == 0)
            {
                _buffers.returnBuffer(_buffer);
                _buffer = null;
            }
        }
        finally
        {
            _lock.unlock();
        }
    }

    @Override
    public String toString()
    {
        // Do NOT use synchronized (this)
        // because it's very easy to deadlock when debugging is enabled.
        // We do a best effort to print the right toString() and that's it.
        Buffer buffer = _buffer;
        return String.format(Locale.getDefault(), "%s@%x closed=%b buffer=%d",
                getClass().getSimpleName(),
                hashCode(),
                _closed,
                buffer == null ? -1 : buffer.length());
    }
}
