/*
 * File: capi.c
 *
 * Copyright 2002 Jorge Arellano Cid <jcid@dillo.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.
 */

/*
 * Cache API
 * This is the module that manages the cache and starts the CCC chains
 * to get the requests served. Kind of a broker.
 */

#include "msg.h"
#include "capi.h"
#include "IO/Url.h"
#include "chain.h"
#include "list.h"
#include "interface.h"
#include "history.h"
#include "nav.h"

/* for testing dpi chat */
#include "bookmark.h"

#define DEBUG_LEVEL 5
#include "debug.h"

typedef struct {
   DilloWeb *web;        /* Necessary for <dpi cmd='open_url' ...> */
   void *bw;
   gchar *server;
   gchar *datastr;
   gint SockFD;
   gint Flags;
   gint DpiPipe[2];
   ChainLink *InfoSend;
   ChainLink *InfoRecv;
   ChainLink *InfoPipe;

   gint Ref;
} dpi_conn_t;

/* Flags for conn */
enum {
   PENDING = 1,
   TIMEOUT = 2   /* unused */
};

/*
 * Local data
 */
/* Data list for active dpi connections */
static dpi_conn_t **DpiConn = NULL;
static gint DpiConnSize;
static gint DpiConnMax = 4;

/* ID for the timeout function that waits for dpid to start */
static guint dpi_conn_timeout_id = 0;

/*
 * Forward declarations
 */
void a_Capi_ccc(int Op, int Branch, int Dir, ChainLink *Info,
                void *Data1, void *Data2);


/* ------------------------------------------------------------------------- */

/*
 * Create a new connection data structure
 */
static dpi_conn_t *
 Capi_dpi_conn_new(DilloWeb *web, void *bw, char *server, gchar *datastr)
{
   dpi_conn_t *conn;

   conn = g_new(dpi_conn_t, 1);
   conn->web = web;
   conn->bw = bw;
   conn->server = g_strdup(server);
   conn->datastr = g_strdup(datastr);
   conn->SockFD = -1;
   conn->Flags = PENDING;
   conn->InfoSend = a_Chain_new();
   conn->InfoRecv = NULL;   /* will be set later */
   conn->InfoPipe = NULL;   /* may be set later */
   conn->Ref = 0;           /* Reference count */
   return conn;
}

/*
 * Increment the reference count and add to the list if not present
 */
static void Capi_dpi_conn_ref(dpi_conn_t *conn)
{
   if (++conn->Ref == 1) {
      /* add the connection data to list */
      a_List_add(DpiConn, DpiConnSize, DpiConnMax);
      DpiConn[DpiConnSize] = conn;
      DpiConnSize++;
   }
}

/*
 * Decrement the reference count (and remove from list when zero)
 */
static void Capi_dpi_conn_unref(dpi_conn_t *conn)
{
   gint i, j;

   --conn->Ref;
   if (conn->Ref == 0) {
      for (i = 0; i < DpiConnSize; ++i)
         if (DpiConn[i] == conn) {
            /* remove conn preserving the list order */
            for (j = i; j + 1 < DpiConnSize; ++j)
               DpiConn[j] = DpiConn[j + 1];
            --DpiConnSize;
            g_free(conn->web);
            g_free(conn->server);
            g_free(conn->datastr);
            g_free(conn);
            break;
         }
   }
}

/*
 * Find connection data by server
 */
static dpi_conn_t *Capi_dpi_conn_find(gchar *server)
{
   gint i;

   for (i = 0; i < DpiConnSize; ++i)
      if (strcmp(server, DpiConn[i]->server) == 0)
         return DpiConn[i];

   return NULL;
}

/*
 * Resume connections that were waiting for dpid to start.
 */
static void Capi_dpi_conn_resume(void)
{
   gint i;
   DataBuf *dbuf;

   for (i = 0; i < DpiConnSize; ++i)
      if (DpiConn[i]->Flags & PENDING) {
         dpi_conn_t *conn = DpiConn[i];
         dbuf = a_Chain_dbuf_new(conn->datastr, strlen(conn->datastr), 0);
         a_Capi_ccc(OpSend, 1, BCK, conn->InfoSend, dbuf, NULL);
         g_free(dbuf);
         conn->Flags &= ~PENDING;
      }
}

