///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2005 Gabriel Gunderson
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the:
//
// Free Software Foundation, Inc.
// 59 Temple Place, Suite 330
// Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////
// About monotone.cs:
// Provides a way for Mono or .NET applications to talk to the Asterisk
// telephony server via AGI (Asterisk Gateway Interface) or FastAGI.
//
// Author:
// Gabriel Gunderson <gabe(a)gundy.org>, gundy.org
// 
// Contributers:
// Michael Giagnocavo <mgg(a)telefinity.com> (FastAGI support)
//
// Comments:
// I welcome suggestions and fixes.
// Expect things to break between versions while still in the 0.0X stage. :)
///////////////////////////////////////////////////////////////////////////////

using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;

// This namespace is where all of the core AGI and FastAGI functionality is found.
namespace AGI
{

    // Enums for each key on a phone pad.
    public enum Keypad
    {
        Zero = 48,
        One = 49,
        Two = 50,
        Three = 51,
        Four = 52,
        Five = 53,
        Six = 54,
        Seven = 55,
        Eight = 56,
        Nine = 57,
        Star = 42,
        Pound = 35,
        None = 0,
        Unknown = -1
    }

    // Enums for each line status in Asterisk.
    public enum LineStatus
    {
        DownAvailable = 0,
        DownReserved = 1,
        OffHook = 2,
        DigitsDialed = 3,
        LineRinging = 4,
        RemoteEndRinging = 5,
        LineUp = 6,
        LineBusy = 7,
        StatusFailed = -1
    }

    // The object that provides the main functionality.
    public class MonoTone : IDisposable
    {
        private const int ReplyOffset = 11;
        private bool debug;
        private bool disposed;
        private string request;
        private string channel;
        private string language;
        private string type;
        private string uniqueId;
        private string callerId;
        private string dnid;
        private string rdnis;
        private string context;
        private string extension;
        private string priority;
        private string enhanced;
        private string accountCode;
        private TextReader input;
        private TextWriter output;
        private TextWriter errorOutput;

        // Converts strings to the matching KeyPad enum.
        public static Keypad StringToKey(string keyPressed)
        {
            switch (keyPressed)
            {
                case "48": return Keypad.Zero;
                case "49": return Keypad.One;
                case "50": return Keypad.Two;
                case "51": return Keypad.Three;
                case "52": return Keypad.Four;
                case "53": return Keypad.Five;
                case "54": return Keypad.Six;
                case "55": return Keypad.Seven;
                case "56": return Keypad.Eight;
                case "57": return Keypad.Nine;
                case "42": return Keypad.Star;
                case "35": return Keypad.Pound;
                case "0": return Keypad.None;
                default: return Keypad.Unknown;
            }
        }

        // Converts KeyPad enums to strings.
        public static string KeyToString(Keypad keyPressed)
        {
            switch (keyPressed)
            {
                case Keypad.Zero: return "0";
                case Keypad.One: return "1";
                case Keypad.Two: return "2";
                case Keypad.Three: return "3";
                case Keypad.Four: return "4";
                case Keypad.Five: return "5";
                case Keypad.Six: return "6";
                case Keypad.Seven: return "7";
                case Keypad.Eight: return "8";
                case Keypad.Nine: return "9";
                case Keypad.Star: return "Star";
                case Keypad.Pound: return "Pound";
                case Keypad.None: return "None";
                default: return "Unknown";
            }
        }

        // Constructor for MonoTone that takes care of initialization.
        public MonoTone()
        {
            this.input = System.Console.In;
            this.output = System.Console.Out;
            this.output.Flush();
            this.errorOutput = System.Console.Error;
            Initialize();
        }

        // Constructor for MonoTone for FastAGI.
        public MonoTone(string outputIp, int outputPort)
        {
            TcpClient client = new TcpClient();
            client.Connect(outputIp, outputPort);
            this.input = new StreamReader(client.GetStream());
            this.output = new StreamWriter(client.GetStream());
            this.output.Flush();
            this.errorOutput = new StreamWriter(client.GetStream());
            Initialize();
        }

        // Constructor for MonoTone for FastAGI.
        public MonoTone(TcpClient client)
        {
            this.input = new StreamReader(client.GetStream());
            this.output = new StreamWriter(client.GetStream());
            //this.output.Flush();
            this.errorOutput = new StreamWriter(client.GetStream());
            Initialize();
        }

