///////////////////////////////////////////////////////////////////////////////
// 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
//
// Or get off your butt and find a copy of it on the Internet ;)
//
// And yeah, this program is not perfect. I welcome suggestions and fixes.
// Email them to gabe(a)gundy.org
// gundy.org
///////////////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////////////
//////////////////////////// MONO-TONE Version .01 ////////////////////////////
///////////////////////////////////////////////////////////////////////////////

using System;
using System.Text.RegularExpressions;

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
{
    private const int REPLY_OFFSET = 11;
    private bool debug = false;

    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;

    // Converst 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()
    {
        Initialize();
    }

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

    // Property for the 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 RDNIS
    {
        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()
    {
        request = Console.In.ReadLine();
        channel = Console.In.ReadLine();
        language = Console.In.ReadLine();
        type = Console.In.ReadLine();
        uniqueid = Console.In.ReadLine();
        callerid = Console.In.ReadLine();
        dnid = Console.In.ReadLine();
        rdnis = Console.In.ReadLine();
        context = Console.In.ReadLine();
        extension = Console.In.ReadLine();
        priority = Console.In.ReadLine();
        enhanced = Console.In.ReadLine();
        accountcode = Console.In.ReadLine();
        Console.In.ReadLine();
    }

    // Writes text to the Asterisk console.
    public void Output(string Format, params object[] Args)
    {
        Format = String.Concat("MONOTONE: ", Format);
        Console.Error.WriteLine(Format, Args);
    }

    // Overload
    public void Output(string Message)
    {
        Console.Error.WriteLine("MONOTONE: {0}", Message);
    }

    // Overloaded. writes text to the Asterisk console if Debugging in on.
    public void BugInfo(string Message)
    {
        if(debug)
        Console.Error.WriteLine("BUG INFO: {0}" , Message);
    }

    // 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)
        Console.Error.WriteLine(Format, Args);
    }

    // Does all of the reading and writing to STDIN and STDOUT.  Trims common results.
    private string ProcessAGI(string Command)
    {
        BugInfo("ProcessAGI-Command = {0}", Command);
        Console.Out.WriteLine(Command);
        string reply = Console.In.ReadLine();
        BugInfo("ProcessAGI-reply = {0}", reply);
        string result = null;
        Regex replyResult = new Regex("^200 result=");
        if (replyResult.IsMatch(reply))
        {
            result = reply.Substring(REPLY_OFFSET, reply.Length - REPLY_OFFSET);
        }
        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 UnexpectedOutput(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 UnexpectedOutput(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 UnexpectedOutput(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 UnexpectedOutput(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, int EscapeDigits, int TimeOut, bool 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;
        if (OffSetSamples)
            if (Beep)
            Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} offset samples BEEP s={4}", FileName, Format, EscapeDigits, TimeOut, Silence);
            else
            Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} offset samples s={4}", FileName, Format, EscapeDigits, TimeOut, Silence);
        else
            if (Beep)
            Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} BEEP s={4}", FileName, Format, EscapeDigits, TimeOut, Silence);
            else
            Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} s={4}", FileName, Format, EscapeDigits, TimeOut, Silence);
        reply = Console.In.ReadLine();
        switch (reply)
        {
            case "200 result=-1":
                result = false;
                break;
            case "200 result=0":
                result = true;
                break;
            default:
                Console.Error.WriteLine("RECORD FILE gave the unexpected reply of {0}", reply);
                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 Class)
    {
        string command;
        if (On)
            command = String.Format("SET MUSIC ON {0}", Class);
        else
            command = String.Format("SET MUSIC OFF {0}", Class);
        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);
    }
    
    // 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)
    {
        string command = String.Format("STREAM FILE {0} {1} sample offset", FileName, EscapeDigits);
        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);
    }
    
    class MonoToneException : ApplicationException
    {
        public MonoToneException(String Message)
            : base(Message)
        {
        }
        public MonoToneException(String Message, Exception InnerException)
            : base(Message, InnerException)
        {
        }
    }
    
    class FailedAGICall : MonoToneException
    {
        public FailedAGICall(String Message)
            : base(Message)
        {
        }
        public FailedAGICall(String Message, Exception InnerException)
            : base(Message, InnerException)
        {
        }
    }
    
    class InvalidSyntax : MonoToneException
    {
        public InvalidSyntax(String Message)
            : base(Message)
        {
        }
        public InvalidSyntax(String Message, Exception InnerException)
            : base(Message, InnerException)
        {
        }
    }
    
    class UnexpectedOutput : MonoToneException
    {
        public UnexpectedOutput(String Message)
            : base(Message)
        {
        }
        public UnexpectedOutput(String Message, Exception InnerException)
            : base(Message, InnerException)
        {
        }
    }
    
}
}