
#include "lwip/opt.h"
#include "lwip/arch.h"
#include "lwip/api.h"
#include "lwip/sys.h"	// for sys_mutex_lock() / sys_mutex_unlock()

#include "httpserver-netconn.h"

#if LWIP_NETCONN

#ifndef HTTPD_DEBUG
#define HTTPD_DEBUG         LWIP_DBG_OFF
#endif

#define MAX_STATIC_FILES	50
#define MAX_CGI_FILES		20

#define MAX_CGI_PARAMETERS	16

typedef struct
{
	const char * pachFileName;
	const char *(*pfnCallback)(int iIndex, int iNumParams, const char *pcParam[], const char *pcValue[]);
} stCgiHandler_t;

typedef struct
{
	const char * pachFileName;
	const char * pachContentType;
	uint32_t 	 uiContentTypeLen;
	const char * pachPayload;
	uint32_t	 uiPayloadLen;
	bool 		 bKeepAlive;
} stStaticFile_t;

static stCgiHandler_t cgi_handlers[MAX_CGI_FILES];
static int cgi_handlers_num;

static stStaticFile_t astStaticFile[MAX_STATIC_FILES];
static uint32_t uiStaticFileNum;

static sys_mutex_t mtxStaticFileAddMutex;
static sys_mutex_t mtxDynamicCgiAddMutex;

static const char http_html_hdr[] = "HTTP/1.1 404 Not Found\r\nContent-type: text/html\r\n\r\n";
static const char http_index_html[] = "<html><head><title>404</title></head><body><h1>File Not Found!</h1><p>Hohum</p></body></html>";

/**
 * Add a static file (e.g. in a plugin) to the list of files being served
 *
 * @param[in]	pachFileName	Filename
 * @param[in]	pachContentType	Content Type header
 * @param[in]	pachPayload		Payload
 * @param[in]	uiPayloadLen	Length of payload (in bytes)
 * @param[in]	bKeepAlive		true if file should have a Keep Alive header
 *
 * @return	true if was able to add static file OK, else false
 */
bool StaticFileAdd(	const char * pachFileName,
					const char * pachContentType,
					const char * pachPayload,
					uint32_t	 uiPayloadLen,
					bool 		 bKeepAlive)
{
	bool bRetVal = false;
	stStaticFile_t * pstStaticFile;

    sys_mutex_lock( &mtxStaticFileAddMutex );
    {
    	if (uiStaticFileNum < MAX_STATIC_FILES)
    	{
			pstStaticFile = &astStaticFile[uiStaticFileNum];
			pstStaticFile->pachFileName = pachFileName;
			pstStaticFile->pachContentType = pachContentType;
			pstStaticFile->uiContentTypeLen = strlen(pachContentType);
			pstStaticFile->pachPayload = pachPayload;
			pstStaticFile->uiPayloadLen = uiPayloadLen;
			pstStaticFile->bKeepAlive = bKeepAlive;
			__DMB();	// Enforce a Data Memory Barrier between writing object and incrementing the index
			uiStaticFileNum++;
			bRetVal = true;
	    }
	}
	sys_mutex_unlock( &mtxStaticFileAddMutex );

	return bRetVal;
}

/**
 * If a requested file is in our current list of static files being served, send it out
 *
 * @param[in]	conn			Connection to send any data out on
 * @param[in]	pachFilename	Filename (actually, whole data block sent down)
 * @param[in]	uiFilenameLen	Length of filename (actually, length of whole data block)
 *
 * @return		true if filename was recognised and was served OK, else false
 */
const bool StaticFileFind(struct netconn *conn, const char *pachFilename, uint32_t uiFilenameLen)
{
	int i;
	stStaticFile_t * pstStaticFile;
	bool bRetVal = false;
	uint32_t uiLen;

	for (i = 0; i < uiStaticFileNum; i++)
	{
		pstStaticFile = &astStaticFile[i];
		uiLen = strlen(pstStaticFile->pachFileName);
		if (	(memcmp(pachFilename, pstStaticFile->pachFileName, uiLen) == 0)
			&&	((pachFilename[uiLen] == ' ') || (pachFilename[uiLen] == '?'))	)
		{
			netconn_write(conn, pstStaticFile->pachContentType, pstStaticFile->uiContentTypeLen, NETCONN_NOCOPY);

			netconn_write(conn, pstStaticFile->pachPayload, pstStaticFile->uiPayloadLen, NETCONN_NOCOPY);

			bRetVal = true;
			break;
		}
	}

	return bRetVal;
}

