/*
 * Asterisk -- A telephony toolkit for Linux.
 *
 * Asterisk Gateway Interface AddOns
 * 
 * Manuel Guesdon <mguesdon@oxymium.net>
 *
 * This program is free software, distributed under the terms of
 * the GNU General Public License
 */

#include <sys/types.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <asterisk/file.h>
#include <asterisk/logger.h>
#include <asterisk/channel.h>
#include <asterisk/pbx.h>
#include <asterisk/module.h>
#include <asterisk/astdb.h>
#include <math.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/poll.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <asterisk/cli.h>
#include <asterisk/logger.h>
#include <asterisk/options.h>
#include <asterisk/image.h>
#include <asterisk/say.h>
#include <asterisk/app.h>
#include <asterisk/dsp.h>
#include <asterisk/musiconhold.h>
#include <asterisk/manager.h>
#include <asterisk/utils.h>
#include <asterisk/lock.h>
#include <asterisk/agi.h>
#include "../asterisk.h"
#include "../astconf.h"

#define MAX_ARGS 128
#define MAX_COMMANDS 128

#define RETRY	3
/* Recycle some stuff from the CLI interface */
#define fdprintf agi_debug_cli

static char *tdesc = "Oxymium Asterisk Gateway Interface (OxymiumAGI)";

static char *app = "OxymiumAGI";

static char *eapp = "OxymiumEAGI";

static char *deadapp = "OxymiumDeadAGI";

static char *synopsis = "Executes an AGI compliant application";
static char *esynopsis = "Executes an EAGI compliant application";
static char *deadsynopsis = "Executes AGI on a hungup channel";

static char *descrip =
"  [E|Dead]AGI(command|args): Executes an Asterisk Gateway Interface compliant\n"
"program on a channel. AGI allows Asterisk to launch external programs\n"
"written in any language to control a telephony channel, play audio,\n"
"read DTMF digits, etc. by communicating with the AGI protocol on stdin\n"
"and stdout.\n"
"Returns -1 on hangup (except for DeadAGI) or if application requested\n"
" hangup, or 0 on non-hangup exit. \n"
"Using 'EAGI' provides enhanced AGI, with incoming audio available out of band"
"on file descriptor 3\n\n"
"Use the CLI command 'show agi' to list available agi commands\n";

static char usage_putsoundfile[] =
" Usage: PUT SOUNDFILE <soundfile> <size>\n"
"	Put sound file named <soundfile> of size <size> to path_to_sounds/context/<soundfile> \n"
"       Returns:\n"
"          200 result=0 (Wait for data) if agi can send base64 72 characters per line data\n"
"          200 result=-11 (Cound not create directory) file=<soundfile> context=<ChannelContext> otherwise\n"
"          200 result=-10 (Cound not create file) file=<soundfile> context=<ChannelContext> if we can't create file\n"
"       After user has sent data, returns:\n"
"         200 result=0 if all is OK\n"
"       else returns:\n"
"         200 result=-9 (Not enough data) is there's not enough data\n"
"         200 result=-8 (Bad line) is line is has not 72 characters + newline (except for last line)\n"
"         200 result=-7 (Too much data) is agi send too much data\n"
"         200 result=-6 (Problem writing to file) if we can't write more data (disk full,..)\n"
" \n";


static char usage_getsoundfile[] =
" Usage: GET SOUNDFILE <soundfile>\n"
"	Get sound file named <soundfile>i from path_to_sounds/context/<soundfile> \n"
"       Returns:\n"
"          200 result=-1 (File doesn't exist) file=<soundfile> context=<ChannelContext> if we can't get file size\n"
"          200 result=-2 (Could not read file) file=<soundfile> context=<ChannelContext> if we can't read the file\n"
"          200 result=0 (no data following) size=0 if the file is empty\n"
"          200 result=0 (data following) size=<FileSize> if it's OK. Base64 72 characters per line will be sent after\n"
"       If something goes wrong sending data, it will return\n"
"          200 result=-3 (Coud not read enough data)\n"
"\n";

static char usage_isexistingsoundfile[] =
" Usage: ISEXISTING SOUNDFILE <soundfile>\n"
"	if sound file exists at path path_to_sounds/context/<soundfile> \n"
"	  returns:  200 result=0 (File exists) size=<FileSize>\n"
"       else returns: 200 result=-1 (File doesn't exist) file=<soundfile> context=<ChannelContext>\n"
"\n";

static char debug_usage[] = 
"Usage: agi debug\n"
"       Enables dumping of AGI transactions for debugging purposes\n";

static char no_debug_usage[] = 
"Usage: agi no debug\n"
"       Disables dumping of AGI transactions for debugging purposes\n";


