/* Copyright (c) Citrix Systems, Inc. 
 * All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met: 
 * 
 * *   Redistributions of source code must retain the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer. 
 * *   Redistributions in binary form must reproduce the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer in the documentation and/or other 
 *     materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 * SUCH DAMAGE.
 */

using System;
using System.Threading;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using XenCenterLib;

namespace DotNetVnc
{
    public class VNCStream
    {
        private const int THUMBNAIL_SLEEP_TIME = 500;

        private const int RAW_ENCODING = 0;
        private const int COPY_RECTANGLE_ENCODING = 1;
        private const int RRE_ENCODING = 2;
        private const int CORRE_ENCODING = 4;
        private const int HEXTILE_ENCODING = 5;
        private const int CURSOR_PSEUDO_ENCODING = -239;
        private const int DESKTOP_SIZE_PSEUDO_ENCODING = -223;
        private const int XENCENTER_ENCODING = -254;
        private const int QEMU_EXT_KEY_ENCODING = -258;

        private const int SET_PIXEL_FORMAT = 0;
        private const int SET_ENCODINGS = 2;
        private const int FRAMEBUFFER_UPDATE_REQUEST = 3;
        private const int KEY_EVENT = 4;
        private const int KEY_SCAN_EVENT = 254;
        private const int QEMU_MSG = 255;
        private const int POINTER_EVENT = 5;
        private const int CLIENT_CUT_TEXT = 6;

        private const int RAW_SUBENCODING = 1;
        private const int BACKGROUND_SPECIFIED_SUBENCODING = 2;
        private const int FOREGROUND_SPECIFIED_SUBENCODING = 4;
        private const int ANY_SUBRECTS_SUBENCODING = 8;
        private const int SUBRECTS_COLORED_SUBENCODING = 16;

        private const int FRAME_BUFFER_UPDATE = 0;
        private const int BELL = 2;
        private const int SERVER_CUT_TEXT = 3;

        private const int QEMU_EXT_KEY_EVENT = 0;

        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private Thread thread = null;

        /**
         * Current color properties
         */
        private int bitsPerPixel;
        private int bytesPerPixel;
        private int depth;
        private bool bigEndian;
        private bool trueColor;
        private int redMax;
        private int greenMax;
        private int blueMax;
        private int redMaxPlus1;
        private int greenMaxPlus1;
        private int blueMaxPlus1;
        private int redMaxOver2;
        private int greenMaxOver2;
        private int blueMaxOver2;
        private int redShift;
        private int greenShift;
        private int blueShift;
        private bool rgb565;

        /// <summary>
        /// This event will be fired when an error occurs.  The helper thread is guaranteed to be
        /// closing down at this point.
        /// </summary>
        public event Action<object,Exception> ErrorOccurred = null;

        public event EventHandler ConnectionSuccess = null;

        /**
         * The encodings used.  Note that these are ordered: preferred encoding first.
         */
        private static readonly int[] encodings = new int[] {
            //HEXTILE_ENCODING,
            CORRE_ENCODING,
            RRE_ENCODING,
	        COPY_RECTANGLE_ENCODING,
	        RAW_ENCODING,
	        CURSOR_PSEUDO_ENCODING,
	        DESKTOP_SIZE_PSEUDO_ENCODING,
            XENCENTER_ENCODING,
            QEMU_EXT_KEY_ENCODING
	    };

        private readonly IVNCGraphicsClient client;

        private readonly MyStream stream;

        private readonly Object writeLock = new Object();
        private Object pauseMonitor = new Object();

        private volatile bool running = true;

        private int width;
        private int height;

        private bool incremental;
        private bool qemu_ext_key_encoding = false;

        private PixelFormat pixelFormat;
        private PixelFormat pixelFormatCursor;

        private byte[] data = new byte[1228800]; //640*480*32bpp
        private byte[] data_8bpp = null;

        private readonly long imageUpdateThreshold;

        /*
        public struct StatsEntry
        {
            public double time;
            public int size;
        }
        */

        public VNCStream(IVNCGraphicsClient client, Stream stream, bool startPaused)
        {
            this.client = client;
            this.stream = new MyStream(stream);
            this.paused = startPaused;

            long freq;
            if (!Win32.QueryPerformanceFrequency(out freq))
            {
                System.Diagnostics.Trace.Assert(false);
            }
            imageUpdateThreshold = freq / 3;
        }

        public void connect(char[] password)
        {
            System.Diagnostics.Trace.Assert(thread == null);

            thread = new Thread(this.run);
            thread.Name = String.Format("VNC connection to {0} - {1}", client.VmName, client.UUID);
            thread.IsBackground = true;
            thread.Start(password);
        }

        private ProtocolVersion getProtocolVersion()
        {
            byte[] buffer = new byte[12];
            this.stream.readFully(buffer, 0, 12);
            char[] chars = new char[12];
            Encoding.ASCII.GetDecoder().GetChars(buffer, 0, 12, chars, 0);
            String s = new String(chars);
            Regex regex = new Regex("RFB ([0-9]{3})\\.([0-9]{3})\n");
            Match match = regex.Match(s);
            if (!match.Success)
            {
                throw new VNCException("expected protocol version: " + s);
            }

            return new ProtocolVersion(Int32.Parse(match.Groups[1].Value),
                                       Int32.Parse(match.Groups[2].Value));
        }

