/** @file

  Proxy Side Include plugin (PSI)

  Synopsis:

  This plugin allows to insert the content of a file stored on the proxy disk
  into the body of an html response.

  The plugin illustrates how to use a pool of threads in order to do blocking
  calls (here, some disk i/o) in a Traffic Server plugin.

  Further details: Refer to README file.

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <sys/param.h>

#include "ts/ts.h"
#include "thread.h"
#include "tscore/ink_defs.h"

/* This is the number of threads spawned by the plugin.
   Should be tuned based on performance requirements,
   blocking calls duration, etc... */
#define NB_THREADS 3

#define PSI_FILENAME_MAX_SIZE 512
#define PSI_PATH_MAX_SIZE 256
#define PSI_PATH "include"

#define PSI_START_TAG "<!--include="
#define PSI_START_TAG_LEN 12

#define PSI_END_TAG "-->"
#define PSI_END_TAG_LEN 3

#define MIME_FIELD_XPSI "X-Psi"

typedef enum {
  STATE_READ_DATA = 1,
  STATE_READ_PSI  = 2,
  STATE_DUMP_PSI  = 3,
} PluginState;

typedef enum {
  PARSE_SEARCH,
  PARSE_EXTRACT,
} ParseState;

typedef struct {
  unsigned int magic;
  TSVIO output_vio;
  TSIOBuffer output_buffer;
  TSIOBufferReader output_reader;

  TSIOBuffer psi_buffer;
  TSIOBufferReader psi_reader;
  char psi_filename[PSI_FILENAME_MAX_SIZE + 128];
  int psi_filename_len;
  int psi_success;

  ParseState parse_state;

  PluginState state;
  int transform_bytes;
} ContData;

typedef struct {
  TSCont contp;
  TSEvent event;
} TryLockData;

typedef enum {
  STR_SUCCESS,
  STR_PARTIAL,
  STR_FAIL,
} StrOperationResult;

extern Queue job_queue;

static TSTextLogObject log;
static char psi_directory[PSI_PATH_MAX_SIZE];

static int trylock_handler(TSCont contp, TSEvent event, void *edata);

/*-------------------------------------------------------------------------
  cont_data_alloc
  Allocate and initialize a ContData structure associated to a transaction

  Input:
  Output:
  Return Value:
    Pointer on a new allocated ContData structure
  -------------------------------------------------------------------------*/
static ContData *
cont_data_alloc()
{
  ContData *data;

  data                = (ContData *)TSmalloc(sizeof(ContData));
  data->magic         = MAGIC_ALIVE;
  data->output_vio    = NULL;
  data->output_buffer = NULL;
  data->output_reader = NULL;

  data->psi_buffer       = NULL;
  data->psi_reader       = NULL;
  data->psi_filename[0]  = '\0';
  data->psi_filename_len = 0;
  data->psi_success      = 0;

  data->parse_state = PARSE_SEARCH;

  data->state           = STATE_READ_DATA;
  data->transform_bytes = 0;

  return data;
}

/*-------------------------------------------------------------------------
  cont_data_destroy
  Deallocate ContData structure associated to a transaction

  Input:
    data   structure to deallocate
  Output:
  Return Value:
    none
  -------------------------------------------------------------------------*/
static void
cont_data_destroy(ContData *data)
{
  TSDebug(PLUGIN_NAME, "Destroying continuation data");
  if (data) {
    TSAssert(data->magic == MAGIC_ALIVE);
    if (data->output_reader) {
      TSIOBufferReaderFree(data->output_reader);
      data->output_reader = NULL;
    }
    if (data->output_buffer) {
      TSIOBufferDestroy(data->output_buffer);
      data->output_buffer = NULL;
    }
    if (data->psi_reader) {
      TSIOBufferReaderFree(data->psi_reader);
      data->psi_reader = NULL;
    }
    if (data->psi_buffer) {
      TSIOBufferDestroy(data->psi_buffer);
      data->psi_buffer = NULL;
    }
    data->magic = MAGIC_DEAD;
    TSfree(data);
  }
}