/*
 * Test dpid and resume connections if it already started.
 */
static gint Capi_dpi_conn_timeout(gpointer data)
{
   static gint try = 0;
   gint i;

   ++try;
   DEBUG_MSG(5, "Capi_dpi_conn_timeout:: try %d\n", try);

   for (i = 0; i < DpiConnSize; ++i)
      if (DpiConn[i]->Flags & PENDING) {
         dpi_conn_t *conn = DpiConn[i];
         a_Chain_bcb(OpStart, conn->InfoSend, conn->server, NULL);
         break;
      }

   for (i = 0; i < DpiConnSize; ++i)
      if (DpiConn[i]->Flags & PENDING)
         return TRUE;
   try = 0;
   dpi_conn_timeout_id = 0;
   return FALSE;
}
   
/* ------------------------------------------------------------------------- */

/*
 * Safety test: only allow dpi-urls from dpi-generated pages.
 */
gint Capi_verify_dpi_url_request(DilloWeb *web)
{
   DilloUrl *referer = NULL;
   gint allow = FALSE;

   /* test POST and GET */
   if (strchr(URL_STR(web->url), '?') || URL_DATA_(web->url)) {
      /* safety measure: only allow dpi requests from dpi-generated urls */
      if (a_Nav_stack_size(web->bw)) {
         referer = a_History_get_url(NAV_TOP(web->bw));
         if (g_strncasecmp(URL_STR(referer), "dpi:/", 5) == 0)
            allow = TRUE;
      }
   } else {
      allow = TRUE;
   }

   if (!allow) {
      MSG("Capi_verify_dpi_url_request: Permission Denied!\n");
      MSG("  URL_STR : %s\n", URL_STR(web->url));
      if (URL_DATA_(web->url))
         MSG("  URL_DATA: %s\n", URL_DATA(web->url));
   }
   return allow;
}

/*
 * Most used function.
 * todo: clean up the ad-hoc bindings with a clean API.
 */
gint a_Capi_open_url(DilloWeb *web, CA_Callback_t Call, void *CbData)
{
   gint sz;
   char *cmd, *server, *p;
   char *url_str = URL_STR(web->url);

   _MSG("  a_Capi_open_url:: web->Image=%p\n", web->Image);

   if (g_strncasecmp(url_str, "dpi:/", 5) == 0 && !Call) {
      /* dpi request! */
      if (Capi_verify_dpi_url_request(web)) {
         /* Get the server's name */
         p = strchr(url_str + 5, '/');
         server = p ? g_strndup(url_str + 5, p - url_str - 5) : g_strdup("?");
         if (strcmp(server, "bm") == 0) {
            g_free(server);
            server = g_strdup("bookmarks");
         }

         /* Send dpip command */
         cmd = g_strdup_printf("<dpi cmd='open_url' url='%s'>", url_str);
         a_Capi_dpi_send_cmd(web, web->bw, cmd, server, 1);
         g_free(cmd);
         g_free(server);
      }

   } else if (g_strncasecmp(url_str, "ftp:/", 5) == 0 && !Call &&
              (a_Capi_url_read(web->url, &sz) == NULL ||
               (URL_FLAGS(web->url) & URL_E2EReload)) ) {
      cmd = g_strdup_printf("<dpi cmd='open_url' url='%s'>", url_str);
      a_Capi_dpi_send_cmd(web, web->bw, cmd, "ftp", 1);
      g_free(cmd);

   } else if (g_strncasecmp(url_str, "https:/", 7) == 0 && !Call &&
              (a_Capi_url_read(web->url, &sz) == NULL ||
               (URL_FLAGS(web->url) & URL_E2EReload)) ) {
      cmd = g_strdup_printf("<dpi cmd='open_url' url='%s'>", url_str);
      a_Capi_dpi_send_cmd(web, web->bw, cmd, "https", 1);
      g_free(cmd);

   } else {
      return a_Cache_open_url(web, Call, CbData);
   }

   return 0;
}

/*
 * This one is used by DPI (the interface is not very well defined yet).
 * Code flow gets here when a dpi program issues the "start_send_page" cmd.
 */