        private void sendProtocolVersion()
        {
            lock (this.writeLock)
            {
                byte[] bytes = Encoding.ASCII.GetBytes("RFB 003.003\n");
                this.stream.Write(bytes, 0, bytes.Length);
                this.stream.Flush();
            }
        }

        private void readPixelFormat()
        {
            this.bitsPerPixel = this.stream.readCard8();
            this.depth = this.stream.readCard8();
            this.bigEndian = this.stream.readFlag();
            this.trueColor = this.stream.readFlag();
            this.redMax = this.stream.readCard16();
            this.greenMax = this.stream.readCard16();
            this.blueMax = this.stream.readCard16();
            this.redShift = this.stream.readCard8();
            this.greenShift = this.stream.readCard8();
            this.blueShift = this.stream.readCard8();
            this.stream.readPadding(3);
            Log.Debug("readPixelFormat " + this.bitsPerPixel +
                         " " + this.depth);
        }

        private void writePixelFormat()
        {
            Log.Debug("writePixelFormat " + this.bitsPerPixel +
                         " " + this.depth);
            this.stream.writeInt8(SET_PIXEL_FORMAT);
            this.stream.writePadding(3);

            this.stream.writeInt8(this.bitsPerPixel);
            this.stream.writeInt8(this.depth);
            this.stream.writeFlag(this.bigEndian);
            this.stream.writeFlag(this.trueColor);
            this.stream.writeInt16(this.redMax);
            this.stream.writeInt16(this.greenMax);
            this.stream.writeInt16(this.blueMax);
            this.stream.writeInt8(this.redShift);
            this.stream.writeInt8(this.greenShift);
            this.stream.writeInt8(this.blueShift);

            this.stream.writePadding(3);
        }

        private void force32bpp()
        {
            Log.Debug("force32bpp()");

            this.bitsPerPixel = 32;
            this.depth = 24;
            this.trueColor = true;
            this.redMax = 255;
            this.greenMax = 255;
            this.blueMax = 255;
            this.redShift = 16;
            this.greenShift = 8;
            this.blueShift = 0;

            // Note that we keep the endian value from the server.

            setupPixelFormat();

            lock (this.writeLock)
            {
                writePixelFormat();
            }
        }


        private void setupPixelFormat()
        {
            Log.Debug("setupPixelFormat(" + bitsPerPixel + ")");

            bytesPerPixel = bitsPerPixel >> 3;

            redMaxPlus1 = redMax + 1;
            greenMaxPlus1 = greenMax + 1;
            blueMaxPlus1 = blueMax + 1;

            redMaxOver2 = redMax >> 1;
            greenMaxOver2 = greenMax >> 1;
            blueMaxOver2 = blueMax >> 1;

            if (bitsPerPixel == 32 || bitsPerPixel == 8)
            {
                pixelFormat = PixelFormat.Format32bppRgb;
                pixelFormatCursor = PixelFormat.Format32bppArgb;
            }
            else if (bitsPerPixel == 16)
            {
                rgb565 = redShift == 11;
                pixelFormat =
                    rgb565 ?
                        PixelFormat.Format16bppRgb565 :
                        PixelFormat.Format16bppRgb555;
                pixelFormatCursor = PixelFormat.Format16bppArgb1555;
            }
            else
            {
                throw new IOException("unexpected bits per pixel: " + bitsPerPixel);
            }
        }

        private void writeSetEncodings()
        {
            Log.Debug("writeSetEncodings");
            this.stream.writeInt8(SET_ENCODINGS);
            this.stream.writePadding(1);
            this.stream.writeInt16(encodings.Length);
            for (int i = 0; i < encodings.Length; ++i)
            {
                this.stream.writeInt32(encodings[i]);
            }
        }

        private void writeFramebufferUpdateRequest(
            int x, int y, int width, int height, bool incremental
        )
        {
            this.stream.writeInt8(FRAMEBUFFER_UPDATE_REQUEST);
            this.stream.writeFlag(incremental);
            this.stream.writeInt16(x);
            this.stream.writeInt16(y);
            this.stream.writeInt16(width);
            this.stream.writeInt16(height);
        }

        private void handshake()
        {
            ProtocolVersion protocolVersion = getProtocolVersion();
            if (protocolVersion.major < 3)
            {
                throw new VNCException(
                    "don't know protocol version " + protocolVersion.major
                 );
            }
        }

        private void authenticationExchange(char[] password)
        {
            Log.Debug("authenticationExchange");

            int scheme = this.stream.readCard32();
            if (scheme == 0)
            {
                String reason = this.stream.readString();
                throw new VNCException("connection failed: " + reason);
            }
            else if (scheme == 1)
            {
                // no authentication needed
            }
            else if (scheme == 2)
            {
                PasswordAuthentication(password);
            }
            else
            {
                throw new VNCException(
                    "unexpected authentication scheme: " + scheme
                );
            }
        }