/*-------------------------------------------------------------------------
  strsearch_ioreader
  Looks for string pattern in an iobuffer

  Input:
    reader   reader on a iobuffer
    pattern  string to look for (nul terminated)
  Output:
    nparse   number of chars scanned, excluding the matching pattern
  Return Value:
    STR_SUCCESS if pattern found
    STR_PARTIAL if pattern found partially
    STR_FAIL    if pattern not found
  -------------------------------------------------------------------------*/
static StrOperationResult
strsearch_ioreader(TSIOBufferReader reader, const char *pattern, int *nparse)
{
  int index             = 0;
  TSIOBufferBlock block = TSIOBufferReaderStart(reader);
  int slen              = strlen(pattern);

  if (slen <= 0) {
    return STR_FAIL;
  }

  *nparse = 0;

  /* Loop thru each block while we've not yet found the pattern */
  while ((block != NULL) && (index < slen)) {
    int64_t blocklen;
    const char *blockptr = TSIOBufferBlockReadStart(block, reader, &blocklen);
    const char *ptr;

    for (ptr = blockptr; ptr < blockptr + blocklen; ptr++) {
      (*nparse)++;
      if (*ptr == pattern[index]) {
        index++;
        if (index == slen) {
          break;
        }
      } else {
        index = 0;
      }
    }

    /* Parse next block */
    block = TSIOBufferBlockNext(block);
  }

  *nparse -= index; /* Adjust nparse so it doesn't include matching chars */
  if (index == slen) {
    TSDebug(PLUGIN_NAME, "strfind: match for %s at position %d", pattern, *nparse);
    return STR_SUCCESS;
  } else if (index > 0) {
    TSDebug(PLUGIN_NAME, "strfind: partial match for %s at position %d", pattern, *nparse);
    return STR_PARTIAL;
  } else {
    TSDebug(PLUGIN_NAME, "strfind no match for %s", pattern);
    return STR_FAIL;
  }
}

/*-------------------------------------------------------------------------
  strextract_ioreader
  Extract a string from an iobuffer.
  Start reading at position offset in iobuffer and extract until the
  string end_pattern is found.

  Input:
    reader      reader on a iobuffer
    offset      position to start reading
    end_pattern the termination string (nul terminated)
  Output:
    buffer      if success, contains the extracted string, nul terminated
    buflen      if success, contains the buffer length (excluding null char).
  Return Value:
    STR_SUCCESS if extraction successful
    STR_PARTIAL if extraction not yet completed
    STR_FAIL    if extraction failed
  -------------------------------------------------------------------------*/
static int
strextract_ioreader(TSIOBufferReader reader, int offset, const char *end_pattern, char *buffer, int *buflen)
{
  int buf_idx       = 0;
  int p_idx         = 0;
  int nbytes_so_far = 0;
  int plen          = strlen(end_pattern);
  const char *ptr;
  TSIOBufferBlock block = TSIOBufferReaderStart(reader);

  if (plen <= 0) {
    return STR_FAIL;
  }

  /* Now start extraction */
  while ((block != NULL) && (p_idx < plen) && (buf_idx < PSI_FILENAME_MAX_SIZE)) {
    int64_t blocklen;
    const char *blockptr = TSIOBufferBlockReadStart(block, reader, &blocklen);

    for (ptr = blockptr; ptr < blockptr + blocklen; ptr++, nbytes_so_far++) {
      if (nbytes_so_far >= offset) {
        /* Add a new character to the filename */
        buffer[buf_idx++] = *ptr;

        /* If we have reach the end of the filename, we're done */
        if (end_pattern[p_idx] == *ptr) {
          p_idx++;
          if (p_idx == plen) {
            break;
          }
        } else {
          p_idx = 0;
        }

        /* The filename is too long, something is fishy... let's abort extraction */
        if (buf_idx >= PSI_FILENAME_MAX_SIZE) {
          break;
        }
      }
    }

    block = TSIOBufferBlockNext(block);
  }

  /* Error, could not read end of filename */
  if (buf_idx >= PSI_FILENAME_MAX_SIZE) {
    TSDebug(PLUGIN_NAME, "strextract: filename too long");
    *buflen = 0;
    return STR_FAIL;
  }

  /* Full Match */
  if (p_idx == plen) {
    /* Nul terminate the filename, remove the end_pattern copied into the buffer */
    *buflen         = buf_idx - plen;
    buffer[*buflen] = '\0';
    TSDebug(PLUGIN_NAME, "strextract: filename = |%s|", buffer);
    return STR_SUCCESS;
  }
  /* End of filename not yet reached we need to read some more data */
  else {
    TSDebug(PLUGIN_NAME, "strextract: partially extracted filename");
    *buflen = buf_idx - p_idx;
    return STR_PARTIAL;
  }
}