static gint Capi_open_url_raw(DilloWeb *web, gchar *url_str)
{
   DilloWeb *Web;
   DilloUrl *Url;

   /* The server is sending a page, set the end-to-end-reload flag
    * to remove any cached version of the page */
   Url = a_Url_new(url_str, NULL, URL_E2EReload, 0);

   /* Create a new web structure 'Web' because the original URL
    * inside 'web' may have changed (we keep the original flags) */
   Web = a_Web_new(Url);
   Web->bw = web->bw;
   Web->flags |= web->flags;
   Web->Image = web->Image;
   web->Image = NULL;         /* avoid a double free */
   a_Cache_open_url(Web, NULL, NULL);
   a_Url_free(Url);

   _MSG("  Capi_open_url_raw:: Web->Image=%p\n", Web->Image);
   _MSG("  Capi_open_url_raw:: web->Image=%p\n", web->Image);

   /* todo: return a meaningful value */
   return 0;
}

/*
 * Retrieve a char* to the cache contents for URL.
 */
char *a_Capi_url_read(const DilloUrl *Url, gint *Size)
{
   return a_Cache_url_read(Url, Size);
}

/*
 * Send a dpi cmd.
 * (For instance: add_bookmark, open_url, send_preferences, ...)
 */
gint a_Capi_dpi_send_cmd(DilloWeb *web, void *bw, char *cmd, char *server,
                         gint flags)
{
   dpi_conn_t *conn;
   DataBuf *dbuf;

   //**/sleep(1);
   //**/a_Interface_msg(bw, cmd);

   if (flags & 1) {
      /* open a new connection to server */

      /* Create a new connection data struct and add it to the list */
      conn = Capi_dpi_conn_new(web, bw, server, cmd);
      Capi_dpi_conn_ref(conn);
      /* start the CCC operations */
      a_Capi_ccc(OpStart, 1, BCK, conn->InfoSend, conn, server);

   } else {
      /* Re-use an open connection */
      conn = Capi_dpi_conn_find(server);
      if (conn) {
         /* found */
         dbuf = a_Chain_dbuf_new(cmd, strlen(cmd), 0);
         a_Capi_ccc(OpSend, 1, BCK, conn->InfoSend, dbuf, NULL);
         g_free(dbuf);
      } else {
         MSG(" ERROR: [a_Capi_dpi_send_cmd] No open connection found\n");
      }
   }

   return 0;
}


/*
 * CCC function for the CAPI module
 */