        private void PasswordAuthentication(char[] password)
        {
            byte[] keyBytes = new byte[8];
            for (int i = 0; (i < 8) && (i < password.Length); ++i)
            {
                keyBytes[i] = reverse((byte)password[i]);
            }

            DESCryptoServiceProvider des =
                new DESCryptoServiceProvider();

            des.Padding = PaddingMode.None;
            des.Mode = CipherMode.ECB;

            ICryptoTransform chiper =
                des.CreateEncryptor(keyBytes, null);

            byte[] challenge = new byte[16];
            this.stream.readFully(challenge, 0, 16);

            byte[] response = chiper.TransformFinalBlock(challenge, 0, 16);

            this.stream.Write(response, 0, 16);
            this.stream.Flush();

            int status = this.stream.readCard32();

            if (status == 0)
            {
                // ok
            }
            else if (status == 1 || status == 2)
            {
                throw new VNCAuthenticationException();
            }
            else
            {
                throw new VNCException("Bad Authentication Response");
            }
        }

        private static byte reverse(byte v)
        {
            byte r = 0;
            if ((v & 0x01) != 0) r |= 0x80;
            if ((v & 0x02) != 0) r |= 0x40;
            if ((v & 0x04) != 0) r |= 0x20;
            if ((v & 0x08) != 0) r |= 0x10;
            if ((v & 0x10) != 0) r |= 0x08;
            if ((v & 0x20) != 0) r |= 0x04;
            if ((v & 0x40) != 0) r |= 0x02;
            if ((v & 0x80) != 0) r |= 0x01;
            return r;
        }

        private void clientInitialization()
        {
            Log.Debug("clientInitialisation");
            lock (this.writeLock)
            {
                this.stream.writeFlag(true); // shared
                this.stream.Flush();
            }
        }

        private void serverInitialization()
        {
            Log.Debug("serverInitialisation");
            int width = this.stream.readCard16();
            int height = this.stream.readCard16();

            readPixelFormat();

            stream.readString(); /* The desktop name -- we don't care. */

            if (trueColor)
            {
                setupPixelFormat();
                lock (writeLock)
                {
                    writePixelFormat();
                }
            }
            else
            {
                force32bpp();
            }

            desktopSize(width, height);

            lock (this.writeLock)
            {
                writeSetEncodings();
            }
        }

        /**
         * Expects to be lock on writeLock.
         */
        private void writeKey(int command, bool down, int key)
        {
            this.stream.writeInt8(command); //Send Scancodes
            this.stream.writeFlag(down);
            this.stream.writePadding(2);
            this.stream.writeInt32(key);
        }

        private void writeQemuExtKey(int command, bool down, int key, int sym)
        {
            this.stream.writeInt8(command);
            this.stream.writeInt8(QEMU_EXT_KEY_EVENT);
            this.stream.writePadding(1);
            this.stream.writeFlag(down);
            this.stream.writeInt32(sym);
            this.stream.writeInt32(key);
        }

        /**
         * use_qemu_ext_key_encoding: Dictates if we want to use QEMU_EXT_KEY encoding.
         *
         * XS6.2 doesn't properly support QEMU_EXT_KEY and XS6.5 supports QEMU_EXT_KEY encoding
         * only if XS65ESP1051 is applied, so restrict QEMU_EXT_KEY encoding to Inverness and above.
         */
        public void keyScanEvent(bool down, int key, int sym, bool use_qemu_ext_key_encoding)
        {
            lock (this.writeLock)
            {
                try
                {
                    if (qemu_ext_key_encoding && use_qemu_ext_key_encoding)
                    {
                        writeQemuExtKey(QEMU_MSG, down, key, sym);
                    }
                    else
                    {
                        writeKey(KEY_SCAN_EVENT, down, key);
                    }
                    this.stream.Flush();
                }
                catch (IOException e)
                {
                    Log.Warn(e, e);
                }
            }
        }

        public void keyCodeEvent(bool down, int key)
        {
            lock (this.writeLock)
            {
                try
                {
                    writeKey(KEY_EVENT, down, key);
                    this.stream.Flush();
                }
                catch (IOException e)
                {
                    Log.Warn(e, e);
                }
            }
        }

        public void pointerEvent(int buttonMask, int x, int y)
        {
            if (x < 0)
            {
                x = 0;
            }
            else if (x >= width)
            {
                x = width - 1;
            }

            if (y < 0)
            {
                y = 0;
            }
            else if (y >= height)
            {
                y = height - 1;
            }

            lock (this.writeLock)
            {
                try
                {
                    pointerEvent_(buttonMask, x, y);
                    this.stream.Flush();
                }
                catch (IOException e)
                {
                    Log.Warn(e, e);
                }
            }
        }

