// <copyright file="EasyScript.cs" company="Scriptel Corporation">
//      Copyright 2014 - Scriptel Corporation
// </copyright>
namespace EasyScriptAPI
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;

    /// <summary>
    /// This is a utility class that can be used to interact with Scriptel ScripTouch
    /// EasyScript products. It is capable of parsing and rendering keyboard
    /// strings generated when the digitizer is signed.
    /// </summary>
    public class EasyScript
    {
        /// <summary>
        /// This contains the current version of the library.
        /// </summary>
        public static readonly string LibraryVersion = "3.5.29";

        /// <summary>
        /// This contains the date on which the library was built (YYYY-MM-DD HH:MM:SS).
        /// </summary>
        public static readonly string LibraryBuildDate = "2022-08-30 17:12:23-0400";

        /// <summary>
        /// The protocol by which to decode incoming signature strings.
        /// </summary>
        private ISignatureProtocol signatureProtocol;

        /// <summary>
        /// This is the protocol by which we'll decode incoming card swipe strings.
        /// </summary>
        private ICardSwipeProtocol cardProtocol;

        /// <summary>
        /// The current state of the state machine.
        /// </summary>
        private EasyScriptState currentState = EasyScriptState.Unknown;

        /// <summary>
        /// The current position in the incoming string.
        /// </summary>
        private int currentPosition = 0;

        /// <summary>
        /// A sliding window containing the characters needed to progress to the
        /// next state. Used by the parsing state machine.
        /// </summary>
        private StringBuilder currentString = new StringBuilder();

        /// <summary>
        /// The current metadata read from the state machine.
        /// </summary>
        private SignatureMetaData currentMetaData = null;

        /// <summary>
        /// The current decoder being used to parse the signature as defined
        /// by the current protocol being read out of the state machine.
        /// </summary>
        private IEasyScriptDecoder decoder = null;

        /// <summary>
        /// The set of listeners interested in signature events.
        /// </summary>
        private List<IEasyScriptEventListener> listeners = new List<IEasyScriptEventListener>();

        /// <summary>
        /// The current state of the card swipe state machine.
        /// </summary>
        private EasyScriptCardState cardState = EasyScriptCardState.Unknown;

        /// <summary>
        /// The current card swipe buffer.
        /// </summary>
        private StringBuilder cardBuffer = new StringBuilder();

        /// <summary>
        /// This enumeration contains a set of valid states for the signature
        /// parsing state machine.
        /// </summary>
        private enum EasyScriptState
        {
            /// <summary>
            /// The beginning of card character was detected and we're looking
            /// for a card sentinel.
            /// </summary>
            CardSentinel,
            /// <summary>
            /// We've validated that we're receiving card data and we're aggregating
            /// it for when we see the end of card character.
            /// </summary>
            CardData,
            /// <summary>
            /// The beginning of signature character was detected and we're looking
            /// for a signature sentinel.
            /// </summary>
            SignatureSentinel,
            /// <summary>
            /// We've validated that we're receiving signature data and we're
            /// waiting on the protocol version.
            /// </summary>
            ProtocolVersion,
            /// <summary>
            /// We've gotten the protocol version for the signature metadata and
            /// now we're waiting on the model number.
            /// </summary>
            Model,
            /// <summary>
            /// We've gotten the model number for the signature metadata and
            /// now we're waiting on the firmware version.
            /// </summary>
            FirmwareVersion,
            /// <summary>
            /// We've gotten the entire metadata associated with the signature
            /// and now based on the protocol we're expecting a compressed
            /// data stream to pass to the decoder.
            /// </summary>
            CompressedStrokes,
            /// <summary>
            /// We've gotten the entire metadata associated with the signature
            /// and now based on the protocol we're expecting an uncompressed
            /// data stream to pass to the decoder.
            /// </summary>
            UncompressedStrokes,
            /// <summary>
            /// State is presently unknown / undefined.
            /// </summary>
            Unknown,
            /// <summary>
            /// A card swipe is interrupting an uncompressed stream.
            /// </summary>
            CardInterruptingUncompressed,
            /// <summary>
            /// A card swipe is interrupting an uncompressed stream.
            /// </summary>
            CardInterruptingCompressed
        }

        /// <summary>
        /// This enumeration contains states valid for card swipes.
        /// </summary>
        private enum EasyScriptCardState
        {
            /// <summary>
            /// Card data is in the middle of being captured.
            /// </summary>
            CardData,
            /// <summary>
            /// The card has been entirely processed.
            /// </summary>
            CardProcessed,
            /// <summary>
            /// This is not a card.
            /// </summary>
            NotACard,
            /// <summary>
            /// Unknown state.
            /// </summary>
            Unknown
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EasyScript"/> class. Default constructor, uses the STN protocol.
        /// </summary>
        public EasyScript() : this(new STNSignatureProtocol(), new STNCardSwipeProtocol())
        {
        }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="EasyScript"/> class. Pass in the protocol
        /// of the device that you're using.
        /// </summary>
        /// <param name="signatureProtocol">Protocol for the device you're trying to decode signatures for</param>
        /// <param name="cardProtocol">Protocol for the device you're trying to decode card swipes for</param>
        public EasyScript(ISignatureProtocol signatureProtocol, ICardSwipeProtocol cardProtocol)
        {
            this.signatureProtocol = signatureProtocol;
            this.cardProtocol = cardProtocol;
            listeners.Add(new EndOfStreamListener(this));
        }
    
        /// <summary>
        /// This method will take a signature and will attempt to render it onto an Image object.
        /// </summary>
        /// <param name="s">Signature to render</param>
        /// <param name="foreColor">Foreground color (signature color) to use.</param>
        /// <param name="backColor">Background color</param>
        /// <param name="lineThickness">The thickness of each stroke in pixels (not scaled)</param>
        /// <param name="scale">How much to scale the resulting image.</param>
        /// <returns>Image containing the rendered signature</returns>
        public Image RenderSignature(Signature s, Color foreColor, Color backColor, int lineThickness, double scale)
        {
            int width = (int)(this.GetSignatureProtocol().GetWidth() * scale);
            int height = (int)(this.GetSignatureProtocol().GetHeight() * scale);
        
            Bitmap image = new Bitmap(width, height);

            for (int w = 0; w < image.Width; ++w)
            {
                for (int h = 0; h < image.Height; ++h)
                {
                    image.SetPixel(w, h, backColor);
                }
            }

            Graphics g = Graphics.FromImage(image);
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            GraphicsUnit gu = GraphicsUnit.Pixel;
            g.FillRectangle(new SolidBrush(backColor), image.GetBounds(ref gu));
            Pen pen = new Pen(foreColor, lineThickness);
            pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;

            List<List<Coordinate>> strokes = s.GetStrokes();
            for (int stroke = 0; stroke < strokes.Count; ++stroke)
            {
                List<Coordinate> coordList = strokes[stroke];
                for (int c = 0; c < coordList.Count - 1; ++c)
                {
                    Coordinate coord = (Coordinate)coordList[c];
                    Coordinate next = (Coordinate)coordList[c + 1];
                    g.DrawLine(
                        pen,
                        new Point((int)(coord.X * scale), (int)(coord.Y * scale)),
                        new Point((int)(next.X * scale), (int)(next.Y * scale)));
                }
            }
        
            return image;
        }

        /// <summary>
        /// This method attempts to parse a magnetic card swipe from a ScripTouch
        /// device with a magnetic strip reader.
        /// </summary>
        /// <param name="swipe">ScripTouch magnetic strip data.</param>
        /// <returns>Parsed card swipe</returns>
        /// <exception cref="CardSwipeInvalidException">Thrown in the event the card couldn't be properly parsed</exception>
        public CardSwipe ParseCardSwipe(string swipe) 
        {
            if (swipe == null || swipe.Length <= 0)
            {
                throw new CardSwipeInvalidException("Card swipe stream is empty.", 0);
            }
            else if (swipe[0] != this.GetCardProtocol().GetStartStream())
            {
                throw new CardSwipeInvalidException(
                    "Card swipe stream doesn't start with the correct character (" +
                    swipe[0] + " != " + this.GetCardProtocol().GetStartStream() + ")",
                    0);
            }
            else if (swipe[swipe.Length - 1] != this.GetCardProtocol().GetEndStream())
            {
                throw new CardSwipeInvalidException(
                    "Card swipe stream doesn't end with the correct character (" +
                    swipe[swipe.Length - 1] + " != " + this.GetCardProtocol().GetEndStream() + ")",
                    0);
            }
            else if (!swipe.Substring(1, this.GetCardProtocol().GetSentinel().Length).Equals(this.GetCardProtocol().GetSentinel(), StringComparison.CurrentCultureIgnoreCase))
            {
                throw new CardSwipeInvalidException("Card swipe stream doesn't start with the correct sentinel.", 1);
            }
        
            CardSwipe result = new CardSwipe();
            result.SetProtocolVersion(swipe.Substring(8, 1));
            result.SetCardData(swipe.Substring(10, swipe.Length - 10));
        
            result.SetFinancialCard(FinancialCard.Parse(result.GetCardData()));
            result.SetIdentificationCard(IdentificationCard.Parse(result.GetCardData()));
        
            return result;
        }

        /// <summary>
        /// This method takes a EasyScript string and attempts to parse it into
        /// a signature object.
        /// </summary>
        /// <param name="sig">Signature string to parse</param>
        /// <returns>Parsed signature, or null if the signature is cancelled.</returns>
        /// <exception cref="SignatureInvalidException">Thrown in the event there is something wrong with the string</exception>
        public Signature ParseSignature(string sig) 
        {
            if (sig == null)
            {
                throw new SignatureInvalidException("Signature was null", 0);
            }
            if (sig.Length == 0)
            {
                throw new SignatureInvalidException("Signature is empty", 0);
            }
            char[] sigCharArray = sig.ToCharArray();
            if (sigCharArray[0] != signatureProtocol.GetStartStream())
            {
                throw new SignatureInvalidException("Signature does not start with the start sentinal", 0);
            }

            BlockingEasyScriptListener listener = new BlockingEasyScriptListener();
            this.AddListener(listener);

            for (int i = 0; i < sig.Length; i++)
            {
                Parse(sig[i]);
            }

            this.RemoveListener(listener);

            if (!listener.IsDone())
            {
                throw new SignatureInvalidException("No end of signature sentinal found", 0);
            } 
            else if (listener.IsCanceled())
            {
                //Signature was cancelled.
                return null;
            }

            return listener.GetSignature();
        }

        /// <summary>
        /// This method allows streaming of individual characters to produce
        /// streaming signatures using callbacks registered with addListener().
        /// When events are detected in the signature stream events will be sent
        /// to the event listeners.
        /// </summary>
        /// <param name="chr">Next character to parse.</param>
        public void Parse(char chr)
        {
            currentPosition++;

            switch (currentState)
            {
                case EasyScriptState.Unknown:
                    if (chr == signatureProtocol.GetStartStream())
                    {
                        currentState = EasyScriptState.SignatureSentinel;
                        currentPosition = 1;
                        currentString.Length = 0;
                        return;
                    }
                    else if (chr == cardProtocol.GetStartStream())
                    {
                        currentState = EasyScriptState.CardSentinel;
                        currentPosition = 1;
                        cardBuffer.Length = 0;
                        cardBuffer.Append(chr);
                        cardState = EasyScriptCardState.Unknown;
                        return;
                    }
                    break;
                case EasyScriptState.CardSentinel:
                    EasyScriptCardState state = ParseCard(chr);
                    if (state == EasyScriptCardState.CardProcessed || state == EasyScriptCardState.NotACard)
                    {
                        cardState = EasyScriptCardState.Unknown;
                        currentState = EasyScriptState.Unknown;
                    }
                    break;
                case EasyScriptState.CardData:
                    currentString.Append(chr);
                    if (chr == cardProtocol.GetEndStream())
                    {
                        CardSwipe swipe = ParseCardSwipe(currentString.ToString());
                        foreach (IEasyScriptEventListener recv in listeners)
                        {
                            recv.CardSwipe(swipe);
                        }
                        this.currentState = EasyScriptState.Unknown;
                    }
                    break;
                case EasyScriptState.SignatureSentinel:
                    if (chr == signatureProtocol.GetPenUp())
                    {
                        if (currentString.ToString() == signatureProtocol.GetSentinel())
                        {
                            currentState = EasyScriptState.ProtocolVersion;
                            currentMetaData = new SignatureMetaData();
                        }
                        else
                        {
                            currentState = EasyScriptState.Unknown;
                            throw new SignatureInvalidException("Signature sentinel doesn't appear to be correct: " + currentString, currentPosition);
                        }
                    }
                    else
                    {
                        currentString.Append(chr);
                    }
                    break;
                case EasyScriptState.ProtocolVersion:
                    if (chr != signatureProtocol.GetPenUp())
                    {
                        currentMetaData.ProtocolVersion += chr;
                    }
                    else
                    {
                        currentState = EasyScriptState.Model;
                    }
                    break;
                case EasyScriptState.Model:
                    if (chr != signatureProtocol.GetPenUp())
                    {
                        currentMetaData.Model += chr;
                    }
                    else
                    {
                        currentState = EasyScriptState.FirmwareVersion;
                    }
                    break;
                case EasyScriptState.FirmwareVersion:
                    if (chr != signatureProtocol.GetPenUp())
                    {
                        currentMetaData.Version += chr;
                    }
                    else
                    {
                        //We might have a beacon here, limit the protocol version
                        //to just one character.
                        if (currentMetaData.ProtocolVersion.Length > 0)
                        {
                            currentMetaData.ProtocolVersion = currentMetaData.ProtocolVersion.Substring(0, 1);
                        }

                        foreach (IEasyScriptEventListener recv in listeners)
                        {
                            recv.SignatureMetaData(currentMetaData);
                        }
                        if (currentMetaData.ProtocolVersion == "A" || currentMetaData.ProtocolVersion == "B" || currentMetaData.ProtocolVersion == "C")
                        {
                            //Uncompressed
                            currentState = EasyScriptState.UncompressedStrokes;
                            decoder = new EasyScriptUncompressedDecoder(signatureProtocol, currentPosition, listeners);
                        }
                        else if (currentMetaData.ProtocolVersion == "D" || currentMetaData.ProtocolVersion == "E")
                        {
                            //Compressed.
                            currentState = EasyScriptState.CompressedStrokes;
                            decoder = new EasyScriptCompressedDecoder(signatureProtocol, currentPosition, listeners);
                        }
                        else
                        {
                            currentState = EasyScriptState.Unknown;
                            throw new SignatureInvalidException("Unknown signature protocol " + currentMetaData.ProtocolVersion, currentPosition);
                        }
                    }
                    break;
                case EasyScriptState.UncompressedStrokes:
                case EasyScriptState.CompressedStrokes:
                    if (chr == signatureProtocol.GetStartStream())
                    {
                        //Appears to be a new signature, lets assume the previous signature was canceled.
                        foreach (IEasyScriptEventListener recv in listeners)
                        {
                            recv.Cancel();
                        }
                        currentState = EasyScriptState.Unknown;
                        currentString.Length = 0;
                        Parse(chr);
                        break;
                    }

                    if (chr == cardProtocol.GetStartStream() && cardState != EasyScriptCardState.NotACard)
                    {
                        //This could potentially be a card, give the card parser a swing at this.
                        currentState = (currentState == EasyScriptState.UncompressedStrokes) ? EasyScriptState.CardInterruptingUncompressed : EasyScriptState.CardInterruptingCompressed;
                        cardState = EasyScriptCardState.Unknown;
                        cardBuffer.Length = 0;
                        cardBuffer.Append(chr);
                    }
                    else
                    {
                        decoder.ParseSignature(chr);
                    }
                    break;
                case EasyScriptState.CardInterruptingUncompressed:
                case EasyScriptState.CardInterruptingCompressed:
                    state = ParseCard(chr);
                    if (state == EasyScriptCardState.NotACard)
                    {
                        //We were mistaken, this isn't a card after all.
                        currentState = (currentState == EasyScriptState.CardInterruptingUncompressed) ? EasyScriptState.UncompressedStrokes : EasyScriptState.CompressedStrokes;
                        for (int i = 0; i < cardBuffer.Length; i++)
                        {
                            Parse(cardBuffer[i]);
                        }
                        cardState = EasyScriptCardState.Unknown;
                        cardBuffer.Length = 0;
                    }
                    else if (state == EasyScriptCardState.CardProcessed)
                    {
                        currentState = (currentState == EasyScriptState.CardInterruptingUncompressed) ? EasyScriptState.UncompressedStrokes : EasyScriptState.CompressedStrokes;
                    }
                    break;
                default:
                    //We're not interested in this character.
                    break;
            }
        }

        /// <summary>
        /// This method is responsible for taking over from the main state machine
        /// when it is detected that a card swipe is interrupting a signature.
        /// </summary>
        /// <param name="chr">Character to add to the card buffer.</param>
        /// <returns>State the machine is now in.</returns>
        private EasyScriptCardState ParseCard(char chr)
        {
            cardBuffer.Append(chr);

            switch (cardState)
            {
                case EasyScriptCardState.Unknown:
                    if (chr == cardProtocol.GetSentinel()[cardBuffer.Length - 2])
                    {
                        cardState = EasyScriptCardState.CardData;
                        return EasyScriptCardState.CardData;
                    }
                    cardState = EasyScriptCardState.NotACard;
                    return EasyScriptCardState.NotACard;
                case EasyScriptCardState.CardData:
                    if (chr == cardProtocol.GetEndStream())
                    {
                        CardSwipe swipe = ParseCardSwipe(cardBuffer.ToString());
                        foreach (IEasyScriptEventListener recv in listeners)
                        {
                            recv.CardSwipe(swipe);
                        }
                        cardState = EasyScriptCardState.CardProcessed;
                        return EasyScriptCardState.CardProcessed;
                    }
                    break;
                default:
                    return EasyScriptCardState.Unknown;
            }
            return EasyScriptCardState.Unknown;
        }

        /// <summary>
        /// This class is used to determine the end of stream in a decoder-agnostic
        /// way.
        /// </summary>
        private class EndOfStreamListener : IEasyScriptEventListener
        {
            /// <summary>
            /// Reference to the parent so we can change the state of the state
            /// machine.
            /// </summary>
            private EasyScript es;

            /// <summary>
            /// Initializes a new instance of the <see cref="EndOfStreamListener"/> class.
            /// </summary>
            /// <param name="es">Reference to the parent.</param>
            public EndOfStreamListener(EasyScript es)
            {
                this.es = es;
            }

            /// <summary>
            /// Receives coordinates, does nothing as this listener isn't
            /// interested in these events.
            /// </summary>
            /// <param name="coordinate">Coordinate from signature stream.</param>
            public void ReceiveCoordinate(Coordinate coordinate)
            {
                //We're not interested in this event.
            }

            /// <summary>
            /// Notification of new stroke, does nothing as we don't care.
            /// </summary>
            public void NewStroke()
            {
                //We're not interested in this event.
            }

            /// <summary>
            /// Notifies us that this signature was cancelled, sends the state
            /// machine back into the Unknown state.
            /// </summary>
            public void Cancel()
            {
                es.currentState = EasyScriptState.Unknown;
            }

            /// <summary>
            /// Notifies us that this signature is complete, sends the state
            /// machine back into the Unknown state.
            /// </summary>
            public void EndOfSignature()
            {
                es.currentState = EasyScriptState.Unknown;
            }

            /// <summary>
            /// Notification of signature metadata, doesn't do anything as we
            /// don't care.
            /// </summary>
            /// <param name="metadata">Metadata from signature stream.</param>
            public void SignatureMetaData(SignatureMetaData metadata)
            {
                //We're not interested in this event.
            }

            /// <summary>
            /// Notification of card swipe, doesn't do anything as we don't care.
            /// </summary>
            /// <param name="swipe">Card swipe.</param>
            public void CardSwipe(CardSwipe swipe)
            {
                //We're not interested in this event.
            }
        }

        /// <summary>
        /// This method accepts a new event listener which will be called when
        /// signature or card events are detected in the character stream passed in
        /// through ParseSignature().
        /// </summary>
        /// <param name="recv">Listener to notify when events are generated.</param>
        public void AddListener(IEasyScriptEventListener recv)
        {
            listeners.Add(recv);
        }

        /// <summary>
        /// This method removes an event listener from the list of listeners
        /// interested in signature events.
        /// </summary>
        /// <param name="recv">Listener that is no longer interested in signature events.</param>
        public void RemoveListener(IEasyScriptEventListener recv)
        {
            listeners.Remove(recv);
        }

        /// <summary>
        /// Gets the current signature protocol
        /// </summary>
        /// <returns>current signature protocol</returns>
        public ISignatureProtocol GetSignatureProtocol()
        {
            return this.signatureProtocol;
        }

        /// <summary>
        /// Gets the current card swipe protocol
        /// </summary>
        /// <returns>current card swipe protocol</returns>
        public ICardSwipeProtocol GetCardProtocol()
        {
            return this.cardProtocol;
        }

        /// <summary>
        /// This method will attempt to take a keyboard event from either KeyUp
        /// or KeyDown and attempt to turn it into its representation in the
        /// English - US keyboard layout. This should allow EasyScript devices
        /// to work in mixed keyboard environments.
        /// </summary>
        /// <param name="e">Keyboard event to translate.</param>
        /// <returns>Character representing the key press if available, null otherwise.</returns>
        public static char? KeyboardEventToChar(KeyEventArgs e)
        {
            bool shift = e.Modifiers == Keys.Shift;
            switch (e.KeyCode)
            {
                case Keys.Oemtilde: return (!shift) ? '`' : '~';
                case Keys.D1: return (!shift) ? '1' : '!';
                case Keys.D2: return (!shift) ? '2' : '@';
                case Keys.D3: return (!shift) ? '3' : '#';
                case Keys.D4: return (!shift) ? '4' : '$';
                case Keys.D5: return (!shift) ? '5' : '%';
                case Keys.D6: return (!shift) ? '6' : '^';
                case Keys.D7: return (!shift) ? '7' : '&';
                case Keys.D8: return (!shift) ? '8' : '*';
                case Keys.D9: return (!shift) ? '9' : '(';
                case Keys.D0: return (!shift) ? '0' : ')';
                case Keys.OemMinus: return (!shift) ? '-' : '_';
                case Keys.Oemplus: return (!shift) ? '=' : '+';

                case Keys.Q: return (!shift) ? 'q' : 'Q';
                case Keys.W: return (!shift) ? 'w' : 'W';
                case Keys.E: return (!shift) ? 'e' : 'E';
                case Keys.R: return (!shift) ? 'r' : 'R';
                case Keys.T: return (!shift) ? 't' : 'T';
                case Keys.Y: return (!shift) ? 'y' : 'Y';
                case Keys.U: return (!shift) ? 'u' : 'U';
                case Keys.I: return (!shift) ? 'i' : 'I';
                case Keys.O: return (!shift) ? 'o' : 'O';
                case Keys.P: return (!shift) ? 'p' : 'P';
                case Keys.OemOpenBrackets: return (!shift) ? '[' : '{';
                case Keys.OemCloseBrackets: return (!shift) ? ']' : '}';
                case Keys.Oem5: return (!shift) ? '\\' : '|';

                case Keys.A: return (!shift) ? 'a' : 'A';
                case Keys.S: return (!shift) ? 's' : 'S';
                case Keys.D: return (!shift) ? 'd' : 'D';
                case Keys.F: return (!shift) ? 'f' : 'F';
                case Keys.G: return (!shift) ? 'g' : 'G';
                case Keys.H: return (!shift) ? 'h' : 'H';
                case Keys.J: return (!shift) ? 'j' : 'J';
                case Keys.K: return (!shift) ? 'k' : 'K';
                case Keys.L: return (!shift) ? 'l' : 'L';
                case Keys.OemSemicolon: return (!shift) ? ';' : ':';
                case Keys.OemQuotes: return (!shift) ? '\'' : '"';

                case Keys.Z: return (!shift) ? 'z' : 'Z';
                case Keys.X: return (!shift) ? 'x' : 'X';
                case Keys.C: return (!shift) ? 'c' : 'C';
                case Keys.V: return (!shift) ? 'v' : 'V';
                case Keys.B: return (!shift) ? 'b' : 'B';
                case Keys.N: return (!shift) ? 'n' : 'N';
                case Keys.M: return (!shift) ? 'm' : 'M';
                case Keys.Oemcomma: return (!shift) ? ',' : '<';
                case Keys.OemPeriod: return (!shift) ? '.' : '>';
                case Keys.OemQuestion: return (!shift) ? '/' : '?';

                case Keys.Space: return ' ';
                case Keys.Return: return '\r';
                default: return null;
            }
        }
    }
}