        // Constructor for MonoTone for FastAGI.
        public MonoTone(string outputIp, int outputPort, string errorIp, int errorPort)
        {
            TcpClient client = new TcpClient();
            client.Connect(outputIp, outputPort);
            TcpClient errorClient = new TcpClient();
            client.Connect(errorIp, errorPort);
            this.input = new StreamReader(client.GetStream());
            this.output = new StreamWriter(client.GetStream());
            this.output.Flush();
            this.output = new StreamWriter(errorClient.GetStream());
            Initialize();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void Dispose( bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.output != null)
                    {
                        this.output.Flush();
                        this.output.Close();
                    }
                    if (this.input != null)
                    {
                        this.input.Close();
                    }
                    if (this.errorOutput != null)
                    {
                        this.errorOutput.Flush();
                        this.errorOutput.Close();
                    }
                }
            }
            this.disposed = true;
        }

        // Constructor for MonoTone for FastAGI.
        public MonoTone(System.IO.StreamReader input, System.IO.StreamWriter output )
        {
            this.input = input;
            this.output = output;
            Initialize();
        }

        // Overloaded constructor for MonoTone with options for initialization and debugging.
        public MonoTone(bool init, bool debugInfo)
        {
            if (init)
            {
                Initialize();
            }
            if (debugInfo)
            {
                debug = debugInfo;
            }
        }

        // Property for debugging.
        public bool Debug
        {
            get { return debug; }
            set { debug = value; }
        }

        // Read only property for the request variable read in during initialization.
        public string Request
        {
            get { return request; }
        }

        // Read only property for the channel variable read in during initialization.
        public string Channel
        {
            get { return channel; }
        }

        // Read only property for the language variable read in during initialization.
        public string Language
        {
            get { return language; }
        }

        // Read only property for the type variable read in during initialization.
        public string Type
        {
            get { return type; }
        }

        // Read only property for the uniqueid variable read in during initialization.
        public string UniqueId
        {
            get { return uniqueId; }
        }

        // Read only property for the callerid variable read in during initialization.
        public string CallerId
        {
            get { return callerId; }
        }

        // Read only property for the dnid variable read in during initialization.
        public string Dnid
        {
            get { return dnid; }
        }

        // Read only property for the rdnis variable read in during initialization.
        public string Rdns
        {
            get { return rdnis; }
        }

        // Read only property for the context variable read in during initialization.
        public string Context
        {
            get { return context; }
        }

        // Read only property for the extension variable read in during initialization.
        public string Extension
        {
            get { return extension; }
        }

        // Read only property for the priority variable read in during initialization.
        public string Priority
        {
            get { return priority; }
        }

            // Read only property for the enhanced variable read in during initialization.
        public string Enhanced
        {
            get { return enhanced; }
        }

        // Read only property for the accountcode variable read in during initialization.
        public string AccountCode
        {
            get { return accountCode; }
        }

        // Reads in the variables that are available at run time.
        public void Initialize()
        {
            // Read all initialization variables from Asterisk. 
            do { }
            while (processInitialVariable(this.input.ReadLine()));
        }

        private bool processInitialVariable(string line)
        {
            int colon = line.IndexOf(':');
            if (colon < 0)
            {
                // End of initial variables.
                return false;
            }

            // Asterisk formats the variables as "name: value".
            // There's gotta be a more elegant way of dealing with all these variables...
            string name = line.Substring(0, colon).ToLowerInvariant();
            string value = line.Substring(colon + 1).Trim();
            switch (name)
            {
                case "agi_request": this.request = value; break;
                case "agi_channel": this.channel = value; break;
                case "agi_language": this.language = value; break;
                case "agi_type": this.type = value; break;
                case "agi_uniqueid": this.uniqueId = value; break;
                case "agi_callerid": this.callerId = value; break;
                case "agi_dnid": this.dnid = value; break;
                case "agi_rdnis": this.rdnis = value; break;
                case "agi_context": this.context = value; break;
                case "agi_extension": this.extension = value; break;
                case "agi_priority": this.priority = value; break;
                case "agi_enhanced": this.enhanced = value; break;
                case "agi_accountcode": this.accountCode = value; break;
            }
            return true;
        }

        // Writes text to the Asterisk console.
        public void Output(string format, params object[] args)
        {
            format = String.Concat("MONOTONE: ", format);
            this.output.WriteLine(format, args);
            this.output.Flush();
        }

        // Writes text to the Asterisk console.
        public void Output(string message)
        {
            this.output.WriteLine("MONOTONE: {0}", message);
            this.output.Flush();
        }

        // Writes text to the Asterisk console if Debugging in on.
        public void BugInfo(string message)
        {
            if (debug)
            { 
                this.output.WriteLine("BUG INFO: {0}", message);
                this.output.Flush();
            }
        }

        // Writes text to the Asterisk console if Debugging in on.
        public void BugInfo(string format, params object[] args)
        {
            format = String.Concat("BUG INFO: ", format);
            if (debug)
            {
                this.errorOutput.WriteLine(format, args);
                this.errorOutput.Flush();
            }
        }

        // Does all of the reading and writing to STDIN and STDOUT.  Trims common results.
        private string ProcessAgi(string command)
        {
            BugInfo("ProcessAGI-Command = {0}", command);
            // This change needed to be made because of the difference between .NET's and Mono's WriteLine().
            // this.output.WriteLine(command);
            this.output.Write(command + "\n");
            this.output.Flush();
            string reply = this.input.ReadLine();
            BugInfo("ProcessAGI-reply = {0}", reply);
            string result = null;
            Regex replyResult = new Regex("^200 result=");
            if (replyResult.IsMatch(reply))
            {
                result = reply.Substring(ReplyOffset, reply.Length - ReplyOffset);
            }
            BugInfo("ProcessAGI-result = {0}", result);
            return result;
        }

        // Provides additional processing for replies that fit. 
        private bool ProcessKeyPress(string preProcess, out Keypad keyPressed)
        {
            BugInfo("ProcessKeyPress-PreProcess = {0}", preProcess);
            switch (preProcess)
            {
                case "0":
                keyPressed = Keypad.None;
                return true;
                case "-1":
                keyPressed = Keypad.None;
                return false;
                default:
                keyPressed = StringToKey(preProcess);
                return true;
            }
        }

        // Provides additional processing for replies that fit. 
        private bool ProcessParen(string preProcess, out string value)
        {
            BugInfo("ProcessParen-PreProcess = {0}", preProcess);
            string returned = preProcess.Substring(0, 1);
            if (returned == "0")
            {
                value = null;
                return false;
            }
            else
            {
                value = TrimParen(preProcess);
                return true;
            }
        }

        // Provides additional processing for replies that fit. 
        private bool ProcessOneZerro(string preProcess)
        {
            BugInfo("ProcessOneZerro-PreProcess = {0}", preProcess);
            switch (preProcess)
            {
                case "1":
                return true;
                case "0":
                return false;
                default:
                throw new UnexpectedOutputException(preProcess);
            }
        }

        // Provides additional processing for replies that fit. 
        private bool ProcessOneNegOne(string preProcess)
        {
            BugInfo("ProcessOneNegOne-PreProcess = {0}", preProcess);
            switch (preProcess)
            {
                case "1":
                return true;
                case "-1":
                return false;
                default:
                throw new UnexpectedOutputException(preProcess);
            }
        }

        // Provides additional processing for replies that fit. 
        private bool ProcessZeroNegOne(string preProcess)
        {
            BugInfo("ProcessZerroNegOne-PreProcess = {0}", preProcess);
            switch (preProcess)
            {
                case "0":
                return true;
                case "-1":
                return false;
                default:
                throw new UnexpectedOutputException(preProcess);
            }
        }

        // Provides additional processing for replies that fit. 
        private LineStatus ProcessLineStatus(string preProcess)
        {
            BugInfo("ProcessLineStatus-PreProcess = {0}", preProcess);
            switch (preProcess)
            {
                case "-1": return LineStatus.StatusFailed;
                case "0": return LineStatus.DownAvailable;
                case "1": return LineStatus.DownReserved;
                case "2": return LineStatus.OffHook;
                case "3": return LineStatus.DigitsDialed;
                case "4": return LineStatus.LineRinging;
                case "5": return LineStatus.RemoteEndRinging;
                case "6": return LineStatus.LineUp;
                case "7": return LineStatus.LineBusy;
                default: throw new UnexpectedOutputException(preProcess);
            }
        }

        // Trims the () off of replies.
        private string TrimParen(string parenString)
        {
            BugInfo("TrimParen-ParenString = {0}", parenString);
            int start = parenString.IndexOf("(");
            return parenString.Substring(start + 1, parenString.Length - start - 2);
        }

        /////////////////////// ALL METHODS BELOW ARE WRAPPERS ////////////////////////

        // Answers channel if not already in answer state.
        // Returns true if answered and false if failed.
        public bool Answer()
        {
            string command = "ANSWER";
            string reply = ProcessAgi(command);
            return ProcessZeroNegOne(reply);
        }

        // Checks the status of the specified or connected channel.
        // Returns a LineStatus enum.
        public LineStatus ChannelStatus(string channelName)
        {
            string command = String.Format("CHANNEL STATUS {0}", channelName);
            string reply = ProcessAgi(command);
            return ProcessLineStatus(reply);
        }

        // Override finding status for current channel.
        // Returns a LineStatus enum.
        public LineStatus ChannelStatus()
        {
            string command = String.Format("CHANNEL STATUS");
            string reply = ProcessAgi(command);
            return ProcessLineStatus(reply);
        }

        // Deletes an entry in the Asterisk database for a given family and key.
        // Returns true if successful and false if not.
        public bool DatabaseDel(string family, string key)
        {
            string command = String.Format("DATABASE DEL {0} {1}", family, key);
            string reply = ProcessAgi(command);
            return ProcessOneZerro(reply);
        }

        // Deletes a family or specific keytree within a family in the Asterisk database.
        // Returns true if successful and false if not.
        public bool DatabaseDelTree(string family)
        {
            string command = String.Format("DATABASE DELTREE {0}", family);
            string reply = ProcessAgi(command);
            return ProcessOneZerro(reply);
        }

        // Override deletes a family or specific keytree within a family in the Asterisk database.
        // Returns true if successful and false if not.
        public bool DatabaseDelTree(string family, string keyTree)
        {
            string command = String.Format("DATABASE DELTREE {0} {1}", family, keyTree);
            string reply = ProcessAgi(command);
            return ProcessOneZerro(reply);
        }

        // Retrieves an entry in the Asterisk database for a given family and key.
        // Returns true if successful and false if not.
        // Value comes out with the key's value.
        public bool DatabaseGet(string family, string key, out string value)
        {
            string command = String.Format("DATABASE GET {0} {1}", family, key);
            string reply = ProcessAgi(command);
            return ProcessParen(reply, out value);
        }

        // Adds or updates an entry in the Asterisk database for a given family, key, and value.
        // Returns true if successful and false if not.
        public bool DatabasePut(string family, string key, string value)
        {
            string command = String.Format("DATABASE PUT {0} {1} {2}", family, key, value);
            string reply = ProcessAgi(command);
            return ProcessOneZerro(reply);
            // TODO - CHECK PROPER RETURNS
            // failure: 200 result=0
            // success: 200 result=1 (<value>) 
        }

        // Executes a given application.
        // Returns true if successful and false if not.
        // Output comes out with what the application returns from its STDOUT.
        public bool Execute(string application, string options, out string output)
        {
            string command = String.Format("EXEC {0} {1}", application, options);
            string reply = ProcessAgi(command);
            switch (reply)
            {
                case "-2":
                output = null;
                return false;
                default:
                output = reply;
                return true;
            }
        }

        // Stream the given file, and receive DTMF data.
        // Returns a string with the digits received from the other end of the channel.
        public string GetData(string file, int timeout, int maxDigits)
        {
            string command = String.Format("GET DATA {0} {1} {2}", file, timeout, maxDigits);
            return ProcessAgi(command);
            // TODO - CHECK PROPER RETURNS
            // failure: 200 result=-1
            // timeout: 200 result=<digits> (timeout)
            // success: 200 result=<digits>
        }

        // Gets a channel variable.
        // Returns true if successful and false if not.
        // Value comes out with the variables' value.
        public bool GetVariable(string variableName, out string value)
        {
            string command = String.Format("GET VARIABLE {0}", variableName);
            string reply = ProcessAgi(command);
            return ProcessParen(reply, out value);
        }

        // Hangs up the current channel.
        // Returns true if successful and false if not.
        public bool HangUp()
        {
            string command = "HANGUP";
            string reply = ProcessAgi(command);
            return ProcessOneNegOne(reply);
        }

        // Override hangs up the current or specified channel.
        // Returns true if successful and false if not.
        public bool HangUp(string channelName)
        {
            string command = String.Format("HANGUP {0}", channelName);
            string reply = ProcessAgi(command);
            return ProcessOneNegOne(reply);
        }

        // Does nothing.
        // Returns nothing.
        public void Noop()
        {
            string command = "NOOP";
            ProcessAgi(command);
        }

        // Receives text from channels supporting it.
        // Returns true if successful and false if not.
        // Value comes out with ASCII numerical value of the character if one is received. 
        public bool ReceiveChar(int timeout, out string value)
        {
            string command = String.Format("RECEIVE CHAR {0}", timeout);
            string reply = ProcessAgi(command);
            switch (reply)
            {
                case "-1":
                value = null;
                return false;
                case "0":
                value = null;
                return false;
                default:
                value = reply;
                return true;
            }
        }

        // Record to a file until a given DTMF digit in the sequence is received.
        // Returns true if successful and false if not.
        public bool Record(string fileName, string format, string escapeDigits, int timeout, int offsetSamples, bool beep, int silence)
        {
            // TODO - CHECK PROPER RETURNS
            // failure to write: 200 result=-1 (writefile)
            // failure on waitfor: 200 result=-1 (waitfor) endpos=<offset>
            // hangup: 200 result=0 (hangup) endpos=<offset>
            // interrupted: 200 result=<digit> (dtmf) endpos=<offset>
            // timeout: 200 result=0 (timeout) endpos=<offset>
            // random error: 200 result=<error> (randomerror) endpos=<offset>
            bool result;
            string reply;
            string command = string.Format(
            "RECORD FILE {0} {1} {2} {3} ",
            fileName, format, escapeDigits, timeout);
            if (beep)
            {
                command += "BEEP ";
            }
            if (silence > 0)
            {
                command += "s=" + silence;
            }
            this.output.WriteLine(command);
            this.output.Flush();
            reply = this.input.ReadLine();
            switch (reply)
            {
                case "200 result=-1":
                result = false;
                break;
                case "200 result=0":
                result = true;
                break;
                default:
                this.errorOutput.WriteLine("RECORD FILE gave the unexpected reply of {0}", reply);
                this.errorOutput.Flush();
                result = false;
                break;
            }
            return result;
        }

        // Say a given digit string, returning early if any of the given DTMF digits are received on the channel.
        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool SayDigits(string digits, string escapeDigits, out Keypad keyPressed)
        {
            string command = String.Format("SAY DIGITS {0} \"{1}\"", digits, escapeDigits);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
        }

        // Say a given number, returning early if any of the given DTMF digits are received on the channel.
        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool SayNumber(int number, string escapeDigits, out Keypad keyPressed)
        {
            string command = String.Format("SAY NUMBER {0} \"{1}\"", number, escapeDigits);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
        }

        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool SayPhonetic(string phonetic, string escapeDigits, out Keypad keyPressed)
        {
            string command = String.Format("SAY PHONETIC {0} \"{1}\"", phonetic, escapeDigits);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
        }

        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool SayTime(string time, string escapeDigits, out Keypad keyPressed)
        {
            string command = String.Format("SAY TIME {0} \"{1}\"", time, escapeDigits);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
        }

        // Sends the given image on a channel.
        // Returns true if successful and false if not.
        public bool SendImage(string image)
        {
            string command = String.Format("SEND IMAGE {0}", image);
            string reply = ProcessAgi(command);
            return ProcessZeroNegOne(reply);
        }

        // Sends the given text on a channel.
        // Returns true if successful and false if not.
        public bool SendText(string text)
        {
            string command = String.Format("SEND TEXT {0}", text);
            string reply = ProcessAgi(command);
            return ProcessZeroNegOne(reply);
        }

        // Autohangup channel in some time.
        // Returns nothing.
        public void SetAutoHangUp(int seconds)
        {
            string command = String.Format("SET AUTOHANGUP {0}", seconds);
            ProcessAgi(command);
        }

        // Changes the callerid of the current channel.
        // Returns nothing.
        public void SetCallerId(string number)
        {
            string command = String.Format("SET CALLERID {}", number);
            ProcessAgi(command);
        }

        // Sets the context for continuation upon exiting the application.
        // Returns nothing.
        public void SetContext(string context)
        {
            string command = String.Format("SET CONTEXT {0}", context);
            ProcessAgi(command);
        }

        // Changes the extension for continuation upon exiting the application.
        // Returns nothing.
        public void SetExtension(string extension)
        {
            string command = String.Format("SET EXTENSION {0}", extension);
            ProcessAgi(command);
        }

        // Enables/Disables the music on hold generator.
        // Returns nothing.
        public void SetMusic(bool on, string musicClass)
        {
            string command;
            if (on)
            {
                command = String.Format("SET MUSIC ON {0}", musicClass);
            }
            else
            {
                command = String.Format("SET MUSIC OFF {0}", musicClass);
            }
            ProcessAgi(command);
        }

        // Override enables/disables the music on hold generator using default channel.
        // Returns nothing.
        public void SetMusic(bool on)
        {
            string command;
            if (on)
            {
                command = "SET MUSIC ON";
            }
            else
            {
                command = "SET MUSIC OFF";
            }
            ProcessAgi(command);
        }

        // Changes the priority for continuation upon exiting the application.
        // Returns nothing.
        public void SetPriority(int priority)
        {
            string command = String.Format("SET PRIORITY {0}", priority);
            ProcessAgi(command);
        }

        // Sets a channel variable.
        // Returns nothing.
        public void SetVariable(string variableName, string value)
        {
            string command = String.Format("SET VARIABLE {0} {1}", variableName, value);
            ProcessAgi(command);
        }

        public bool StreamFile(string fileName, string escapeDigits, out Keypad keyPressed)
        {
            return StreamFile(fileName, escapeDigits, out keyPressed, 0);
        }

        // Send the given file, allowing playback to be interrupted by the given digits, if any.
        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool StreamFile(string fileName, string escapeDigits, out Keypad keyPressed, int sampleOffset)
        {
            string command = String.Format("STREAM FILE {0} {1} {2}", fileName, escapeDigits, sampleOffset);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
            // TODO - CHECK PROPER RETURNS
            // failure: 200 result=-1 endpos=<sample offset>
            // failure on open: 200 result=0 endpos=0
            // success: 200 result=0 endpos=<offset>
            // digit pressed: 200 result=<digit> endpos=<offset>
            // <offset> is the stream position streaming stopped. If it equals <sample offset> there was probably an error.
            // <digit> is the ascii code for the digit pressed. 
        }

        // Enable/Disable TDD transmission/reception on a channel.
        // Returns true if successful and false if not.
        public bool TddMode(bool on)
        {
            string command;
            if (on)
            {
                command = "TDM MODE ON";
            }
            else
            {
                command = "TDM MODE OFF";
            }
            string result = ProcessAgi(command);
            return ProcessOneZerro(result);
            // TODO - CHECK PROPER RETURNS
            // failure: 200 result=-1
            // not capable: 200 result=0
            // success: 200 result=1 
        }

        // Logs a message to the asterisk verbose log.
        // Returns nothing.
        public void Verbose(string message, int level)
        {
            string command = String.Format("VERBOSE {0} {1}", message, level);
            ProcessAgi(command);
        }

        public void Verbose(string message, params object[] args)
        {
            string command = String.Format(message, args);
            ProcessAgi(command);
        }

        // Waits up to 'timeout' milliseconds for channel to receive a DTMF digit.
        // Returns true if successful and false if not.
        // KeyPressed comes out with a KeyPad if one is received. 
        public bool WaitForDigit(int timeout, out Keypad keyPressed)
        {
            string command = String.Format("WAIT FOR DIGIT {0}", timeout);
            string reply = ProcessAgi(command);
            return ProcessKeyPress(reply, out keyPressed);
        }
    }

    public class MonoToneException : Exception
    {
        public MonoToneException(String message)
            : base(message)
        {
        }
        public MonoToneException(String message, Exception innerException)
            : base(message, innerException)
        {
        }
    }

    public class FailedAgiCallException : MonoToneException
    {
        public FailedAgiCallException(String message)
            : base(message)
        {
        }
        public FailedAgiCallException(String message, Exception innerException)
            : base(message, innerException)
        {
        }
    }

    public class InvalidSyntaxException : MonoToneException
    {
        public InvalidSyntaxException(string message)
            : base(message)
        {
        }
        public InvalidSyntaxException(string message, Exception innerException)
            : base(message, innerException)
        {
        }
    }

    public class UnexpectedOutputException : MonoToneException
    {
        public UnexpectedOutputException(string message)
            : base(message)
        {
        }
        public UnexpectedOutputException(string message, Exception innerException)
            : base(message, innerException)
        {
        }
    }
}