static int agidebug = 0;

STANDARD_LOCAL_USER;

LOCAL_USER_DECL;


static void agi_debug_cli(int fd, char *fmt, ...)
{
	char *stuff;
	int res = 0;

	va_list ap;
	va_start(ap, fmt);
	res = vasprintf(&stuff, fmt, ap);
	va_end(ap);
	if (res == -1) {
		ast_log(LOG_ERROR, "Out of memory\n");
	} else {
		if (agidebug)
			ast_verbose("OxymiumAGI Tx >> %s", stuff);
		ast_carefulwrite(fd, stuff, strlen(stuff), 100);
		free(stuff);
	}
}


#define SizeToBase64Size(l) (((l) + 2) / 3 * 4 + 1)// Length rounded to 4 byte block.
#define Base64SizeToDataSize(l)  ((l) / 4  * 3 + 1)    // Length rounded to 3 byte octet.

// you must free the returned pointer
static char* soundFfileNamePath(const char* filename,const char* context)
{
  char *path;
  int path_size = 0;
  char tmp[AST_CONFIG_MAX_PATH]="";

  snprintf(tmp, sizeof(tmp), "%s/%s", ast_config_AST_VAR_DIR, "sounds");
  path_size = strlen(tmp) + strlen(context) + strlen(filename) + 10;
  path = malloc(path_size);
  if (path)
    {
      if (filename[0] == '/') 
	snprintf(path, path_size, "%s/%s%s", tmp,context,filename);
      else
	snprintf(path, path_size, "%s/%s/%s", tmp, context,filename);
    }
  return path;
};

static const char* next_sep(const char *path)
{
  while (*path)
    {
      if (*path == '/' || *path == '\\')
	return path;
      else
	path++;
    };
  return NULL;
}

int mkdirhierforfilepath(const char* path)
{
  const char *next, *prev;
  char buf[2048];
  struct stat st;
  
  prev = path;
  while ((next = next_sep(prev)))
    {
      if (next>path)//skip 1st /
	{
	  strncpy(buf, path, next - path);
	  buf[next - path] = '\0';
	  // if parent dir doesn't exist yet create it
	  if (stat(buf, &st)==0)
	    {
	      if (agidebug)
		ast_verbose("OxymiumAGI: %s exists\n",buf);
	    }
	  else
	    {
	      if (agidebug)
		ast_verbose("OxymiumAGI: Create %s\n",buf);
	      if (mkdir(buf,777)!=0)
		{
		  if (agidebug)
		    ast_verbose("OxymiumAGI: Can't create: %s\n",buf);
		  return -1;
		};
	    };
	};
      prev = next + 1;
    }
  return 0;
}

	
static int handle_putsoundfile(struct ast_channel *chan, AGI *agi, int argc, char *argv[])
{
  int res=0;
  long size = 0;
  char* path=NULL;
  FILE* outFile=NULL;
	
        if (argc != 4)
		return RESULT_SHOWUSAGE;
	if ((sscanf(argv[3], "%ld", &size) != 1))
		return RESULT_SHOWUSAGE;

	if (agidebug)
	  ast_verbose("OxymiumAGI PutSoundFile: Have to read %ld bytes\n",size);
	
	path=soundFfileNamePath(argv[2],chan->context);

	if (agidebug)
	  ast_verbose("OxymiumAGI PutSoundFile: File: %s\n",path);

	if (mkdirhierforfilepath(path)!=0)
	  {
	    fdprintf(agi->fd, "200 result=-11 (Cound not create directory) file=%s context=%s\n",argv[2],chan->context);
	    res=-11;	    
	  }
	else if (!(outFile = fopen(path, "wb")))
	  {
	    ast_cli(agi->fd, "Could not create file '%s'\n", path);
	    fdprintf(agi->fd, "200 result=-10 (Cound not create file) file=%s context=%s\n",argv[2],chan->context);
	    res=-10;	    
	  }
	else
	  {
	    char bufferIn[74];
	    unsigned char bufferOut[74];
	    long written_byte_size=0;
	    int lineCount=0;

	    fdprintf(agi->fd, "200 result=0 (Wait for data)\n", 0);

	    while(res==0 && written_byte_size<size)
	      {
		int readen_char_count=0;

		// Number of characters to read (includuing newline)
		int char_count=SizeToBase64Size(size-written_byte_size);

		if (char_count>72) // complete line
		  char_count=73; //complete line + newline
		else
		  char_count++; // partial line + newline

		if (agidebug)
		  ast_verbose("OxymiumAGI PutSoundFile: Try read %d characters\n",char_count);

		readen_char_count=read(agi->ctrl,bufferIn,char_count);

		if (agidebug)
		  ast_verbose("OxymiumAGI PutSoundFile: readen %d characters\n",readen_char_count);
		if (readen_char_count<0 || readen_char_count<(char_count-3))
		  {
		    ast_verbose("OxymiumAGI PutSoundFile: readen %d characters < %d\n",readen_char_count,char_count);
		    fdprintf(agi->fd, "200 result=-9 (Not enough data)\n");
		    res=-9;
		  }
		else
		  {
		    bufferIn[readen_char_count]=0;
		    if (agidebug)
		      ast_verbose("OxymiumAGI PutSoundFile: line %d:\n%s\n",lineCount,bufferIn);
		    if (bufferIn[readen_char_count-1]!='\n')
		      {
			ast_verbose("OxymiumAGI PutSoundFile: bad line (no newline at end)\n");
			fdprintf(agi->fd, "200 result=-8 (Bad line)\n");
			res=-8;
		      }
		    else
		      {
			int decoded_size=ast_base64decode(bufferOut,bufferIn,Base64SizeToDataSize(readen_char_count-1));
			if (agidebug)
			  {
			    ast_verbose("OxymiumAGI PutSoundFile: line index=%d decoded %d\n",lineCount,decoded_size);
			    ast_verbose("OxymiumAGI PutSoundFile: Total Readen %ld\n",written_byte_size);
			  };
			if (written_byte_size+decoded_size>size+2)
			  {
			    ast_verbose("OxymiumAGI PutSoundFile: too much data %ld>%ld\n",written_byte_size+decoded_size,size);
			    fdprintf(agi->fd, "200 result=-7 (Too much data)\n");
			    res=-7;
			  }
			else if (written_byte_size+decoded_size>size)
			  decoded_size=size-written_byte_size;

			if (agidebug)
			  ast_verbose("OxymiumAGI PutSoundFile: write %d\n",decoded_size);

			written_byte_size+=decoded_size;
			size_t written=fwrite(bufferOut,1,decoded_size,outFile);

			if (agidebug)
			  ast_verbose("OxymiumAGI PutSoundFile: written %d\n",written);

			if (written!=decoded_size)
			  {
			    ast_verbose("OxymiumAGI PutSoundFile: too much data %ld>%ld\n",written_byte_size+decoded_size,size);
			    fdprintf(agi->fd, "200 result=-6 (Problem writing to file)\n");
			    res=-6;
			  };
		      };
		  };		
		lineCount++;
	      };
	    fclose(outFile);
	  };
	if (res==0)
	  {
	    fdprintf(agi->fd, "200 result=%d\n",res);
	  };
	free(path);
	return RESULT_SUCCESS;
}