/**
 * Extract named parameters from a series of ampersand-separated RESTful parameters
 *
 * @param[in]	pachParams		Input REST string (e.g. "a=1&b=2")
 * @param[out]	papchParams		Output array holding parameter names
 * @param[out]	papchValues		Output array holding parameter values
 * @param[in]	uiMaxParams		Maximum size of the two output arrays
 *
 * @return		Number of parameters found (up to the maximum number passed down)
 */
static int extract_parameters(char *pachParams, const char ** papchParams, const char ** papchValues, uint32_t uiMaxParams)
{
	char *pair;
	char *equals;
	uint32_t uiLoop;

	/* If we have no parameters at all, return 0 immediately. */
	if (!pachParams || (pachParams[0] != '?'))
	{
		return 0;
	}

	/*
	 * Parse up to uiMaxParams from the passed string and ignore the
	 * remainder (if any)
	 **/
	pair = &pachParams[1];
	for (uiLoop = 0; (uiLoop < uiMaxParams) && (pair != NULL); uiLoop++)
	{
		/* Save the name of the parameter */
		papchParams[uiLoop] = pair;

		/* Remember the start of this name=value pair */
		equals = pair;

		/* Find the start of the next name=value pair and replace the delimiter
		* with a 0 to terminate the previous pair string. */
		pair = strchr(pair, '&');
		if (pair)
		{
			*pair++ = '\0';
		}
		else
		{
			/* We didn't find a new parameter so find the end of the URI and
			* replace the space with a '\0' */
			pair = strchr(equals, ' ');
			// Benignly allow slightly malformed case to pass by without comment
			if (pair == equals)
			{
				break;
			}
			if (pair)
			{
				*pair = '\0';
			}
			pair = NULL;	// Make this the last parameter processed
		}

		/* Now find the '=' in the previous pair, replace it with '\0' and save
		 * the parameter value string. */
		equals = strchr(equals, '=');
		if (equals)
		{
			*equals++ = '\0';
			papchValues[uiLoop] = equals;
		}
		else
		{
			papchValues[uiLoop] = "";
		}
  }

  return uiLoop;
}

/**
 * Add a dynamic cgi script (e.g. in a plugin) to the list of cgi scripts being served
 *
 * @param[in]	pachFileName	Script name of cgi
 * @param[in]	pfn_Callback	Callback to call if cgi script name is recognised
 *
 * @return	true if was able to add dynamic cgi OK, else false
 */
bool DynamicCgiAdd(	const char * pachFileName,
		const char *(*pfn_Callback)(int iIndex, int iNumParams, const char *pcParam[], const char *pcValue[]) )
{
	stCgiHandler_t * pstCgi;
	bool bRetVal;

	bRetVal = false;
    sys_mutex_lock( &mtxDynamicCgiAddMutex );
	{
    	if ((cgi_handlers_num < MAX_CGI_FILES) && (pachFileName) && (pfn_Callback))
	    {
			pstCgi = &cgi_handlers[cgi_handlers_num];
			pstCgi->pachFileName = pachFileName;
			pstCgi->pfnCallback = pfn_Callback;
			__DMB();	// Enforce a Data Memory Barrier between writing object and incrementing the index
			cgi_handlers_num++;
			bRetVal = true;
	    }
	}
	sys_mutex_unlock( &mtxDynamicCgiAddMutex );

	return bRetVal;
}

/**
 * If a requested file is in our current list of dynamic cgis being served, send it out
 *
 * @param[in]	conn			Connection to send any data out on
 * @param[in]	pachFilename	Filename (actually, whole data block sent down)
 * @param[in]	uiFilenameLen	Length of filename (actually, length of whole data block)
 *
 * @return		true if cgi name was recognised and was triggered OK, else false
 */