/*-------------------------------------------------------------------------
  parse_data
  Search for psi filename in the data.

  Input:
    contp   continuation for the current transaction
    reader  reader on the iobuffer that contains data
    avail   amount of data available in the iobuffer
  Output:
    towrite    amount of data in the iobuffer that can be written
               to the downstream vconnection
    toconsume  amount of data in the iobuffer to consume
  Return Value:
    0   if no psi filename found
    1  if a psi filename was found
  -------------------------------------------------------------------------*/
static int
parse_data(TSCont contp, TSIOBufferReader input_reader, int avail, int *toconsume, int *towrite)
{
  ContData *data;
  int nparse = 0;
  int status;

  data = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  if (data->parse_state == PARSE_SEARCH) {
    /* Search for the start pattern */
    status = strsearch_ioreader(input_reader, PSI_START_TAG, &nparse);
    switch (status) {
    case STR_FAIL:
      /* We didn't found the pattern */
      *toconsume        = avail;
      *towrite          = avail;
      data->parse_state = PARSE_SEARCH;
      return 0;
    case STR_PARTIAL:
      /* We need to read some more data */
      *toconsume        = nparse;
      *towrite          = nparse;
      data->parse_state = PARSE_SEARCH;
      return 0;
    case STR_SUCCESS:
      /* We found the start_pattern, let's go ahead */
      data->psi_filename_len = 0;
      data->psi_filename[0]  = '\0';
      data->parse_state      = PARSE_EXTRACT;
      break;
    default:
      TSAssert(!"strsearch_ioreader returned unexpected status");
    }
  }

  /* And now let's extract the filename */
  status = strextract_ioreader(input_reader, nparse + PSI_START_TAG_LEN, PSI_END_TAG, data->psi_filename, &data->psi_filename_len);
  switch (status) {
  case STR_FAIL:
    /* We couldn't extract a valid filename */
    *toconsume        = nparse;
    *towrite          = nparse;
    data->parse_state = PARSE_SEARCH;
    return 0;
  case STR_PARTIAL:
    /* We need to read some more data */
    *toconsume        = nparse;
    *towrite          = nparse;
    data->parse_state = PARSE_EXTRACT;
    return 0;
  case STR_SUCCESS:
    /* We got a valid filename */
    *toconsume        = nparse + PSI_START_TAG_LEN + data->psi_filename_len + PSI_END_TAG_LEN;
    *towrite          = nparse;
    data->parse_state = PARSE_SEARCH;
    return 1;
  default:
    TSAssert(!"strextract_ioreader returned bad status");
  }

  return 0;
}

// TODO: Use libc basename function
//
/*-------------------------------------------------------------------------
  strip_path
  Utility func to strip path from a filename (= _basename cmd on unix)
  Input:
    filename
  Output :
    None
  Return Value:
    Filename with path stripped
  -------------------------------------------------------------------------*/
static const char *
_basename(const char *filename)
{
  char *cptr;
  const char *ptr = filename;

  while ((cptr = strchr(ptr, (int)'/')) != NULL) {
    ptr = cptr + 1;
  }
  return ptr;
}

/*-------------------------------------------------------------------------
  psi_include
  Read file to include. Copy its content into an iobuffer.

  This is the function doing blocking calls and called by the plugin's threads

  Input:
    data      continuation for the current transaction
  Output :
    data->psi_buffer  contains the file content
    data->psi_sucess  0 if include failed, 1 if success
  Return Value:
    0  if failure
    1  if success
  -------------------------------------------------------------------------*/
