// <copyright file="EasyScriptCompressedDecoder.cs" company="Scriptel Corporation">
//      Copyright 2015 - Scriptel Corporation
// </copyright>
namespace EasyScriptAPI
{
    using System;
    using System.Collections.Generic;
    using System.Text;

    /// <summary>
    /// This class is responsible for uncompressing and parsing compressed
    /// signature streams.
    /// </summary>
    public class EasyScriptCompressedDecoder: IEasyScriptDecoder
    {
        /// <summary>
        /// This is a buffer that will contain the characters required to unpack
        /// the next value from the stream.
        /// </summary>
        private StringBuilder decompressBuffer = new StringBuilder();
        /// <summary>
        /// The most recent decompressed value.
        /// </summary>
        private int? decompressedValue = 0;
        /// <summary>
        /// The current node in the huffman table.
        /// </summary>
        private BinaryTree currentNode = null;
        /// <summary>
        /// Whether or not we're decoding x (true) or y (false).
        /// </summary>
        private bool decodingX;
        /// <summary>
        /// The current sign for x (-1 or 1).
        /// </summary>
        private int xSign;
        /// <summary>
        /// The current sign for y (-1 or 1).
        /// </summary>
        private int ySign;
        /// <summary>
        /// The previous coordinate with which to generate the next coordinate.
        /// </summary>
        private Coordinate prevCoordinate;
        /// <summary>
        /// The new coordinate which superceeds the previous coordinate.
        /// </summary>
        private Coordinate newCoordinate;
        /// <summary>
        /// Scaled coordinate.
        /// </summary>
        private Coordinate absCoordinate;
        /// <summary>
        /// Whether or not a new stroke was detected.
        /// </summary>
        private bool newStrokeFound = false;
        /// <summary>
        /// Whether or not the next coordinate should have a new stroke sent before it.
        /// </summary>
        private bool newStroke = false;
        /// <summary>
        /// The signature protocol being used to decode the keyboard events.
        /// </summary>
        private ISignatureProtocol signatureProtocol;
        /// <summary>
        /// The list of listeners interested in events for this stream.
        /// </summary>
        private List<IEasyScriptEventListener> listeners;
        /// <summary>
        /// The current position in the stream.
        /// </summary>
        private int currentPosition;

        /// <summary>
        /// This enumeration contains special decoding codes used to unpack the
        /// compressed stream in special ways.
        /// </summary>
        private enum EasyScriptCompressCode
        {
            /// <summary>
            /// The current half of the previous coordinate should be copied
            /// to this coordinate.
            /// </summary>
            RepeatHalf = 59,
            /// <summary>
            /// This indiciates that a new stroke has begun.
            /// </summary>
            NewStrokeCode = 60,
            /// <summary>
            /// Changes the sign of X.
            /// </summary>
            ReverseXCode = 61,
            /// <summary>
            /// Changes the sign of Y.
            /// </summary>
            ReverseYCode = 62,
            /// <summary>
            /// Changes the sign of X and Y.
            /// </summary>
            ReverseXAndYCode = 63,
            /// <summary>
            /// This indicates the X value was intended to be 61.
            /// </summary>
            ReverseXCodeSwap = 3001,
            /// <summary>
            /// This indicates the Y value was intended to be 62.
            /// </summary>
            ReverseYCodeSwap = 3002,
            /// <summary>
            /// This indiciates that the x value was intended to be 63.
            /// </summary>
            ReverseXAndYCodeSwap = 3003,
            /// <summary>
            /// This indicates the current X or Y value was intended to be 59.
            /// </summary>
            RepeatHalfSwap = 3004,
            /// <summary>
            /// This indicates the X value was intended to be 60.
            /// </summary>
            NewStrokeCodeSwap = 3005,
            /// <summary>
            /// Indiciates the signature has been cancelled.
            /// </summary>
            CancelCode = 3007
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EasyScriptCompressedDecoder"/> class.
        /// Initializes the decompressor and gets ready to receive signature data.
        /// </summary>
        /// <param name="signatureProtocol">Signature protocol to use to decode keyboard keys.</param>
        /// <param name="currentPosition">Current position in the stream.</param>
        /// <param name="listeners">Listeners interested in signature events.</param>
        public EasyScriptCompressedDecoder(ISignatureProtocol signatureProtocol, int currentPosition, List<IEasyScriptEventListener> listeners)
        {
            newStroke = true;
            decompressBuffer.Length = 0;
            currentNode = BinaryTreeReader.GetCompressionTree();
            decompressedValue = 0;
            decodingX = true;
            xSign = 1;
            ySign = 1;
            prevCoordinate = new Coordinate(0, 0);
            newCoordinate = new Coordinate(0, 0);
            absCoordinate = new Coordinate(0, 0);
            newStrokeFound = false;

            this.signatureProtocol = signatureProtocol;
            this.listeners = listeners;
            this.currentPosition = currentPosition;
        }

