/** 
    java implementation of the SimpleCapi interface, to be used
    to interface to the capiserver program.

    Copyright (c) 2002 Matthias Kramm (kramm@quiss.org)

    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.

    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

    @author Matthias Kramm (kramm@quiss.org)
*/

import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;

public class SimpleCapi 
{
    static final int TIMEOUT=0;
    static final int CAPI_EVENT_HANGUP=1;    // connection got terminated
    static final int CAPI_EVENT_INCOMING=2;  // incoming call
    static final int CAPI_EVENT_DONE=3;      // job is done
    static final int CAPI_EVENT_DIALTONE=4;  // other end pressed some keys
    static final int CAPI_EVENT_CONNECTED=5; // an outgoing call got answered

    /* kept for compatibilty- these values are now also in CapiEvent */
    static final int CAPI_REASON_TECHNICAL=1;
    static final int CAPI_REASON_BUSY=2;
    static final int CAPI_REASON_WRONGNUMBER=3; //"kein anschluss unter dieser nummer" - in germany
    static final int CAPI_REASON_REJECTED=4;
    static final int CAPI_REASON_NOANSWER=5;   //other end didn't answer in time
      
    /**Establishes a connection to the main Caiviar IVR Server. The
     * Caiviar IVR Server is an application running on some remote (or local)
     * system which has ISDN hardware build into it. This server will then
     * act according to the commands which are send with the function calls
     * of this class.
     *
     * @param server       The Adress of the IVR Server
     * @param port	   The port on the IVR Server (usually 7347)
     */
    public SimpleCapi(String server, int port) throws IOException, NoLineFreeException
    {
	socket=new Socket(server, port);
	OutputStream out=socket.getOutputStream();
	InputStream in=socket.getInputStream();
	this.in = new BufferedReader(new InputStreamReader(in));
	this.out = new PrintWriter(out);
	try {
	    line = Integer.parseInt(readAnswer("open="));
	} catch(CommandFailedException e) {
	    throw new NoLineFreeException(e.getMessage());
	}
    }

    /**
     * sets another host msisdn than the one specified in the defaults.
     * Use this before calling any other functions.
     * @param msisdn   the msisdn to use
     */
    synchronized public void setMSISDN(String msisdn) throws IOException, CommandFailedException
    {
	sendCommand("set msisdn "+msisdn);
	line = Integer.parseInt(readAnswer("ok="));
    }

    /**
     * signal to the server that incoming calls should trigger an event. If
     * this is called, incoming connections will trigger a CAPI_EVENT_INCOMING
     * event. Note that to reset this, you'll have to terminate the connection
     * to the server and reconnect.
     */
    synchronized public void acceptCalls () throws IOException, CommandFailedException
    {
	sendCommand("accept");
	readAnswer("ok");
    }

    /** 
     * call the specified msisdn. 
     * 
     * @param msisdn  the msisdn to call
     * @param timeout  the number of seconds to wait for an answer
     * @returns 0  if somebody answered in time, REASON otherwise
     */
    synchronized public int call ( String msisdn, int timeout) throws IOException, CommandFailedException
    {
	sendCommand("call "+msisdn+" "+timeout);
	return Integer.parseInt(readAnswer("ok="));
    }

    /**
     * call the specified msisdn, return immediately. It returns
     * a job id which can be used to wait for the call to 
     * succeed (CAPI_EVENT_DONE) or fail (CAPI_EVENT_HANGUP)
     * @param  msisdn the misdn to call
     * @returns  a job id for the calling task
     */
    synchronized public int call_nonblock ( String msisdn) throws IOException, CommandFailedException
    {
	sendCommand("call_nonblock "+msisdn);
	return Integer.parseInt(readAnswer("ok="));
    }

    /**
     * put a file on the server, for subsequent play() or
     * fax() calls.
     * @param filename The filename of the file to send
     * @param destname How the file should be named on the server. Must not
     *                 contain the characters '/' or '\\'.
     */
    synchronized public void put (String filename, String destname) throws IOException, CommandFailedException
    {
	FileInputStream file = new FileInputStream(filename);
	sendCommand("put "+destname+" "+file.available());
	byte [] array= new byte[file.available()];
	int ret = file.read(array);
	int t;
	for(t=0;t<array.length;t++)
	{
	    /* FIXME: some %!@ing Java-REs do a 8bit->7bit conversion
	       here */
	    out.write(array[t]);
	}
	out.flush();
	readAnswer("ok");
    }