static int
psi_include(TSCont contp, void *edata ATS_UNUSED)
{
#define BUFFER_SIZE 1024
  ContData *data;
  TSFile filep;
  char buf[BUFFER_SIZE];
  char inc_file[PSI_PATH_MAX_SIZE + PSI_FILENAME_MAX_SIZE];

  /* We manipulate plugin continuation data from a separate thread.
     Grab mutex to avoid concurrent access */
  TSMutexLock(TSContMutexGet(contp));
  data = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  if (!data->psi_buffer) {
    data->psi_buffer = TSIOBufferCreate();
    data->psi_reader = TSIOBufferReaderAlloc(data->psi_buffer);
  }

  /* For security reason, we do not allow to include files that are
     not in the directory <plugin_path>/include.
     Also include file cannot contain any path. */
  snprintf(inc_file, sizeof(inc_file), "%s/%s", psi_directory, _basename(data->psi_filename));

  /* Read the include file and copy content into iobuffer */
  if ((filep = TSfopen(inc_file, "r")) != NULL) {
    TSDebug(PLUGIN_NAME, "Reading include file %s", inc_file);

    while (TSfgets(filep, buf, BUFFER_SIZE) != NULL) {
      TSIOBufferBlock block;
      int64_t len, avail, ndone, ntodo, towrite;
      char *ptr_block;

      len   = strlen(buf);
      ndone = 0;
      ntodo = len;
      while (ntodo > 0) {
        /* TSIOBufferStart allocates more blocks if required */
        block     = TSIOBufferStart(data->psi_buffer);
        ptr_block = TSIOBufferBlockWriteStart(block, &avail);
        towrite   = MIN(ntodo, avail);

        memcpy(ptr_block, buf + ndone, towrite);
        TSIOBufferProduce(data->psi_buffer, towrite);
        ntodo -= towrite;
        ndone += towrite;
      }
    }
    TSfclose(filep);
    data->psi_success = 1;
    if (log) {
      TSTextLogObjectWrite(log, "Successfully included file: %s", inc_file);
    }
  } else {
    data->psi_success = 0;
    if (log) {
      TSTextLogObjectWrite(log, "Failed to include file: %s", inc_file);
    }
  }

  /* Change state and schedule an event EVENT_IMMEDIATE on the plugin continuation
     to let it know we're done. */

  /* Note: if the blocking call was not in the transformation state (i.e. in
     TS_HTTP_READ_REQUEST_HDR, TS_HTTP_OS_DNS and so on...) we could
     use TSHttpTxnReenable to wake up the transaction instead of sending an event. */

  TSContSchedule(contp, 0, TS_THREAD_POOL_DEFAULT);
  data->psi_success = 0;
  data->state       = STATE_READ_DATA;
  TSMutexUnlock(TSContMutexGet(contp));

  return 0;
}

/*-------------------------------------------------------------------------
  wake_up_streams
  Send an event to the upstream vconnection to either
    - ask for more data
    - let it know we're done
  Reenable the downstream vconnection
  Input:
    contp      continuation for the current transaction
  Output :
  Return Value:
   0 if failure
   1 if success
  -------------------------------------------------------------------------*/
static int
wake_up_streams(TSCont contp)
{
  TSVIO input_vio;
  ContData *data;
  int ntodo;

  data = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  input_vio = TSVConnWriteVIOGet(contp);
  ntodo     = TSVIONTodoGet(input_vio);

  if (ntodo > 0) {
    TSVIOReenable(data->output_vio);
    TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_READY, input_vio);
  } else {
    TSDebug(PLUGIN_NAME, "Total bytes produced by transform = %d", data->transform_bytes);
    TSVIONBytesSet(data->output_vio, data->transform_bytes);
    TSVIOReenable(data->output_vio);
    TSContCall(TSVIOContGet(input_vio), TS_EVENT_VCONN_WRITE_COMPLETE, input_vio);
  }

  return 1;
}

/*-------------------------------------------------------------------------
  handle_transform
   Get data from upstream vconn.
   Parse it.
   Include file if include tags found.
   Copy data to downstream vconn.
   Wake up upstream to get more data.

  Input:
    contp      continuation for the current transaction
  Output :
  Return Value:
   0 if failure
   1 if success
  -------------------------------------------------------------------------*/