static long get_file_size(const char* path)
{
  struct stat buf;
  if (stat(path,&buf)==0)
    {
      return (long)buf.st_size;
    }
  else
    return -1;
};

static int handle_isexistingsoundfile(struct ast_channel *chan, AGI *agi, int argc, char *argv[])
{
  int res=0;
  long size = 0;
  char* path=NULL;

  if (argc != 3)
    return RESULT_SHOWUSAGE;
  
  path=soundFfileNamePath(argv[2],chan->context);

  if (agidebug)
    ast_verbose("OxymiumAGI IsExistingSoundFile: File: %s\n",path);

  size=get_file_size(path);

  if (size<0)
    {
      fdprintf(agi->fd, "200 result=-1 (File doesn't exist) file=%d context=%s\n",argv[2],chan->context);
      res=-1;
    }
  else
    {
      fdprintf(agi->fd, "200 result=0 (File exists) size=%ld\n",size);
      res=0;
    }
  free(path);
  return RESULT_SUCCESS;
}

static int handle_getsoundfile(struct ast_channel *chan, AGI *agi, int argc, char *argv[])
{
  int res=0;
	long size = 0;
	char* path=NULL;
	
	if (argc != 3)
		return RESULT_SHOWUSAGE;

	path=soundFfileNamePath(argv[2],chan->context);
	if (agidebug)
	  ast_verbose("OxymiumAGI GetSoundFile: File: %s\n",path);

	size=get_file_size(path);

	if (size<0)
	  {
	    fdprintf(agi->fd, "200 result=-1 (file doesn't exist) file=%d context=%s\n",argv[2],chan->context);
	    res=-1;
	  }
	else if (size==0)
	  {
	    fdprintf(agi->fd, "200 result=0 (no data following) size=0\n");
	    res=0;
	  }
	else
	  {
	    FILE* inFile=NULL;
	    if (agidebug)
	      ast_verbose("OxymiumAGI GetSoundFile: Have to write %ld bytes\n",size);
	
	    if (!(inFile = fopen(path, "rb")))
	      {
		ast_cli(agi->fd, "Could not read file '%s'\n", path);
		fdprintf(agi->fd, "200 result=-2 (Could not read file) file=%s context=%s\n",argv[2],chan->context);
		res=-2;
	      }
	    else
	      {
		unsigned char bufferIn[74];
		char bufferOut[74];
		long readen_size=0;
		int lineCount=0;

		fdprintf(agi->fd, "200 result=0 (data following) size=%ld\n",size);
		res=0;

		while(res==0 && readen_size<size)
		  {
		    int readen_byte_size=0;

		    // Number of bytes to read
		    int byte_size=54; //72 characters line
		    
		    if (readen_size+byte_size>size) // not complete line
		      byte_size=size-readen_size;

		    if (agidebug)
		      ast_verbose("OxymiumAGI GetSoundFile: Try read %d bytes\n",byte_size);

		    readen_byte_size=fread(bufferIn,1,byte_size,inFile);

		    if (agidebug)
		      ast_verbose("OxymiumAGI GetSoundFile: readen %d bytes\n",readen_byte_size);

		    if (readen_byte_size<byte_size)
		      {
			ast_verbose("OxymiumAGI GetSoundFile: readen %d bytes < %d\n",readen_byte_size,byte_size);
			fdprintf(agi->fd, "200 result=-3 (Coud not read enough data)\n");
			res=-3;
		      }
		    else
		      {
			int encoded_char_count=ast_base64encode(bufferOut,bufferIn,readen_byte_size,74);
			if (agidebug)
			  ast_verbose("OxymiumAGI GetSoundFile: line index=%d encoded %d\n",lineCount,encoded_char_count);
			bufferOut[encoded_char_count]=0;
			if (agidebug)
			  ast_verbose("OxymiumAGI GetSoundFile: line %d:\n%s\n",lineCount,bufferOut);

			fdprintf(agi->fd, "%s\n",bufferOut);

			readen_size+=readen_byte_size;

			if (agidebug)
			  ast_verbose("OxymiumAGI GetSoundFile: readen_size=%ld\n",readen_size);
		      };
		    lineCount++;
		  };		
		fclose(inFile);
	      };
	  };
	free(path);
	return RESULT_SUCCESS;
}