        public void pointerWheelEvent(int x, int y, int r)
        {
            lock (this.writeLock)
            {
                try
                {
                    /*
                      The RFB protocol specifies a down-up pair for each
                      scroll of the wheel, on button 4 for scrolling up, and
                      button 5 for scrolling down.
                    */

                    int m;

                    if (r < 0)
                    {
                        r = -r;
                        m = 8;
                    }
                    else
                    {
                        m = 16;
                    }
                    for (int i = 0; i < r; i++)
                    {
                        pointerEvent_(m, x, y);
                        pointerEvent_(0, x, y);
                    }

                    this.stream.Flush();
                }
                catch (IOException e)
                {
                    Log.Warn(e, e);
                }
            }
        }


        private void pointerEvent_(int buttonMask, int x, int y)
        {
            this.stream.writeInt8(POINTER_EVENT);
            this.stream.writeInt8(buttonMask);
            this.stream.writeInt16(x);
            this.stream.writeInt16(y);
        }


        public void clientCutText(String text)
        {
            Log.Debug("cutEvent");

            lock (this.writeLock)
            {
                try
                {
                    this.stream.writeInt8(CLIENT_CUT_TEXT);
                    this.stream.writePadding(3);
                    this.stream.writeString(text);
                    this.stream.Flush();
                }
                catch (IOException e)
                {
                    Log.Warn(e, e);
                }
            }
        }

        /// <summary>
        /// Creates an image in place in this.data.  It expects the pixel data to already be in this.data.  
        /// </summary>
        /// <param name="start">the start of image data in this.data</param>
        /// <param name="length">the length of image data in this data</param>
        /// <param name="mask_length">length of cursor mask (after the image in this.data), as specified by
        /// the RFB protocol specification for the Cursor pseudo-encoding (1-bpp, packed). If 0,
        /// the mask is assumed to be totally opaque (as used by normal "raw" packets). Masks are not
        /// supported for 8-bpp images.</param>
        private void createImage(int width, int height, int x, int y, int start, int length, int mask_length, bool cursor)
        {
            if (width == 0 || height == 0)
                return;

            byte[] data_to_render;
            int stride;

            if (bitsPerPixel == 32)
            {
                stride = width * 4;
                data_to_render = data;

                System.Diagnostics.Debug.Assert(length == height * stride);

                if (cursor)
                {
                    // for mask
                    int j = 0; // bit within the current byte (k)
                    int k = start + length; //byte
                    int m = 0; // bit within the current row

                    for (int i = start; i < start + length; i += 4)
                    {
                        bool mask = (data[k] & (1 << (7 - j))) == 0;
                        data[i + 3] = (byte)(mask ? 0 : 0xff);

                        j++;
                        m++;

                        if (m == width)
                        {
                            j = 0;
                            m = 0;
                            k++;
                        }
                        else if (j > 7)
                        {
                            j = 0;
                            k++;
                        }
                    }
                }
            }
            else if (bitsPerPixel == 16)
            {
                // Bitmap requires that stride is a multiple of 4, so we 
                // will have to expand the data if width is odd.
                bool expand_data = width % 2 == 1;
                int stride_correction = expand_data ? 2 : 0;
                stride = width * 2 + stride_correction;
                data_to_render = expand_data ? new byte[stride * height] : data;

                System.Diagnostics.Debug.Assert(length == height * width * 2);

                if (cursor)
                {
                    int p = 0;      // Byte within the destination data_to_render.
                    // for mask
                    int j = 0; // bit within the current byte (k)
                    int k = start + length; //byte
                    int m = 0; // bit within the current row

                    for (int i = start; i < start + length; i += 2)
                    {
                        bool mask = (data[k] & (1 << (7 - j))) == 0;
                        byte mask_bit = (byte)(mask ? 0 : 0x80);

                        if (rgb565)
                        {
                            // Convert the 565 data into 1555.
                            data_to_render[p] = (byte)((data[i] & 0x1f) | ((data[i] & 0xe0) >> 1));
                            data_to_render[p + 1] = (byte)(((data[i + 1] & 0x7) >> 1) | (data[i + 1] & 0x78) | mask_bit);
                        }
                        else
                        {
                            // Add the mask bit -- everything else is OK because it's already 555.
                            data_to_render[p] = data[i];
                            data_to_render[p + 1] = (byte)(data[i + 1] | mask_bit);
                        }

                        j++;
                        m++;
                        p += 2;

                        if (m == width)
                        {
                            j = 0;
                            m = 0;
                            k++;
                            p += stride_correction;
                        }
                        else if (j > 7)
                        {
                            j = 0;
                            k++;
                        }
                    }
                }
                else if (expand_data)
                {
                    int w2 = width * 2;
                    int i = start;  // Byte within the source data.
                    int p = 0;      // Byte within the destination data_to_render.

                    for (int m = 0; m < height; m++)
                    {
                        Array.Copy(data, i, data_to_render, p, w2);
                        i += w2;
                        p += stride;
                    }
                }
            }
            else if (bitsPerPixel == 8)
            {
                stride = width * 4;
                data_to_render = data_8bpp;

                System.Diagnostics.Debug.Assert(length == width * height);

                // for mask
                int j = 0; // bit within the current byte (k)
                int k = start + length; //byte
                int m = 0; // bit within the current row

                for (int i = start, n = 0; i < start + length; i++, n += 4)
                {
                    data_8bpp[n + 2] = (byte)(((((data[i] >> redShift) & redMax) << 8) + redMaxOver2) / redMaxPlus1);
                    data_8bpp[n + 1] = (byte)(((((data[i] >> greenShift) & greenMax) << 8) + greenMaxOver2) / greenMaxPlus1);
                    data_8bpp[n] = (byte)(((((data[i] >> blueShift) & blueMax) << 8) + blueMaxOver2) / blueMaxPlus1);
                    if (cursor)
                    {
                        bool mask = (data[k] & (1 << (7 - j))) == 0;
                        data_8bpp[n + 3] = (byte)(mask ? 0 : 0xff);

                        j++;
                        m++;

                        if (m == width)
                        {
                            j = 0;
                            m = 0;
                            k++;
                        }
                        else if (j > 7)
                        {
                            j = 0;
                            k++;
                        }
                    }
                    else
                    {
                        data_8bpp[n + 3] = 0;
                    }
                }
            }
            else
            {
                throw new Exception("unexpected bits per pixel");
            }

            BitmapToClient(width, height, x, y, start, stride, cursor, data_to_render);
        }