static int
handle_transform(TSCont contp)
{
  TSVConn output_conn;
  TSVIO input_vio;
  ContData *data;
  TSIOBufferReader input_reader;
  int toread, avail, psi, toconsume = 0, towrite = 0;

  /* Get the output (downstream) vconnection where we'll write data to. */
  output_conn = TSTransformOutputVConnGet(contp);

  /* Get upstream vio */
  input_vio = TSVConnWriteVIOGet(contp);
  data      = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  if (!data->output_buffer) {
    data->output_buffer = TSIOBufferCreate();
    data->output_reader = TSIOBufferReaderAlloc(data->output_buffer);

    /* INT64_MAX because we don't know yet how much bytes we'll produce */
    data->output_vio = TSVConnWrite(output_conn, contp, data->output_reader, INT64_MAX);
  }

  /* If the input VIO's buffer is NULL, the transformation is over */
  if (!TSVIOBufferGet(input_vio)) {
    TSDebug(PLUGIN_NAME, "input_vio NULL, terminating transformation");
    TSVIONBytesSet(data->output_vio, data->transform_bytes);
    TSVIOReenable(data->output_vio);
    return 1;
  }

  /* Determine how much data we have left to read. */
  toread = TSVIONTodoGet(input_vio);

  if (toread > 0) {
    input_reader = TSVIOReaderGet(input_vio);
    avail        = TSIOBufferReaderAvail(input_reader);

    /* There are some data available for reading. Let's parse it */
    if (avail > 0) {
      /* No need to parse data if there are too few bytes left to contain
         an include command... */
      if (toread > (PSI_START_TAG_LEN + PSI_END_TAG_LEN)) {
        psi = parse_data(contp, input_reader, avail, &toconsume, &towrite);
      } else {
        towrite   = avail;
        toconsume = avail;
        psi       = 0;
      }

      if (towrite > 0) {
        /* Update the total size of the doc so far */
        data->transform_bytes += towrite;

        /* Copy the data from the read buffer to the output buffer. */
        /* TODO: Should we check the return value of TSIOBufferCopy() ? */
        TSIOBufferCopy(TSVIOBufferGet(data->output_vio), TSVIOReaderGet(input_vio), towrite, 0);
        /* Reenable the output connection so it can read the data we've produced. */
        TSVIOReenable(data->output_vio);
      }

      if (toconsume > 0) {
        /* Consume data we've processed an we are no longer interested in */
        TSIOBufferReaderConsume(input_reader, toconsume);

        /* Modify the input VIO to reflect how much data we've completed. */
        TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + toconsume);
      }

      /* Did we find a psi filename to execute in the data ? */
      if (psi) {
        Job *new_job;
        /* Add a request to include a file into the jobs queue.. */
        /* We'll be called back once it's done with an EVENT_IMMEDIATE */
        TSDebug(PLUGIN_NAME, "Psi filename extracted, adding an include job to thread queue");
        data->state = STATE_READ_PSI;

        /* Create a new job request and add it to the queue */
        new_job = job_create(contp, &psi_include, NULL);
        add_to_queue(&job_queue, new_job);

        /* Signal to the threads there is a new job */
        thread_signal_job();

        return 1;
      }
    }
  }

  /* Wake up upstream and downstream vconnections */
  wake_up_streams(contp);

  return 1;
}

/*-------------------------------------------------------------------------
  dump_psi
  Dump the psi_output to the downstream vconnection.

  Input:
    contp      continuation for the current transaction
  Output :
  Return Value:
   0 if failure
   1 if success
  -------------------------------------------------------------------------*/
static int
dump_psi(TSCont contp)
{
  ContData *data;
  int psi_output_len;

/* TODO: This is odd, do we need to get the input_vio, but never use it ?? */
#if 0
  TSVIO input_vio;
  input_vio = TSVConnWriteVIOGet(contp);
#endif

  data = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  /* If script exec succeeded, copy its output to the downstream vconn */
  if (data->psi_success == 1) {
    psi_output_len = TSIOBufferReaderAvail(data->psi_reader);

    if (psi_output_len > 0) {
      data->transform_bytes += psi_output_len;

      TSDebug(PLUGIN_NAME, "Inserting %d bytes from include file", psi_output_len);
      /* TODO: Should we check the return value of TSIOBufferCopy() ? */
      TSIOBufferCopy(TSVIOBufferGet(data->output_vio), data->psi_reader, psi_output_len, 0);
      /* Consume all the output data */
      TSIOBufferReaderConsume(data->psi_reader, psi_output_len);

      /* Reenable the output connection so it can read the data we've produced. */
      TSVIOReenable(data->output_vio);
    }
  }

  /* Change state to finish up reading upstream data */
  data->state = STATE_READ_DATA;
  return 0;
}

