#include <esp8266.h>
#include <osapi.h>

#include "multipart.h"
#include "cgi.h"

#define BOUNDARY_SIZE 128

typedef enum {
  STATE_SEARCH_BOUNDARY = 0, // state: searching multipart boundary
  STATE_SEARCH_HEADER,       // state: search multipart file header
  STATE_SEARCH_HEADER_END,   // state: search the end of the file header
  STATE_UPLOAD_FILE,         // state: read file content
  STATE_ERROR,               // state: error (stop processing)
} MultipartState;

struct _MultipartCtx {
  MultipartCallback callBack;           // callback for multipart events
  int               position;           // current file position
  int               startTime;          // timestamp when connection was initiated
  int               recvPosition;       // receive position (how many bytes was processed from the HTTP post)
  char *            boundaryBuffer;     // buffer used for boundary detection
  int               boundaryBufferPtr;  // pointer in the boundary buffer
  MultipartState    state;              // multipart processing state
};

// this method is responsible for creating the multipart context
MultipartCtx * ICACHE_FLASH_ATTR multipartCreateContext(MultipartCallback callback)
{
  MultipartCtx * ctx = (MultipartCtx *)os_malloc(sizeof(MultipartCtx));
  ctx->callBack = callback;
  ctx->position = ctx->startTime = ctx->recvPosition = ctx->boundaryBufferPtr = 0;
  ctx->boundaryBuffer = NULL;
  ctx->state = STATE_SEARCH_BOUNDARY;
  return ctx;
}

// for allocating buffer for multipart upload 
void ICACHE_FLASH_ATTR multipartAllocBoundaryBuffer(MultipartCtx * context)
{
  if( context->boundaryBuffer == NULL )
    context->boundaryBuffer = (char *)os_malloc(3*BOUNDARY_SIZE + 1);
  context->boundaryBufferPtr = 0;
}

// for freeing multipart buffer
void ICACHE_FLASH_ATTR multipartFreeBoundaryBuffer(MultipartCtx * context)
{
  if( context->boundaryBuffer != NULL )
  {
    os_free(context->boundaryBuffer);
    context->boundaryBuffer = NULL;
  }
}

// for destroying the context
void ICACHE_FLASH_ATTR multipartDestroyContext(MultipartCtx * context)
{
  multipartFreeBoundaryBuffer(context);
  os_free(context);
}

// this is because of os_memmem is missing
void * mp_memmem(const void *l, size_t l_len, const void *s, size_t s_len)
{
	register char *cur, *last;
	const char *cl = (const char *)l;
	const char *cs = (const char *)s;

	/* we need something to compare */
	if (l_len == 0 || s_len == 0)
		return NULL;

	/* "s" must be smaller or equal to "l" */
	if (l_len < s_len)
		return NULL;

	/* special case where s_len == 1 */
	if (s_len == 1)
		return memchr(l, (int)*cs, l_len);

	/* the last position where its possible to find "s" in "l" */
	last = (char *)cl + l_len - s_len;

	for (cur = (char *)cl; cur <= last; cur++)
		if (cur[0] == cs[0] && memcmp(cur, cs, s_len) == 0)
			return cur;

	return NULL;
}


// this method is for processing data coming from the HTTP post request
//   context:   the multipart context
//   boundary:  a string which indicates boundary
//   data:      the received data
//   len:       the received data length (can't be bigger than BOUNDARY_SIZE)
//   last:      last packet indicator
//
// Detecting a boundary is not easy. One has to take care of boundaries which are splitted in 2 packets
//   [Packet 1, 5 bytes of the boundary][Packet 2, remaining 10 bytes of the boundary];
//
// Algorythm:
//   - create a buffer which size is 3*BOUNDARY_SIZE
//   - put data into the buffer as long as the buffer size is smaller than 2*BOUNDARY_SIZE
//   - search boundary in the received buffer, if found: boundary reached -> process data before boundary -> process boundary
//   - if not found -> process the first BOUNDARY_SIZE amount of bytes from the buffer
//   - remove processed data from the buffer
// this algorythm guarantees that no boundary loss will happen