static int agi_do_debug(int fd, int argc, char *argv[])
{
	if (argc != 2)
		return RESULT_SHOWUSAGE;
	agidebug = 1;
	ast_cli(fd, "AGI Debugging Enabled\n");
	return RESULT_SUCCESS;
}

static int agi_no_debug(int fd, int argc, char *argv[])
{
	if (argc != 3)
		return RESULT_SHOWUSAGE;
	agidebug = 0;
	ast_cli(fd, "AGI Debugging Disabled\n");
	return RESULT_SUCCESS;
}

static struct ast_cli_entry  cli_debug =
	{ { "oxymiumagi", "debug", NULL }, agi_do_debug, "Enable Oxymium AGI debugging", debug_usage };

static struct ast_cli_entry  cli_no_debug =
	{ { "oxymiumagi", "no", "debug", NULL }, agi_no_debug, "Disable Oxymium AGI debugging", no_debug_usage };

static agi_command commands[3] = {
	{ { "put", "soundfile", NULL }, handle_putsoundfile, "Put Sound File", usage_putsoundfile },
	{ { "get", "soundfile", NULL }, handle_getsoundfile, "Get Sound File", usage_getsoundfile },
	{ { "isexisting", "soundfile", NULL }, handle_isexistingsoundfile, "Is existing Sound File", usage_isexistingsoundfile }
};

int unload_module(void)
{
	STANDARD_HANGUP_LOCALUSERS;
	agi_unregister(&commands[2]);
	agi_unregister(&commands[1]);
	agi_unregister(&commands[0]);
	ast_cli_unregister(&cli_debug);
	ast_cli_unregister(&cli_no_debug);

	ast_unregister_application(eapp);
	ast_unregister_application(deadapp);
	return ast_unregister_application(app);
}

int load_module(void)
{
	ast_cli_register(&cli_debug);
	ast_cli_register(&cli_no_debug);

	agi_register(&commands[2]);
	agi_register(&commands[1]);
	agi_register(&commands[0]);
	ast_register_application(deadapp, NULL /*deadagi_exec*/, deadsynopsis, descrip);
	ast_register_application(eapp, NULL/*eagi_exec*/, esynopsis, descrip);
	return ast_register_application(app, NULL/*agi_exec*/, synopsis, descrip);
}

char *description(void)
{
	return tdesc;
}

int usecount(void)
{
	int res;
	STANDARD_USECOUNT(res);
	return res;
}

char *key()
{
	return ASTERISK_GPL_KEY;
}