        /// <summary>
        /// Parses a character at a time from a signature stream.
        /// </summary>
        /// <param name="chr">Next character to parse.</param>
        public void ParseSignature(char chr)
        {
            currentPosition++;
            if (newStroke && chr != signatureProtocol.GetEndStream())
            {
                foreach (IEasyScriptEventListener recv in listeners)
                {
                    recv.NewStroke();
                }
                newStroke = false;
            }
            
            if (chr == signatureProtocol.GetEndStream())
            {
                foreach (IEasyScriptEventListener recv in listeners)
                {
                    recv.EndOfSignature();
                }
            }
            else if (chr == signatureProtocol.GetCancelStream())
            {
                foreach (IEasyScriptEventListener recv in listeners)
                {
                    recv.Cancel();
                }
                newStroke = false;
            }
            else
            {
                DecompressStream(chr);
            }
        }

        /// <summary>
        /// Attempts to decompress the next character.
        /// </summary>
        /// <param name="c">Character to decompress.</param>
        private void DecompressStream(char c)
        {
            decompressBuffer.Append(c);

            if (decompressBuffer.Length < 2)
            {
                return;
            }

            int value = GetTableValue(decompressBuffer[0]);
            value += GetTableValue(decompressBuffer[1]);
            if (decompressBuffer.Length == 3)
            {
                value += GetTableValue(decompressBuffer[2]);
            }
            else if (value == 505)
            {
                return;
            }

            decompressBuffer.Length = 0;

            if (value > 511)
            {
                throw new SignatureInvalidException("Invalid compressed signature, value > 511: " + value, currentPosition);
            }

            Decompress(value);
        }

        /// <summary>
        /// Attempts to decompress a value read from the character stream.
        /// </summary>
        /// <param name="bits">Value of the looked up character in the huffman table.</param>
        private void Decompress(int bits)
        {
            for (int i = 256; i > 0; i >>= 1)
            {
                int b = bits & i;
                currentNode = (b == 0) ? currentNode.Left : currentNode.Right;

                if (currentNode.Value != null)
                {
                    if (currentNode.Value < 64)
                    {
                        DataDecode((int)(decompressedValue + currentNode.Value));
                        decompressedValue = 0;
                    }
                    else
                    {
                        decompressedValue = currentNode.Value;
                    }
                    this.currentNode = BinaryTreeReader.GetCompressionTree();
                }
            }
        }