        private void BitmapToClient(int width, int height, int x, int y, int start, int stride, bool cursor, byte [] img)
        {
            GCHandle handle = GCHandle.Alloc(img, GCHandleType.Pinned);
            try
            {
                IntPtr pointer = Marshal.UnsafeAddrOfPinnedArrayElement(img, start);
                using (Bitmap bitmap = new Bitmap(width, height, stride, cursor ? pixelFormatCursor : pixelFormat, pointer))
                {
                    if (cursor)
                    {
                        client.ClientSetCursor(bitmap, x, y, width, height);
                    }
                    else
                    {
                        client.ClientDrawImage(bitmap, x, y, width, height);
                    }
                }
            }
            catch (ArgumentException exn)
            {
                Log.Error(exn, exn);
            }
            finally
            {
                handle.Free();
            }
        }

        private void readRawEncoding(int x, int y, int width, int height)
        {
            readRawEncoding_(0, x, y, width, height, false);
        }

        /**
         * @param mask If true, read a mask after the raw data, as used by the
         * Cursor pseudo-encoding.
         * @param start The position in this.data to start using
         */
        private void readRawEncoding_(int start, int x, int y, int width, int height, bool cursor)
        {
            if (width < 0 || height < 0)
            {
                throw new VNCException("Invalid size: " + width + " x " +
                                       height);
            }

            int length = width * height * bytesPerPixel;
            if (data.Length < start + length)
                throw new VNCException("Server error: received rectangle bigger than desktop!");
            this.stream.readFully(this.data, start, length);

            int mask_length = 0;

            if (cursor)
            {
                // 1 bit mask.
                int scanline = (width + 7) >> 3;
                mask_length = scanline * height;
                System.Diagnostics.Trace.Assert(this.data.Length >= start + length + mask_length);
                this.stream.readFully(this.data, start + length, mask_length);
            }

            createImage(width, height, x, y, start, length, mask_length, cursor);
        }


        private void readCopyRectangleEncoding(int dx, int dy, int width, int height)
        {
            int x = this.stream.readCard16();
            int y = this.stream.readCard16();
            client.ClientCopyRectangle(x, y, width, height, dx, dy);
        }

        private Color readColor()
        {
            byte[] color = readColorBytes();
            return Color.FromArgb(color[2], color[1], color[0]);
        }

        private byte[] readColorBytes(byte[] color, int start)
        {
            uint pixel =
                bitsPerPixel == 32 ?
                    (uint)(color[start] |
                           color[start + 1] << 8 |
                           color[start + 2] << 16 |
                           color[start + 3] << 24) :
                bitsPerPixel == 16 ?
                    (uint)(color[start] |
                           color[start + 1] << 8) :
                    (uint)color[start];

            byte[] newColor = new byte[4];

            //ARGB Encoding
            newColor[3] = 0xFF;
            newColor[2] = (byte)(((((pixel >> redShift) & redMax) << 8) + redMaxOver2) / redMaxPlus1);
            newColor[1] = (byte)(((((pixel >> greenShift) & greenMax) << 8) + greenMaxOver2) / greenMaxPlus1);
            newColor[0] = (byte)(((((pixel >> blueShift) & blueMax) << 8) + blueMaxOver2) / blueMaxPlus1);

            return newColor;
        }

        private byte[] readColorBytes()
        {
            int n = bitsPerPixel >> 3;
            byte[] color = new byte[n];

            this.stream.readFully(color, 0, n);

            return readColorBytes(color, 0);
        }