    /**
     * get a file from the server. (E.g. one that record() has created)
     * @param filename The filename of the file to receive. Must not
     *                 contain the characters '/' or '\\'.
     * @param destname The destination file.
     */
    synchronized public void get (String filename, String destname) throws IOException, CommandFailedException
    {
	throw new CommandFailedException("not implemented yet!");
    }

    /** 
     * remove a file from the server.
     * @param filename The filename of the file to remove. Like in put(), the
     *                 filename must not contain the characters '/' and '\\'.
     */
    synchronized public void del (String filename) throws IOException, CommandFailedException
    {
	sendCommand("del "+filename);
	readAnswer("ok");
    }

    /** 
     * generate one or more DTMFs.
     * @param notes A String of DTMFs to generate. Allowed characters are
     *              '0'-'9','*','#'.
     */
    synchronized public void beep (String notes) throws IOException, CommandFailedException
    {
	sendCommand("beep "+notes);
	readAnswer("ok");
    }

    /** 
     * put existing call on hold.
     *	    @returns true on success, false on failure (e.g. if the caller
     *                hung up already)
     *
     */
    synchronized public boolean hold() throws IOException, CommandFailedException
    {
	sendCommand("hold");
	return Integer.parseInt(readAnswer("ok="))!=0;
    }
 
    /**
     * retrieve call on hold.
     *	    @returns true on success, false on failure (e.g. if the caller
     *                hung up already)
     *
     */
    synchronized public boolean retrieve() throws IOException, CommandFailedException
    {
	sendCommand("retrieve");
	return Integer.parseInt(readAnswer("ok="))!=0;
    }

    /**
     * do a supplemental service command, for example "3conf".
     *	    @param command the command to send, e.g. "3conf".
     *	    @returns true on success, false on failure.
     */
    synchronized public boolean supplemental(String command) throws IOException, CommandFailedException
    {
	sendCommand("supplemental "+command);
	return Integer.parseInt(readAnswer("ok="))!=0;
    }

    /** 
     * send a sound file into a line.  This call does not block,
     * but rather returns a job id which can be used
     * for waiting for the end of the sound file.
     *	    @param  filename the filename of the soundfile
     *	    @returns  a job id. An event with this job id will be
     *	              generated when the end of the audio data is 
     *	              reached.
     */
    synchronized public int play (String filename) throws IOException, CommandFailedException
    {
	sendCommand("play "+filename);
	return Integer.parseInt(readAnswer("ok="));
    }

    /** 
     * use tts functionality to speak a text.  This call does not block,
     * but rather returns a job id which can be used
     * for waiting for the end of the speaking process.
     *	    @param  text the text to speech
     *	    @returns  a job id. An event with this job id will be
     *	              generated when the end of the audio data is 
     *	              reached.
     */
    synchronized public int speak (String text) throws IOException, CommandFailedException
    {
	sendCommand("speak \""+text+"\"");
	return Integer.parseInt(readAnswer("ok="));
    }

    /**
     * record the other end's voice into buf. This call does not block,
     * but rather returns a job id which can be used for waiting for
     * the end of an recording. Notice that other events (especially
     * DIALTONE and HANGUP) will still be sent.
     *	    @param filename the filename to store the recorded file in. this file will be put
     *                      on the server, not the local directory!
     *	    @param seconds how many seconds to record. The recording will be shorter if the
     *                     other end hangs up.
     *	    @returns a job id, which can be used to wait for the end
     *	     of the recording
     */
    synchronized public int record (String filename, int seconds) throws IOException, CommandFailedException
    {
	sendCommand("record \""+filename+"\" "+seconds);
	return Integer.parseInt(readAnswer("ok="));
    }

    /**
     * send a fax through the line. This call blocks until the fax
     * has been transmitted. You should have transferred the file
     * to fax on the server with put() before calling this function.
     * The fax file can either be SFF, Postscript or HTML. Depending
     * on your capiserver, Postscript or HTML may not be supported.
     * @param msisdn the msisdn of the fax machine
     * @param filename the filename of the fax file (on the server)
     * @returns true on success, false on failure
     */
    synchronized public boolean fax (String msisdn, String filename) throws IOException, CommandFailedException
    {
	sendCommand("fax "+msisdn+" "+filename);
	return Integer.parseInt(readAnswer("ok="))==1;
    }

    /**
     * stop a running job. Most likely you will want to stop jobs
     * which play soundfiles (for barge-in functionality)
     * @param jobid the job to stop
     */
    synchronized public void stop (int jobid) throws IOException, CommandFailedException
    {
	sendCommand("stop "+jobid);
	readAnswer("ok");
    }