        /// <summary>
        /// Attempts to determine the actual value of a decompressed value.
        /// </summary>
        /// <param name="value">Decompressed value to parse.</param>
        private void DataDecode(int value)
        {
            if (decodingX)
            {
                switch ((EasyScriptCompressCode)value)
                {
                    case EasyScriptCompressCode.NewStrokeCode:
                        newStrokeFound = true;
                        break;
                    case EasyScriptCompressCode.CancelCode:
                        foreach (IEasyScriptEventListener recv in listeners)
                        {
                            recv.Cancel();
                        }
                        break;
                    case EasyScriptCompressCode.ReverseXCode:
                        xSign = -xSign;
                        break;
                    case EasyScriptCompressCode.ReverseYCode:
                        ySign = -ySign;
                        break;
                    case EasyScriptCompressCode.ReverseXAndYCode:
                        xSign = -xSign;
                        ySign = -ySign;
                        break;
                    case EasyScriptCompressCode.RepeatHalf:
                        newCoordinate.X = prevCoordinate.X;
                        decodingX = false;
                        break;
                    case EasyScriptCompressCode.RepeatHalfSwap:
                        newCoordinate.X = (int)EasyScriptCompressCode.RepeatHalf;
                        decodingX = false;
                        break;
                    case EasyScriptCompressCode.ReverseXCodeSwap:
                        newCoordinate.X = (int)EasyScriptCompressCode.ReverseXCode;
                        decodingX = false;
                        break;
                    case EasyScriptCompressCode.ReverseYCodeSwap:
                        newCoordinate.X = (int)EasyScriptCompressCode.ReverseYCode;
                        decodingX = false;
                        break;
                    case EasyScriptCompressCode.ReverseXAndYCodeSwap:
                        newCoordinate.X = (int)EasyScriptCompressCode.ReverseXAndYCode;
                        decodingX = false;
                        break;
                    case EasyScriptCompressCode.NewStrokeCodeSwap:
                        newCoordinate.X = (int)EasyScriptCompressCode.NewStrokeCode;
                        decodingX = false;
                        break;
                    default:
                        newCoordinate.X = value;
                        decodingX = false;
                        break;
                }
            }
            else
            {
                prevCoordinate.X = newCoordinate.X;
                newCoordinate.X *= xSign;
                switch ((EasyScriptCompressCode)value)
                {
                    case EasyScriptCompressCode.RepeatHalf:
                        newCoordinate.Y = prevCoordinate.Y;
                        break;
                    case EasyScriptCompressCode.RepeatHalfSwap:
                        newCoordinate.Y = (int)EasyScriptCompressCode.RepeatHalf;
                        break;
                    default:
                        newCoordinate.Y = value;
                        prevCoordinate.Y = newCoordinate.Y;
                        break;
                }

                newCoordinate.Y *= ySign;
                if (newStrokeFound)
                {
                    foreach (IEasyScriptEventListener recv in listeners)
                    {
                        recv.NewStroke();
                    }
                    newStrokeFound = false;
                }

                Absolute(newCoordinate);
                newCoordinate.X = 0;
                newCoordinate.Y = 0;
                decodingX = true;
            }
        }

        /// <summary>
        /// Adjusts the coordinate to match the expected coordinate plane.
        /// </summary>
        /// <param name="coordinate">Coordinate to scale.</param>
        private void Absolute(Coordinate coordinate)
        {
            double max = 2999;
            double x, y;
            absCoordinate.X += coordinate.X;
            absCoordinate.Y += coordinate.Y;
            x = (absCoordinate.X / max) * signatureProtocol.GetWidth();
            y = (absCoordinate.Y / max) * signatureProtocol.GetHeight();

            Coordinate coord = new Coordinate(x, y);
            foreach (IEasyScriptEventListener recv in listeners)
            {
                recv.ReceiveCoordinate(coord);
            }
        }

        /// <summary>
        /// Attempts to turn a character into a value using the signature
        /// protocol.
        /// </summary>
        /// <param name="c">Character to get the value of.</param>
        /// <returns>Characters value in the signature protocol table.</returns>
        private int GetTableValue(char c)
        {
            byte[] xs = signatureProtocol.GetXValues()[0];
            byte[] ys = signatureProtocol.GetYValues()[0];
            for (int i = 0; i < xs.Length; i++)
            {
                if ((c == xs[i]) || (c == ys[i]))
                {
                    return i * 23;
                }
            }

            xs = signatureProtocol.GetXValues()[1];
            ys = signatureProtocol.GetYValues()[1];
            for (int i = 0; i < xs.Length; i++)
            {
                if ((c == xs[i]) || (c == ys[i]))
                {
                    return i;
                }
            }
            throw new SignatureInvalidException("Invalid keyboard code detected in compression stream C=" + c + " (" + ((int)c) + ")", currentPosition);
        }

    }
}