        private void readRREEncoding(int x, int y, int width, int height)
        {
            int n = this.stream.readCard32();
            Color background = readColor();
            client.ClientFillRectangle(x, y, width, height, background);
            for (int i = 0; i < n; ++i)
            {
                Color foreground = readColor();
                int rx = this.stream.readCard16();
                int ry = this.stream.readCard16();
                int rw = this.stream.readCard16();
                int rh = this.stream.readCard16();
                client.ClientFillRectangle(x + rx, y + ry, rw, rh, foreground);
            }
        }

        private void readCoRREEncoding(int x, int y, int width, int height)
        {
            int n = this.stream.readCard32();
            Color background = readColor();
            client.ClientFillRectangle(x, y, width, height, background);
            for (int i = 0; i < n; ++i)
            {
                Color foreground = readColor();
                int rx = this.stream.readCard8();
                int ry = this.stream.readCard8();
                int rw = this.stream.readCard8();
                int rh = this.stream.readCard8();
                client.ClientFillRectangle(x + rx, y + ry, rw, rh, foreground);
            }
        }

        private void readFillRectangles(int rx, int ry, int n)
        {
            int pixelSize = (this.bitsPerPixel + 7) >> 3;
            int length = n * (pixelSize + 2);
            this.stream.readFully(data, 0, length);
            int index = 0;
            for (int i = 0; i < n; ++i)
            {
                Color foreground = readColor();
                int sxy = data[index++] & 0xff;
                int sx = sxy >> 4;
                int sy = sxy & 0xf;
                int swh = data[index++] & 0xff;
                int sw = (swh >> 4) + 1;
                int sh = (swh & 0xf) + 1;
                client.ClientFillRectangle(
                    rx + sx, ry + sy, sw, sh, foreground
                );
            }
        }

        private void readRectangles(int rx, int ry, int n, Color foreground)
        {
            for (int i = 0; i < n; ++i)
            {
                int sxy = this.stream.readCard8();
                int sx = sxy >> 4;
                int sy = sxy & 0xf;
                int swh = this.stream.readCard8();
                int sw = (swh >> 4) + 1;
                int sh = (swh & 0xf) + 1;
                client.ClientFillRectangle(
                    rx + sx, ry + sy, sw, sh, foreground
                );
            }
        }

        private void fillRectBytes(byte[] data, int stride, int x, int y, int width, int height, byte[] color)
        {
            // Fast path thin rectangles

            int p = (y * stride) + (x * 4);
            int skip;

            if (width == 1 && height == 1)
            {
                data[p + 0] = color[0];
                data[p + 1] = color[1];
                data[p + 2] = color[2];
                data[p + 3] = color[3];
            }
            else if (width == 1)
            {
                skip = stride - 4;

                for (int i = 0; i < height; i++)
                {
                    data[p + 0] = color[0];
                    data[p + 1] = color[1];
                    data[p + 2] = color[2];
                    data[p + 3] = color[3];

                    p += skip;
                }
            }
            else if (height == 1)
            {
                for (int i = 0; i < width; i++)
                {
                    data[p + 0] = color[0];
                    data[p + 1] = color[1];
                    data[p + 2] = color[2];
                    data[p + 3] = color[3];

                    p += 4;
                }
            }
            else
            {
                skip = stride - (width * 4);

                for (int j = 0; j < height; j++)
                {
                    for (int i = 0; i < width; i++)
                    {
                        data[p + 0] = color[0];
                        data[p + 1] = color[1];
                        data[p + 2] = color[2];
                        data[p + 3] = color[3];

                        p += 4;
                    }
                    p += skip;
                }
            }
        }