bool DynamicCgiFind( struct netconn *conn, const char * pachFileName, uint32_t uiFileNameLen )
{
	const char * apchParams[MAX_CGI_PARAMETERS];
	const char * apchValues[MAX_CGI_PARAMETERS];
	stCgiHandler_t * pstCgi;
	bool bRetVal;
	int i;
	uint32_t uiNumParams;
	const char * pachPayload;
	char * pachDuplicate;
	uint32_t uiLen;

	bRetVal = false;
	for (i = 0; i < cgi_handlers_num; i++)
	{
		pstCgi = &cgi_handlers[i];
		uiLen = strlen(pstCgi->pachFileName);
		if (	(memcmp(pstCgi->pachFileName, pachFileName, uiLen) == 0)
			&&	((pachFileName[uiLen] == ' ') || (pachFileName[uiLen] == '?'))	)
		{
			pachDuplicate = malloc(uiFileNameLen + 1U);
			memcpy(pachDuplicate, pachFileName, uiFileNameLen);
			pachDuplicate[uiFileNameLen] = 0x00;

			uiNumParams = extract_parameters(&pachDuplicate[uiLen], apchParams, apchValues, MAX_CGI_PARAMETERS);
			pachPayload = (pstCgi->pfnCallback)(i, uiNumParams, apchParams, apchValues);

			if (pachPayload != NULL)
			{
				/* Send the cgi-generated page */
				netconn_write(conn, pachPayload, strlen(pachPayload), NETCONN_NOFLAG);
				free((void *) pachPayload);
			}

			free((void *) pachDuplicate);

			bRetVal = true;
			break;
		}
	}

	return bRetVal;
}

/** Serve one HTTP connection accepted in the http thread */
static void
http_server_netconn_serve(struct netconn *conn)
{
  struct netbuf *inbuf;
  char *buf;
  u16_t buflen;
  err_t err;
  
  /* Read the data from the port, blocking if nothing yet there. 
   We assume the request (the part we care about) is in one netbuf */
  err = netconn_recv(conn, &inbuf);
  
  if (err == ERR_OK) {
    netbuf_data(inbuf, (void**)&buf, &buflen);
    
    /* Is this an HTTP GET command? (only check the first 5 chars, since
    there are other formats for GET, and we're keeping it very simple )*/
    if (buflen>=5 &&
        buf[0]=='G' &&
        buf[1]=='E' &&
        buf[2]=='T' &&
        buf[3]==' ' &&
        buf[4]=='/' )
    {
    	if (StaticFileFind( conn, &buf[4], buflen - 4U ) == false)
    	{
    		if (DynamicCgiFind( conn, &buf[4], buflen - 4U ) == false)
    		{
				/* Send the HTML 404 header
				 * subtract 1 from the size, since we dont send the \0 in the string
				 * NETCONN_NOCOPY: our data is const static, so no need to copy it
				*/
				netconn_write(conn, http_html_hdr, sizeof(http_html_hdr)-1, NETCONN_NOCOPY);

				/* Send our HTML page */
				netconn_write(conn, http_index_html, sizeof(http_index_html)-1, NETCONN_NOCOPY);
    		}
    	}
    }
  }
  /* Close the connection (server closes in HTTP) */
  netconn_close(conn);
  
  /* Delete the buffer (netconn_recv gives us ownership,
   so we have to make sure to deallocate the buffer) */
  netbuf_delete(inbuf);
}

/** The main function, never returns! */
static void
http_server_netconn_thread(void *arg)
{
  struct netconn *conn, *newconn;
  err_t err;
  LWIP_UNUSED_ARG(arg);
  
  /* Create a new TCP connection handle */
  /* Bind to port 80 (HTTP) with default IP address */
#if LWIP_IPV6
  conn = netconn_new(NETCONN_TCP_IPV6);
  netconn_bind(conn, IP6_ADDR_ANY, 80);
#else /* LWIP_IPV6 */
  conn = netconn_new(NETCONN_TCP);
  netconn_bind(conn, IP_ADDR_ANY, 80);
#endif /* LWIP_IPV6 */
  LWIP_ERROR("http_server: invalid conn", (conn != NULL), return;);
  
  /* Put the connection into LISTEN state */
  netconn_listen(conn);
  
  do {
    err = netconn_accept(conn, &newconn);
    if (err == ERR_OK) {
      http_server_netconn_serve(newconn);
      netconn_delete(newconn);
    }
  } while(err == ERR_OK);
  LWIP_DEBUGF(HTTPD_DEBUG,
    ("http_server_netconn_thread: netconn_accept received error %d, shutting down",
    err));
  netconn_close(conn);
  netconn_delete(conn);
}

/** Initialize the HTTP server (start its thread) */
void
http_server_netconn_init(void)
{
	sys_mutex_new( &mtxStaticFileAddMutex );
	sys_mutex_new( &mtxDynamicCgiAddMutex );

	sys_thread_new("http_server_netconn", http_server_netconn_thread, NULL, DEFAULT_THREAD_STACKSIZE, DEFAULT_THREAD_PRIO);
}

#endif /* LWIP_NETCONN*/