int ICACHE_FLASH_ATTR multipartProcessData(MultipartCtx * context, char * boundary, char * data, int len, int last)
{
  if( len != 0 ) // add data to the boundary buffer
  {
    os_memcpy(context->boundaryBuffer + context->boundaryBufferPtr, data, len);
    
    context->boundaryBufferPtr += len;
    context->boundaryBuffer[context->boundaryBufferPtr] = 0;
  }
  
  while( context->boundaryBufferPtr > 0 )
  {
    if( ! last && context->boundaryBufferPtr <= 2 * BOUNDARY_SIZE ) // return if buffer is too small and not the last packet is processed
      return 0;
  
    int dataSize = BOUNDARY_SIZE;
  
    char * boundaryLoc = mp_memmem( context->boundaryBuffer, context->boundaryBufferPtr, boundary, os_strlen(boundary) );
    if( boundaryLoc != NULL )
    {
      int pos = boundaryLoc - context->boundaryBuffer;
      if( pos > BOUNDARY_SIZE ) // process in the next call
        boundaryLoc = NULL;
      else
        dataSize = pos;
    }
    
    if( dataSize != 0 ) // data to process
    {
      switch( context->state )
      {
        case STATE_SEARCH_HEADER:
        case STATE_SEARCH_HEADER_END:
          {
            char * chr = os_strchr( context->boundaryBuffer, '\n' );
            if( chr != NULL )
            {
	      // chop datasize to contain only one line
              int pos = chr - context->boundaryBuffer + 1;
              if( pos < dataSize ) // if chop smaller than the dataSize, delete the boundary
              {
                dataSize = pos;
                boundaryLoc = NULL; // process boundary next time
              }
              if( context->state == STATE_SEARCH_HEADER_END )
              {
                if( pos == 1 || ( ( pos == 2 ) && ( context->boundaryBuffer[0] == '\r' ) ) ) // empty line?
                {
                  context->state = STATE_UPLOAD_FILE;
                  context->position = 0;
                }
              }
              else if( os_strncmp( context->boundaryBuffer, "Content-Disposition:",  20 ) == 0 )
              {
                char * fnam = os_strstr( context->boundaryBuffer, "filename=" );
                if( fnam != NULL )
                {
                  int pos = fnam - context->boundaryBuffer + 9;
                  if( pos < dataSize )
                  {
                    while(context->boundaryBuffer[pos] == ' ') pos++; // skip spaces
                    if( context->boundaryBuffer[pos] == '"' ) // quote start
                    {
                      pos++;
                      int start = pos;
                      while( pos < context->boundaryBufferPtr )
                      {
                        if( context->boundaryBuffer[pos] == '"' ) // quote end
                          break;
                        pos++;
                      }
                      if( pos < context->boundaryBufferPtr )
                      {
                        context->boundaryBuffer[pos] = 0; // terminating zero for the file name
                        os_printf("Uploading file: %s\n", context->boundaryBuffer + start);
                        if( context->callBack( FILE_START, context->boundaryBuffer + start, pos - start, 0 ) ) // FILE_START callback
			  return 1; // if an error happened
                        context->boundaryBuffer[pos] = '"'; // restore the original quote
                        context->state = STATE_SEARCH_HEADER_END;
                      }
                    }
                  }
                }
              }
            }
          }
          break;
        case STATE_UPLOAD_FILE:
          {
            char c = context->boundaryBuffer[dataSize];
            context->boundaryBuffer[dataSize] = 0; // add terminating zero (for easier handling)
            if( context->callBack( FILE_DATA, context->boundaryBuffer, dataSize, context->position ) ) // FILE_DATA callback
	      return 1;
            context->boundaryBuffer[dataSize] = c;
            context->position += dataSize;
          }
          break;
        default:
          break;
      }
    }
    
    if( boundaryLoc != NULL ) // boundary found?
    {
      dataSize += os_strlen(boundary); // jump over the boundary
      if( context->state == STATE_UPLOAD_FILE )
      {
        if( context->callBack( FILE_DONE, NULL, 0, context->position ) ) // file done callback
	  return 1; // if an error happened
        os_printf("File upload done\n");
      }

      context->state = STATE_SEARCH_HEADER; // search the next header
    }
    
    // move the buffer back with dataSize
    context->boundaryBufferPtr -= dataSize;
    os_memcpy(context->boundaryBuffer, context->boundaryBuffer + dataSize, context->boundaryBufferPtr);
  }
  
  return 0;
}

// for processing multipart requests
int ICACHE_FLASH_ATTR multipartProcess(MultipartCtx * context, HttpdConnData * connData )
{
  if (connData->conn==NULL) return HTTPD_CGI_DONE; // Connection aborted. Clean up.
  
  if (connData->requestType == HTTPD_METHOD_POST) {
    HttpdPostData *post = connData->post;
    
    if( post->multipartBoundary == NULL )
    {
      errorResponse(connData, 404, "Only multipart POST is supported");
      return HTTPD_CGI_DONE;
    }

    if( connData->startTime != context->startTime )
    {
      // reinitialize, as this is a new request
      context->position = 0;
      context->recvPosition = 0;
      context->startTime = connData->startTime;
      context->state = STATE_SEARCH_BOUNDARY;
 
      multipartAllocBoundaryBuffer(context);
      
      if( context->callBack( FILE_UPLOAD_START, NULL, 0, context->position ) ) // start uploading files
        context->state = STATE_ERROR;
    }

    if( context->state != STATE_ERROR )
    {
      int feed = 0;
      while( feed < post->buffLen )
      {
        int len = post->buffLen - feed;
        if( len > BOUNDARY_SIZE )
          len = BOUNDARY_SIZE;
        if( multipartProcessData(context, post->multipartBoundary, post->buff + feed, len, 0) )
        {
          context->state = STATE_ERROR;
          break;
        }
        feed += len;
      }
    }
    
    context->recvPosition += post->buffLen;
    if( context->recvPosition < post->len )
      return HTTPD_CGI_MORE;

    if( context->state != STATE_ERROR )
    {
      // this is the last package, process the remaining data
      if( multipartProcessData(context, post->multipartBoundary, NULL, 0, 1) )
        context->state = STATE_ERROR;
      else if( context->callBack( FILE_UPLOAD_DONE, NULL, 0, context->position ) ) // done with files
        context->state = STATE_ERROR;
    }
    
    multipartFreeBoundaryBuffer( context );
    
    if( context->state == STATE_ERROR )
      errorResponse(connData, 400, "Invalid file upload!");
    else
    {
      httpdStartResponse(connData, 204);
      httpdEndHeaders(connData);
    }
    return HTTPD_CGI_DONE;
  }
  else {
    errorResponse(connData, 404, "Only multipart POST is supported");
    return HTTPD_CGI_DONE;
  }
}