        private void readHextileEncoding(int x, int y, int width, int height)
        {
            /*
             * Basically, we have two ways of doing this.
             * 1. Draw the hextile to a byte buffer, convert to image
             *    and draw to client
             * 2. Draw the individual rectangles of the hex encoding
             *    directly to the client
             * 
             * I have found its faster to do 1) when size is > 64
             */
            //GraphicsUtils.startTime();
            if (width * height > 64)
            {
                byte[] background = { 0, 0, 0, 0xFF }; // Black
                byte[] foreground = { 0xFF, 0xFF, 0xFF, 0xFF }; // White

                byte[] buff = new byte[width * height * 4]; //assume 32 bpp
                int stride = width * 4;

                for (int sy = 0; sy < height; sy += 16)
                {
                    int sheight = Math.Min(16, height - sy);

                    for (int sx = 0; sx < width; sx += 16)
                    {
                        int swidth = Math.Min(16, width - sx);

                        int mask = this.stream.readCard8();

                        if ((mask & RAW_SUBENCODING) != 0)
                        {
                            int length = swidth * sheight * 4;
                            this.stream.readFully(this.data, 0, length);

                            int index = 0;
                            int skip = stride - (swidth * 4);
                            int p = (sy * stride) + (sx * 4);

                            for (int i = 0; i < sheight; i++)
                            {
                                for (int j = 0; j < swidth; j++)
                                {
                                    byte[] color = readColorBytes(this.data, index);

                                    index += 4; //assumed 32bpp here

                                    buff[p + 3] = color[3];
                                    buff[p + 2] = color[2];
                                    buff[p + 1] = color[1];
                                    buff[p + 0] = color[0];

                                    p += 4;
                                }

                                p += skip;
                            }
                        }
                        else
                        {
                            if ((mask & BACKGROUND_SPECIFIED_SUBENCODING) != 0)
                            {
                                background = readColorBytes();
                            }

                            fillRectBytes(buff, stride, sx, sy, swidth, sheight, background);

                            if ((mask & FOREGROUND_SPECIFIED_SUBENCODING) != 0)
                            {
                                foreground = readColorBytes();
                            }

                            if ((mask & ANY_SUBRECTS_SUBENCODING) != 0)
                            {
                                int n = this.stream.readCard8();
                                if ((mask & SUBRECTS_COLORED_SUBENCODING) != 0)
                                {
                                    int length = n * 6; //assume 32bpp
                                    this.stream.readFully(data, 0, length);
                                    int index = 0;
                                    for (int i = 0; i < n; ++i)
                                    {
                                        byte[] color = new byte[4];
                                        uint pixel = (uint)(data[index + 0] & 0xFF | data[index + 1] << 8
                                                     | data[index + 2] << 16 | data[index + 3] << 24);

                                        //ARGB Encoding
                                        color[3] = 0xFF;
                                        color[2] = (byte)((pixel >> this.redShift) & this.redMax);
                                        color[1] = (byte)((pixel >> this.greenShift) & this.greenMax);
                                        color[0] = (byte)((pixel >> this.blueShift) & this.blueMax);

                                        index += 4;
                                        int txy = data[index++] & 0xff;
                                        int tx = txy >> 4;
                                        int ty = txy & 0xf;
                                        int twh = data[index++] & 0xff;
                                        int tw = (twh >> 4) + 1;
                                        int th = (twh & 0xf) + 1;

                                        this.fillRectBytes(
                                            buff, stride, sx + tx, sy + ty, tw, th, color
                                        );
                                    }
                                }
                                else
                                {
                                    for (int i = 0; i < n; ++i)
                                    {
                                        int txy = this.stream.readCard8();
                                        int tx = txy >> 4;
                                        int ty = txy & 0xf;
                                        int twh = this.stream.readCard8();
                                        int tw = (twh >> 4) + 1;
                                        int th = (twh & 0xf) + 1;

                                        this.fillRectBytes(
                                            buff, stride, sx + tx, sy + ty, tw, th, foreground
                                        );
                                    }
                                }
                            }
                        }
                    }
                }

                // Now convert to image and write to screen
                GCHandle handle = GCHandle.Alloc(buff, GCHandleType.Pinned);
                try
                {
                    IntPtr pointer = Marshal.UnsafeAddrOfPinnedArrayElement(buff, 0);
                    using (Bitmap bitmap = new Bitmap(width, height, stride, PixelFormat.Format32bppArgb, pointer))
                    {
                        client.ClientDrawImage(bitmap, x, y, width, height);
                    }
                }
                finally
                {
                    handle.Free();
                }
            }
            else
            {
                Color foreground = Color.White;
                Color background = Color.Black;

                int xCount = (width + 15) >> 4;
                int yCount = (height + 15) >> 4;
                for (int yi = 0; yi < yCount; ++yi)
                {
                    int ry = y + (yi << 4);
                    int rh = (yi == (yCount - 1)) ? height & 0xf : 16;
                    if (rh == 0)
                    {
                        rh = 16;
                    }
                    for (int xi = 0; xi < xCount; ++xi)
                    {
                        int rx = x + (xi << 4);
                        int rw = (xi == (xCount - 1)) ? width & 0xf : 16;
                        if (rw == 0)
                        {
                            rw = 16;
                        }
                        int mask = this.stream.readCard8();
                        if ((mask & RAW_SUBENCODING) != 0)
                        {
                            readRawEncoding(rx, ry, rw, rh);
                        }
                        else
                        {
                            if ((mask & BACKGROUND_SPECIFIED_SUBENCODING) != 0)
                            {
                                background = readColor();
                            }
                            client.ClientFillRectangle(rx, ry, rw, rh, background);
                            if ((mask & FOREGROUND_SPECIFIED_SUBENCODING) != 0)
                            {
                                foreground = readColor();
                            }
                            if ((mask & ANY_SUBRECTS_SUBENCODING) != 0)
                            {
                                int n = this.stream.readCard8();
                                if ((mask & SUBRECTS_COLORED_SUBENCODING) != 0)
                                {
                                    readFillRectangles(rx, ry, n);
                                }
                                else
                                {
                                    readRectangles(rx, ry, n, foreground);
                                }
                            }
                        }
                    }
                }
            }
            /*double time = GraphicsUtils.endTime("hextile");
            int size = width * height;
            StatsEntry entry = new StatsEntry();
            entry.time = time;
            entry.size = size;

            this.client.stats.Add(entry);*/
        }

        private void readCursorPseudoEncoding(int x, int y, int width,
                                              int height)
        {
            readRawEncoding_(0, x, y, width, height, true);
        }