void a_Capi_ccc(int Op, int Branch, int Dir, ChainLink *Info,
                void *Data1, void *Data2)
{
   dpi_conn_t *conn;

   a_Chain_debug_msg("a_Capi_ccc", Op, Branch, Dir);

   if (Branch == 1) {
      if (Dir == BCK) {
         /* Command sending branch */
         switch (Op) {
         case OpStart:
            Info->LocalKey = Data1;
            a_Chain_link_new(Info, a_Capi_ccc, BCK, a_Dpi_ccc, 2, 1);
            a_Chain_bcb(OpStart, Info, Data2, NULL);
            break;
         case OpSend:
            a_Chain_bcb(OpSend, Info, Data1, NULL);
            break;
         case OpEnd:
            a_Chain_bcb(OpEnd, Info, NULL, NULL);
            Capi_dpi_conn_unref(Info->LocalKey);
            g_free(Info);
            break;
         case OpStop:
         case OpAbort:
            MSG(" Not implemented\n");
            break;
         }
      } else {  /* FWD */
         /* Command sending branch (status) */
         switch (Op) {
         case OpSend:
            if (!Data2) {
               g_warning("Capi.c: Opsend [1F] Data2 = NULL\n");
            } else if (strcmp(Data2, "SockFD") == 0) {
               /* start the receiving branch */
               dpi_conn_t *conn = Info->LocalKey;
               conn->SockFD = (gint)Data1;
               a_Capi_ccc(OpStart, 2, BCK, a_Chain_new(),Info->LocalKey, NULL);
            } else if (strcmp(Data2, "DpidOK") == 0) {
               /* send the data inmediatly! */
               Capi_dpi_conn_resume();
            } else if (strcmp(Data2, "DpidEAGAIN") == 0) {
               /* set a timeout function to retry later... */
               if (dpi_conn_timeout_id == 0)
                  dpi_conn_timeout_id = 
                     gtk_timeout_add(250, (GtkFunction) Capi_dpi_conn_timeout,
                                     NULL);
            }
            break;
         case OpStop:
         case OpAbort:
            conn = Info->LocalKey;
            if (Data1 && !strcmp(Data1, "ERR_dpid"))
               a_Interface_msg(conn->bw, "ERROR: can't start dpid daemon!");
            Capi_dpi_conn_unref(conn);
            g_free(Info);
            break;
         }
      }

   } else if (Branch == 2) {
      if (Dir == BCK) {
         /* Server listening branch (status) */
         switch (Op) {
         case OpStart:
            {
            dpi_conn_t *conn = Data1;
            Info->LocalKey = Data1;
            Capi_dpi_conn_ref(conn);
            conn->InfoRecv = Info;
            a_Chain_link_new(Info, a_Capi_ccc, BCK, a_Dpi_ccc, 3, 2);
            a_Chain_bcb(OpStart, Info, (void *)conn->SockFD, "SockFD");
            break;
            }
         case OpStop:
         case OpAbort:
            Capi_dpi_conn_unref(Info->LocalKey);
            MSG(" Not implemented\n");
            break;
         }
      } else {  /* FWD */
         /* Server listening branch */
         switch (Op) {
         case OpSend:
            conn = Info->LocalKey;
            if (strcmp(Data2, "send_status_message") == 0) {
               a_Interface_msg(conn->bw, Data1);
            } else if (strcmp(Data2, "chat") == 0) {
               a_Interface_msg(conn->bw, Data1);
               a_Bookmarks_chat_add(NULL, NULL, Data1);
            } else if (strcmp(Data2, "start_send_page") == 0) {
               /* start the pipe-writing chain */
               a_Capi_ccc(OpStart, 3, BCK, a_Chain_new(), Info->LocalKey,NULL);
               /* let the dpi know the reading end of the pipe */
               a_Chain_bcb(OpSend, Info, (void *)conn->DpiPipe[0], NULL);
               /* start the pipe-reading chain */
               Capi_open_url_raw(conn->web, Data1);
            } else if (strcmp(Data2, "send_page_2eof") == 0) {
               a_Capi_ccc(OpSend, 3, BCK, conn->InfoPipe, Data1, NULL);
            } else if (strcmp(Data2, "reload_request") == 0) {
               a_Nav_reload(conn->bw);
            }
            break;
         case OpEnd:
            {
            dpi_conn_t *conn = Info->LocalKey;
            a_Chain_del_link(Info, BCK);
            conn->InfoRecv = NULL;
            Capi_dpi_conn_unref(conn);
            if (conn->InfoSend) {
               /* Propagate OpEnd to the sending branch too */
               a_Capi_ccc(OpEnd, 1, BCK, conn->InfoSend, NULL, NULL);
            }
            if (conn->InfoPipe) {
               /* Propagate OpEnd to the pipe branch too */
               a_Capi_ccc(OpEnd, 3, BCK, conn->InfoPipe, NULL, NULL);
            }
            g_free(Info);
            break;
            }
         case OpStop:
         case OpAbort:
            MSG(" Not implemented\n");
            break;
         }
      }

   } else if (Branch == 3) {
      if (Dir == BCK) {
         /* Pipe writing branch */
         switch (Op) {
         case OpStart:
            {
            dpi_conn_t *conn = Data1;
            Info->LocalKey = Data1;
            Capi_dpi_conn_ref(conn);
            conn->InfoPipe = Info;
            if (pipe(conn->DpiPipe)) {
               MSG(" Error with pipe\n");
               return;
            }
            a_Chain_link_new(Info, a_Capi_ccc, BCK, a_IO_ccc, 3, 3);
            a_Chain_bcb(OpStart, Info, (void *)conn->DpiPipe[1], "SockFD");
            break;
            }
         case OpSend:
            a_Chain_bcb(OpSend, Info, Data1, NULL);
            break;
         case OpEnd:
            a_Chain_bcb(OpEnd, Info, NULL, NULL);
            Capi_dpi_conn_unref(Info->LocalKey);
            g_free(Info);
            break;
         case OpStop:
         case OpAbort:
            Capi_dpi_conn_unref(Info->LocalKey);
            MSG(" Not implemented\n");
            break;
         }
      } else {  /* FWD */
         /* Pipe branch (status) */
      }
   }
}