/*-------------------------------------------------------------------------
  transform_handler
  Handler for all events received during the transformation process

  Input:
    contp      continuation for the current transaction
    event      event received
    data       pointer on optional data
  Output :
  Return Value:
  -------------------------------------------------------------------------*/
static int
transform_handler(TSCont contp, TSEvent event, void *edata ATS_UNUSED)
{
  TSVIO input_vio;
  ContData *data;
  int state, retval;

  /* This section will be called by both TS internal
     and the thread. Protect it with a mutex to avoid
     concurrent calls. */

  /* Handle TryLock result */
  if (TSMutexLockTry(TSContMutexGet(contp)) != TS_SUCCESS) {
    TSCont c       = TSContCreate(trylock_handler, NULL);
    TryLockData *d = TSmalloc(sizeof(TryLockData));

    d->contp = contp;
    d->event = event;
    TSContDataSet(c, d);
    TSContSchedule(c, 10, TS_THREAD_POOL_DEFAULT);
    return 1;
  }

  data = TSContDataGet(contp);
  TSAssert(data->magic == MAGIC_ALIVE);

  state = data->state;

  /* Check to see if the transformation has been closed */
  retval = TSVConnClosedGet(contp);
  if (retval) {
    /* If the thread is still executing its job, we don't want to destroy
       the continuation right away as the thread will call us back
       on this continuation. */
    if (state == STATE_READ_PSI) {
      TSContSchedule(contp, 10, TS_THREAD_POOL_DEFAULT);
    } else {
      TSMutexUnlock(TSContMutexGet(contp));
      cont_data_destroy(TSContDataGet(contp));
      TSContDestroy(contp);
      return 1;
    }
  } else {
    switch (event) {
    case TS_EVENT_ERROR:
      input_vio = TSVConnWriteVIOGet(contp);
      TSContCall(TSVIOContGet(input_vio), TS_EVENT_ERROR, input_vio);
      break;

    case TS_EVENT_VCONN_WRITE_COMPLETE:
      TSVConnShutdown(TSTransformOutputVConnGet(contp), 0, 1);
      break;

    case TS_EVENT_VCONN_WRITE_READY:
      /* downstream vconnection is done reading data we've write into it.
         let's read some more data from upstream if we're in read state. */
      if (state == STATE_READ_DATA) {
        handle_transform(contp);
      }
      break;

    case TS_EVENT_IMMEDIATE:
      if (state == STATE_READ_DATA) {
        /* upstream vconnection signals some more data ready to be read
           let's try to transform some more data */
        handle_transform(contp);
      } else if (state == STATE_DUMP_PSI) {
        /* The thread scheduled an event on our continuation to let us
           know it has completed its job
           Let's dump the include content to the output vconnection */
        dump_psi(contp);
        wake_up_streams(contp);
      }
      break;

    default:
      TSAssert(!"Unexpected event");
      break;
    }
  }

  TSMutexUnlock(TSContMutexGet(contp));
  return 1;
}

/*-------------------------------------------------------------------------
  trylock_handler
  Small handler to handle TSMutexLockTry failures

  Input:
    contp      continuation for the current transaction
    event      event received
    data       pointer on optional data
  Output :
  Return Value:
  -------------------------------------------------------------------------*/
static int
trylock_handler(TSCont contp, TSEvent event ATS_UNUSED, void *edata ATS_UNUSED)
{
  TryLockData *data = TSContDataGet(contp);
  transform_handler(data->contp, data->event, NULL);
  TSfree(data);
  TSContDestroy(contp);
  return 0;
}

/*-------------------------------------------------------------------------
  transformable
  Determine if the current transaction should be transformed or not

  Input:
    txnp      current transaction
  Output :
  Return Value:
    1  if transformable
    0  if not
  -------------------------------------------------------------------------*/