        private void readFrameBufferUpdate()
        {
            this.stream.readPadding(1);
            int n = this.stream.readCard16();
            Log.Debug("reading " + n + " rectangles");
            bool fb_updated = false;
            long start;
            long end;
            Win32.QueryPerformanceCounter(out start);
            for (int i = 0; i < n; ++i)
            {
                int x = this.stream.readCard16();
                int y = this.stream.readCard16();
                int width = this.stream.readCard16();
                int height = this.stream.readCard16();
                int encoding = this.stream.readCard32();
                Log.Debug("read " + x + " " + y + " " + width + " " +
                             height + " " + encoding);
                switch (encoding)
                {
                    case RAW_ENCODING:
                        readRawEncoding(x, y, width, height);
                        break;
                    case RRE_ENCODING:
                        readRREEncoding(x, y, width, height);
                        break;
                    case CORRE_ENCODING:
                        readCoRREEncoding(x, y, width, height);
                        break;
                    case COPY_RECTANGLE_ENCODING:
                        readCopyRectangleEncoding(x, y, width, height);
                        break;
                    case HEXTILE_ENCODING:
                        readHextileEncoding(x, y, width, height);
                        break;

                    case CURSOR_PSEUDO_ENCODING:
                        readCursorPseudoEncoding(x, y, width, height);
                        break;

                    case DESKTOP_SIZE_PSEUDO_ENCODING:
                        desktopSize(width, height);
                        // Since the desktop size has changed, we want a full buffer update next time
                        incremental = false;
                        break;

                    case QEMU_EXT_KEY_ENCODING:
                        qemu_ext_key_encoding = true;
                        break;

                    default:
                        throw new VNCException("unimplemented encoding: " + encoding);
                }

                Win32.QueryPerformanceCounter(out end);
                if (end - start > imageUpdateThreshold)
                {
                    client.ClientFrameBufferUpdate();
                    start = end;
                    fb_updated = true;
                }
                else
                {
                    fb_updated = false;
                }
            }

            if (!fb_updated)
                client.ClientFrameBufferUpdate();
        }


        private void desktopSize(int width, int height)
        {
            this.width = width;
            this.height = height;
            int neededBytes = width * height * bytesPerPixel;
            if (neededBytes > data.Length)
            {
                data = new byte[neededBytes];
            }
            if (bitsPerPixel == 8 && (data_8bpp == null || neededBytes * 4 > data_8bpp.Length))
            {
                data_8bpp = new byte[neededBytes * 4];
            }
            client.ClientDesktopSize(width, height);
        }

        private void readServerCutText()
        {
            this.stream.readPadding(3);
            String text = this.stream.readString();
            client.ClientCutText(text);
        }

        private void readServerMessage()
        {
            Log.Debug("readServerMessage");

            int type = this.stream.readCard8();

            switch (type)
            {
                case FRAME_BUFFER_UPDATE:
                    Log.Debug("Update");
                    //GraphicsUtils.startTime();
                    readFrameBufferUpdate();
                    //GraphicsUtils.endTime("readFrameBufferUpdate");
                    break;
                case BELL:
                    Log.Debug("Bell");
                    client.ClientBell();
                    break;
                case SERVER_CUT_TEXT:
                    Log.Debug("Cut text");
                    readServerCutText();
                    break;
                default:
                    throw new VNCException("unknown server message: " + type);
            }
        }

        public readonly Object updateMonitor = new Object(); 

        private void run(object o)
        {
            char[] password = (char[])o;

            try
            {
                handshake();
                sendProtocolVersion();
                authenticationExchange(password);
                clientInitialization();
                serverInitialization();

                if (ConnectionSuccess != null)
                    ConnectionSuccess(this, null);

                // Request a full framebuffer update the first time
                incremental = false;

                while (running)
                {
                    lock (this.writeLock)
                    {
                        writeFramebufferUpdateRequest(0, 0, width, height, incremental);
                        this.stream.Flush();
                    }

                    incremental = true;

                    readServerMessage();

                    lock (pauseMonitor)
                    {
                        lock(updateMonitor)
                            Monitor.PulseAll(updateMonitor);

                        if (paused)
                            Monitor.Wait(pauseMonitor);
                    }
                }
            }
            catch (Exception e)
            {
                if (running && this.ErrorOccurred != null)
                    ErrorOccurred(this, e);
            }
        }

        /// <summary>
        /// Nothrow guarantee.
        /// </summary>
        public void Close()
        {
            if (!running)
                return;

            running = false;
            try
            {
                stream.Close();
                lock (pauseMonitor)
                    Monitor.PulseAll(this.pauseMonitor);
                thread.Interrupt();
            }
            catch
            {
            }
        }

        private bool paused = true;

        public void Pause()
        {
            paused = true;
        }

        public void Unpause(bool fullupdate)
        {
            incremental = !fullupdate;
            paused = false;
            lock (this.pauseMonitor)
                Monitor.PulseAll(this.pauseMonitor);
        }

        public void Unpause()
        {
            Unpause(false);
        }
    }
}