    /**
     * wait for something to happen. This might be a hangup by the other
     * end, a job that finished, an incoming call etc.
     *	    @param timeout the time to listen. If nothing happened while
     *                we were waiting, return a TIMEOUT event. If timeout=0, 
     *                wait forever.
     */
    synchronized public CapiEvent listen ( int timeout) throws IOException, CommandFailedException
    {
	sendCommand("listen "+timeout);
	CapiEvent event = new CapiEvent();
	String s;
	s=readAnswer("msisdn=");
	event.msisdn = s;
	s=readAnswer("called_msisdn=");
	event.called_msisdn = s;
	s=readAnswer("id=");
	event.id = Integer.parseInt(s);
	s=readAnswer("type=");
	event.type = Integer.parseInt(s);
	s=readAnswer("jobid=");
	event.jobid = Integer.parseInt(s);
	s=readAnswer("tone=");
	switch(Integer.parseInt(s)) {
		case 35: event.tone = '#';
		         break;
		case 42: event.tone = '*';
		         break;
		case 48: event.tone = '0';
		         break;
		case 49: event.tone = '1';
		         break;
		case 50: event.tone = '2';
		         break;
		case 51: event.tone = '3';
		         break;
		case 52: event.tone = '4';
		         break;
		case 53: event.tone = '5';
		         break;
		case 54: event.tone = '6';
		         break;
		case 55: event.tone = '7';
		         break;
		case 56: event.tone = '8';
		         break;
		case 57: event.tone = '9';
		         break;
	}
	s=readAnswer("reason=");
	event.reason = Integer.parseInt(s);
	s=readAnswer("ok");
	return event;
    }

    /**
     * answer an incoming call. 
     *	    @param event the INCOMING event to answer
     *	    @returns true on success, false on failure (e.g. if the caller
     *                hung up or another thread has answered the Call already)
     */
    synchronized public boolean answer (CapiEvent event) throws IOException, CommandFailedException
    {
	sendCommand("answer "+event.id);
	return Integer.parseInt(readAnswer("ok="))!=0;
    }

    /**
     * reject an incoming call.
     *	    @param event the INCOMING event to hang up on
     */
    synchronized public void reject (CapiEvent event) throws IOException, CommandFailedException
    {
	sendCommand("reject "+event.id);
        readAnswer("ok");
    }

    /**
     * ignore an incoming call. This is different from reject in that
     * the call keeps ringing, so that other devices may answer it.
     *	    @param event the INCOMING event to ignore
     */
    synchronized public void ignore (CapiEvent event) throws IOException, CommandFailedException
    {
	sendCommand("ignore "+event.id);
        readAnswer("ok");
    }
    
    /**
     * forward an incoming call / a connection
     *	    @param msisdn the new msisdn
     *	    @returns true on success, false on failure (e.g. if the caller
     *                hung up already)
     */
    synchronized public boolean forward ( String msisdn)  throws IOException, CommandFailedException
    {
	sendCommand("forward "+msisdn);
	return Integer.parseInt(readAnswer("ok="))!=0;
    }

    /**
     * hangup on a connection. This call blocks until the connection is correctly
     * terminated.
     */
    synchronized public void hangup () throws IOException, CommandFailedException
    {
	sendCommand("hangup");
	readAnswer("ok");
    }

    /**
     * merge the audio datas of two connections (or connections-to-be).
     * @param capi1 SimpleCapi object corresponding to the first connection
     * @param capi2 SimpleCapi object corresponding to the second connection
     */
    synchronized public void connect(SimpleCapi capi1, SimpleCapi capi2) throws IOException, CommandFailedException
    {
	sendCommand("connect "+capi1.line+" "+capi2.line);
	readAnswer("ok");
    }

    /**
     * close the connection to the capiserver. Also called
     * by the class's finalize method.
     */
    synchronized public void close()
    {
	out.println("close");
	this.out.flush();
	try{
	    readAnswer("ok");
	}catch(Exception e){}
    }

    /* internal routines */

    private void sendCommand(String s) throws IOException
    {
//	System.out.println("send>"+s);
	this.out.println(s);
	this.out.flush();
    }

    private String readAnswer(String header) throws IOException, CommandFailedException
    {
	String s;
	s=in.readLine();
	if(s==null)
	   throw new IOException("Got NULL while reading from socket");
//      System.out.println("receive>"+s);
	if(!header.equalsIgnoreCase(s.substring(0,header.length())))
	{
	    if(s.startsWith("error: "))
		throw new CommandFailedException(s.substring("error: ".length()));
	    throw new IOException("while reading from socket: got, \""+s+"\" but expected \""+header+"*\"");
	}
	return s.substring(header.length());
    }

    public void finalize ()
    {
	close();
    }
    
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out = null;
    private int line;
}