static int
transformable(TSHttpTxn txnp)
{
  /*  We are only interested in transforming "200 OK" responses
     with a Content-Type: text/ header and with X-Psi header */
  TSMBuffer bufp;
  TSMLoc hdr_loc, field_loc;
  TSHttpStatus resp_status;
  const char *value;

  if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &bufp, &hdr_loc)) {
    resp_status = TSHttpHdrStatusGet(bufp, hdr_loc);
    if (resp_status != TS_HTTP_STATUS_OK) {
      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
      return 0;
    }

    field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, -1);
    if (field_loc == TS_NULL_MLOC) {
      TSError("[%s] Unable to search Content-Type field", PLUGIN_NAME);
      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
      return 0;
    }

    value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, -1, NULL);
    if ((value == NULL) || (strncasecmp(value, "text/", sizeof("text/") - 1) != 0)) {
      TSHandleMLocRelease(bufp, hdr_loc, field_loc);
      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
      return 0;
    }

    TSHandleMLocRelease(bufp, hdr_loc, field_loc);

    field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, MIME_FIELD_XPSI, -1);

    TSHandleMLocRelease(bufp, hdr_loc, field_loc);
    TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
  }

  return 1;
}

/*-------------------------------------------------------------------------
  transform_add
  Create a transformation and alloc data structure

  Input:
    txnp      current transaction
  Output :
  Return Value:
    1  if transformation created
    0  if not
  -------------------------------------------------------------------------*/
static int
transform_add(TSHttpTxn txnp)
{
  TSCont contp;
  ContData *data;

  contp = TSTransformCreate(transform_handler, txnp);
  data  = cont_data_alloc();
  TSContDataSet(contp, data);

  TSHttpTxnHookAdd(txnp, TS_HTTP_RESPONSE_TRANSFORM_HOOK, contp);
  return 1;
}

/*-------------------------------------------------------------------------
  read_response_handler
  Handler for events related to hook READ_RESPONSE

  Input:
    contp      continuation for the current transaction
    event      event received
    data       pointer on eventual data
  Output :
  Return Value:
  -------------------------------------------------------------------------*/
static int
read_response_handler(TSCont contp ATS_UNUSED, TSEvent event, void *edata)
{
  TSHttpTxn txnp = (TSHttpTxn)edata;

  switch (event) {
  case TS_EVENT_HTTP_READ_RESPONSE_HDR:
    if (transformable(txnp)) {
      TSDebug(PLUGIN_NAME, "Add a transformation");
      transform_add(txnp);
    }
    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
    return 0;
  default:
    break;
  }

  return 0;
}

/*-------------------------------------------------------------------------
  TSPluginInit
  Function called at plugin init time

  Input:
    argc  number of args
    argv  list vof args
  Output :
  Return Value:
  -------------------------------------------------------------------------*/
void
TSPluginInit(int argc ATS_UNUSED, const char *argv[] ATS_UNUSED)
{
  TSPluginRegistrationInfo info;
  int i;
  TSReturnCode retval;

  info.plugin_name   = "psi";
  info.vendor_name   = "Apache";
  info.support_email = "";

  if (TSPluginRegister(&info) != TS_SUCCESS) {
    TSError("[%s] Plugin registration failed", PLUGIN_NAME);
  }

  /* Initialize the psi directory = <plugin_path>/include */
  snprintf(psi_directory, sizeof(psi_directory), "%s/%s", TSPluginDirGet(), PSI_PATH);

  /* create an TSTextLogObject to log any psi include */
  retval = TSTextLogObjectCreate("psi", TS_LOG_MODE_ADD_TIMESTAMP, &log);
  if (retval == TS_ERROR) {
    TSError("[%s] Failed creating log for psi plugin", PLUGIN_NAME);
    log = NULL;
  }

  /* Create working threads */
  thread_init();
  init_queue(&job_queue);

  for (i = 0; i < NB_THREADS; i++) {
    char *thread_name = (char *)TSmalloc(64);
    sprintf(thread_name, "Thread[%d]", i);
    if (!TSThreadCreate(thread_loop, thread_name)) {
      TSError("[%s] Failed creating threads", PLUGIN_NAME);
      return;
    }
  }

  TSHttpHookAdd(TS_HTTP_READ_RESPONSE_HDR_HOOK, TSContCreate(read_response_handler, TSMutexCreate()));
  TSDebug(PLUGIN_NAME, "Plugin started");
}
