/** @file

  Traffic generator intercept plugin

  @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 <ts/ts.h>
#include <ts/remap.h>

#include <cerrno>
#include <cinttypes>
#include <iterator>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <unistd.h>

// Generator plugin
//
// The incoming URL must consist of 2 or more path components. The first
// component indicates cacheability, the second the number of bytes in the
// response body. Subsequent path components are ignored, so they can be used
// to uniqify cache keys (assuming that caching is enabled).
//
// Examples,
//
// /cache/100/6b1e2b1fa555b52124cb4e511acbae2a
//      -> return 100 bytes, cached
//
// /cache/21474836480/large/response
//      -> return 20G bytes, cached
//
// TODO Add query parameter options.
//
// It would be pretty useful to add support for query parameters to tweak how the response
// is handled. The following parameters seem useful:
//
//  - delay before sending the response headers
//  - delay before sending the response body
//  - turn off local caching of the response (ie. even for /cache/* URLs)
//  - force chunked encoding by omitting Content-Length
//  - specify the Cache-Control max-age
//
// We ought to scale the IO buffer size in proportion to the size of the response we are generating.

#define PLUGIN "generator"

static DbgCtl dbg_ctl{PLUGIN};

#define VDEBUG(fmt, ...) Dbg(dbg_ctl, fmt, ##__VA_ARGS__)

#if DEBUG
#define VERROR(fmt, ...) Dbg(dbg_ctl, fmt, ##__VA_ARGS__)
#else
#define VERROR(fmt, ...) TSError("[%s] %s: " fmt, PLUGIN, __FUNCTION__, ##__VA_ARGS__)
#endif

#define VIODEBUG(vio, fmt, ...)                                                                                              \
  VDEBUG("vio=%p vio.cont=%p, vio.cont.data=%p, vio.vc=%p " fmt, (vio), TSVIOContGet(vio), TSContDataGet(TSVIOContGet(vio)), \
         TSVIOVConnGet(vio), ##__VA_ARGS__)

static TSCont  TxnHook;
static uint8_t GeneratorData[32 * 1024];

static int StatCountBytes     = -1;
static int StatCountResponses = -1;

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

struct GeneratorRequest;

union argument_type {
  void             *ptr;
  intptr_t          ecode;
  TSVConn           vc;
  TSVIO             vio;
  TSHttpTxn         txn;
  GeneratorRequest *grq;

  argument_type(void *_p) : ptr(_p) {}
};

// Return the length of a string literal (without the trailing NUL).
template <unsigned N>
unsigned
lengthof(const char (&)[N])
{
  return N - 1;
}

// This structure represents the state of a streaming I/O request. It
// is directional (ie. either a read or a write). We need two of these
// for each TSVConn; one to push data into the TSVConn and one to pull
// data out.
struct IOChannel {
  TSVIO            vio = nullptr;
  TSIOBuffer       iobuf;
  TSIOBufferReader reader;

  IOChannel() : iobuf(TSIOBufferSizedCreate(TS_IOBUFFER_SIZE_INDEX_32K)), reader(TSIOBufferReaderAlloc(iobuf)) {}
  ~IOChannel()
  {
    if (this->reader) {
      TSIOBufferReaderFree(this->reader);
    }

    if (this->iobuf) {
      TSIOBufferDestroy(this->iobuf);
    }
  }

  void
  read(TSVConn vc, TSCont contp)
  {
    this->vio = TSVConnRead(vc, contp, this->iobuf, INT64_MAX);
  }

  void
  write(TSVConn vc, TSCont contp)
  {
    this->vio = TSVConnWrite(vc, contp, this->reader, INT64_MAX);
  }
};

struct GeneratorHttpHeader {
  TSMBuffer    buffer;
  TSMLoc       header;
  TSHttpParser parser;

  GeneratorHttpHeader()
  {
    this->buffer = TSMBufferCreate();
    this->header = TSHttpHdrCreate(this->buffer);
    this->parser = TSHttpParserCreate();
  }

  ~GeneratorHttpHeader()
  {
    if (this->parser) {
      TSHttpParserDestroy(this->parser);
    }

    TSHttpHdrDestroy(this->buffer, this->header);
    TSHandleMLocRelease(this->buffer, TS_NULL_MLOC, this->header);
    TSMBufferDestroy(this->buffer);
  }
};

struct GeneratorRequest {
  off_t    nbytes = 0; // Number of bytes to generate.
  unsigned flags  = 0;
  unsigned delay  = 0; // Milliseconds to delay before sending a response.
  unsigned maxage;     // Max age for cache responses.
  unsigned bytesSeen;

  int64_t             contentLength;
  IOChannel           readio;
  IOChannel           writeio;
  GeneratorHttpHeader rqheader;
  TSHttpStatus        status = TS_HTTP_STATUS_NONE;

  enum {
    CACHEABLE = 0x0001,
    ISHEAD    = 0x0002,
    ISPOST    = 0x0004,
  };

  GeneratorRequest() : maxage(60 * 60 * 24) {}
  ~GeneratorRequest() = default;
};

// Destroy a generator request, including the per-txn continuation.
static void
GeneratorRequestDestroy(GeneratorRequest *grq, TSVIO vio, TSCont contp)
{
  if (vio) {
    TSVConnClose(TSVIOVConnGet(vio));
  }

  TSContDestroy(contp);
  delete grq;
}

static off_t
GeneratorParseByteCount(const char *ptr, const char *end)
{
  off_t nbytes = 0;

  for (; ptr < end; ++ptr) {
    switch (*ptr) {
    case '0':
      nbytes = nbytes * 10 + 0;
      break;
    case '1':
      nbytes = nbytes * 10 + 1;
      break;
    case '2':
      nbytes = nbytes * 10 + 2;
      break;
    case '3':
      nbytes = nbytes * 10 + 3;
      break;
    case '4':
      nbytes = nbytes * 10 + 4;
      break;
    case '5':
      nbytes = nbytes * 10 + 5;
      break;
    case '6':
      nbytes = nbytes * 10 + 6;
      break;
    case '7':
      nbytes = nbytes * 10 + 7;
      break;
    case '8':
      nbytes = nbytes * 10 + 8;
      break;
    case '9':
      nbytes = nbytes * 10 + 9;
      break;
    default:
      return -1;
    }
  }

  return nbytes;
}

static void
HeaderFieldDateSet(GeneratorHttpHeader &http, const char *field_name, int64_t field_len, time_t value)
{
  TSMLoc field;

  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
  TSMimeHdrFieldValueDateSet(http.buffer, http.header, field, value);
  TSMimeHdrFieldAppend(http.buffer, http.header, field);
  TSHandleMLocRelease(http.buffer, http.header, field);
}

static void
HeaderFieldIntSet(GeneratorHttpHeader &http, const char *field_name, int64_t field_len, int64_t value)
{
  TSMLoc field;

  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
  TSMimeHdrFieldValueInt64Set(http.buffer, http.header, field, -1, value);
  TSMimeHdrFieldAppend(http.buffer, http.header, field);
  TSHandleMLocRelease(http.buffer, http.header, field);
}

static void
HeaderFieldStringSet(GeneratorHttpHeader &http, const char *field_name, int64_t field_len, const char *value)
{
  TSMLoc field;

  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
  TSMimeHdrFieldValueStringSet(http.buffer, http.header, field, -1, value, -1);
  TSMimeHdrFieldAppend(http.buffer, http.header, field);
  TSHandleMLocRelease(http.buffer, http.header, field);
}

static int64_t
GeneratorGetRequestHeader(GeneratorHttpHeader &request, const char *field_name, int64_t field_len, int64_t default_value)
{
  TSMLoc field;

  field = TSMimeHdrFieldFind(request.buffer, request.header, field_name, field_len);
  if (field != TS_NULL_MLOC) {
    default_value = TSMimeHdrFieldValueInt64Get(request.buffer, request.header, field, -1);
  }

  TSHandleMLocRelease(request.buffer, request.header, field);
  return default_value;
}

static TSReturnCode
WriteResponseHeader(GeneratorHttpHeader &response, TSHttpStatus status)
{
  if (TSHttpHdrTypeSet(response.buffer, response.header, TS_HTTP_TYPE_RESPONSE) != TS_SUCCESS) {
    VERROR("failed to set type");
    return TS_ERROR;
  }
  if (TSHttpHdrVersionSet(response.buffer, response.header, TS_HTTP_VERSION(1, 1)) != TS_SUCCESS) {
    VERROR("failed to set HTTP version");
    return TS_ERROR;
  }
  if (TSHttpHdrStatusSet(response.buffer, response.header, status) != TS_SUCCESS) {
    VERROR("failed to set HTTP status");
    return TS_ERROR;
  }

  if (TSHttpHdrReasonSet(response.buffer, response.header, TSHttpHdrReasonLookup(status), -1) != TS_SUCCESS) {
    VERROR("failed to set expand HTTP status");
    return TS_ERROR;
  };

  return TS_SUCCESS;
}

static TSReturnCode
GeneratorWriteFailureResponse(GeneratorRequest *grq, TSHttpStatus status)
{
  GeneratorHttpHeader response;
  VDEBUG("writing failure response header");

  if (WriteResponseHeader(response, status) != TS_SUCCESS) {
    return TS_ERROR;
  }

  int hdrlen = TSHttpHdrLengthGet(response.buffer, response.header);

  TSHttpHdrPrint(response.buffer, response.header, grq->writeio.iobuf);

  TSVIONBytesSet(grq->writeio.vio, hdrlen);
  TSVIOReenable(grq->writeio.vio);

  return TS_SUCCESS;
}

static TSReturnCode
GeneratorWriteResponse(GeneratorRequest *grq)
{
  GeneratorHttpHeader response;
  VDEBUG("writing GET response");

  if (WriteResponseHeader(response, TS_HTTP_STATUS_OK) != TS_SUCCESS) {
    return TS_ERROR;
  }

  // Set the Content-Length header.
  HeaderFieldIntSet(response, TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH, grq->nbytes);

  // Set the Cache-Control header.
  if (grq->flags & GeneratorRequest::CACHEABLE) {
    char buf[64];

    snprintf(buf, sizeof(buf), "max-age=%u, public", grq->maxage);
    HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, buf);
    HeaderFieldDateSet(response, TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED, time(nullptr));
  } else {
    HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, "private");
  }

  // Write the header to the IO buffer. Set the VIO bytes so that we can get a WRITE_COMPLETE
  // event when this is done.
  int hdrlen = TSHttpHdrLengthGet(response.buffer, response.header);

  TSHttpHdrPrint(response.buffer, response.header, grq->writeio.iobuf);
  TSVIONBytesSet(grq->writeio.vio, hdrlen);
  TSVIOReenable(grq->writeio.vio);

  TSStatIntIncrement(StatCountBytes, hdrlen);

  return TS_SUCCESS;
}

static TSReturnCode
GeneratorPOSTResponse(GeneratorRequest *grq)
{
  GeneratorHttpHeader response;
  VDEBUG("writing POST response");

  if (WriteResponseHeader(response, TS_HTTP_STATUS_OK) != TS_SUCCESS) {
    return TS_ERROR;
  }

  int hdrlen = TSHttpHdrLengthGet(response.buffer, response.header);

  TSHttpHdrPrint(response.buffer, response.header, grq->writeio.iobuf);

  TSVIONBytesSet(grq->writeio.vio, hdrlen);
  TSVIOReenable(grq->writeio.vio);

  return TS_SUCCESS;
}

static bool
GeneratorParseRequest(GeneratorRequest *grq)
{
  TSMLoc      url;
  const char *path;
  const char *end;
  int         pathsz;
  unsigned    count = 0;

  path = TSHttpHdrMethodGet(grq->rqheader.buffer, grq->rqheader.header, &pathsz);

  if (path == TS_HTTP_METHOD_HEAD) {
    grq->flags |= GeneratorRequest::ISHEAD;
  } else if (path == TS_HTTP_METHOD_POST) {
    VDEBUG("requested operation is POST");
    grq->flags |= GeneratorRequest::ISPOST;

    grq->contentLength = GeneratorGetRequestHeader(grq->rqheader, "Content-Length", lengthof("Content-Length"), -1);

    grq->bytesSeen = 0;

    // implicitly reject "Transfer-Encoding: chunked" (or requests with invalid content-length)
    return grq->contentLength > 0;
  }

  grq->delay  = GeneratorGetRequestHeader(grq->rqheader, "Generator-Delay", lengthof("Generator-Delay"), grq->delay);
  grq->maxage = GeneratorGetRequestHeader(grq->rqheader, "Generator-MaxAge", lengthof("Generator-MaxAge"), grq->maxage);

  // Next, parse our parameters out of the URL.
  if (TSHttpHdrUrlGet(grq->rqheader.buffer, grq->rqheader.header, &url) != TS_SUCCESS) {
    VERROR("failed to get URI handle");
    return false;
  }

  path = TSUrlPathGet(grq->rqheader.buffer, url, &pathsz);
  if (!path) {
    VDEBUG("empty path");
    return false;
  }

  VDEBUG("requested path is %.*s", pathsz, path);

  end = path + pathsz;
  while (path < end) {
    const char *sep = path;
    size_t      nbytes;

    while (*sep != '/' && sep < end) {
      ++sep;
    }

    nbytes = std::distance(path, sep);
    if (nbytes) {
      VDEBUG("path component is %.*s", (int)nbytes, path);

      switch (count) {
      case 0:
        // First path component is "cache" or "nocache".
        if (memcmp(path, "cache", 5) == 0) {
          grq->flags |= GeneratorRequest::CACHEABLE;
        } else if (memcmp(path, "nocache", 7) == 0) {
          grq->flags &= ~GeneratorRequest::CACHEABLE;
        } else {
          VDEBUG("first component is %.*s, expecting 'cache' or 'nocache'", (int)nbytes, path);
          goto fail;
        }

        break;

      case 1:
        // Second path component is a byte count.
        grq->nbytes = GeneratorParseByteCount(path, sep);
        VDEBUG("generator byte count is %lld", (long long)grq->nbytes);
        if (grq->nbytes >= 0) {
          // We don't care about any other path components.
          TSHandleMLocRelease(grq->rqheader.buffer, grq->rqheader.header, url);
          return true;
        }

        goto fail;
      }

      ++count;
    }

    path = sep + 1;
  }

fail:
  TSHandleMLocRelease(grq->rqheader.buffer, grq->rqheader.header, url);
  return false;
}

// Handle events from TSHttpTxnServerIntercept. The intercept
// starts with TS_EVENT_NET_ACCEPT, and then continues with
// TSVConn events.
static int
GeneratorInterceptHook(TSCont contp, TSEvent event, void *edata)
{
  argument_type arg(edata);

  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, TSHttpEventNameLookup(event), event, arg.ptr);

  switch (event) {
  case TS_EVENT_NET_ACCEPT: {
    // TS_EVENT_NET_ACCEPT will be delivered when the server intercept
    // is set up by the core. We just need to allocate a generator
    // request state and start reading the VC.
    GeneratorRequest *grq = new GeneratorRequest();

    TSStatIntIncrement(StatCountResponses, 1);
    VDEBUG("allocated server intercept generator grq=%p", grq);

    // This continuation was allocated in GeneratorTxnHook. Reset the
    // data to keep track of this generator request.
    TSContDataSet(contp, grq);

    // Start reading the request from the server intercept VC.
    grq->readio.read(arg.vc, contp);
    VIODEBUG(grq->readio.vio, "started reading generator request");

    return TS_EVENT_NONE;
  }

  case TS_EVENT_NET_ACCEPT_FAILED: {
    // TS_EVENT_NET_ACCEPT_FAILED will be delivered if the
    // transaction is cancelled before we start tunnelling
    // through the server intercept. One way that this can happen
    // is if the intercept is attached early, and then we server
    // the document out of cache.
    argument_type cdata(TSContDataGet(contp));

    // There's nothing to do here except nuke the continuation
    // that was allocated in GeneratorTxnHook().
    VDEBUG("cancelling server intercept request for txn=%p", cdata.txn);

    TSContDestroy(contp);
    return TS_EVENT_NONE;
  }

  case TS_EVENT_VCONN_READ_READY: {
    argument_type        cdata    = TSContDataGet(contp);
    GeneratorHttpHeader &rqheader = cdata.grq->rqheader;

    VDEBUG("reading vio=%p vc=%p, grq=%p", arg.vio, TSVIOVConnGet(arg.vio), cdata.grq);

    TSParseResult result = TS_PARSE_CONT;

    if (cdata.grq->status == TS_HTTP_STATUS_OK && cdata.grq->flags & GeneratorRequest::ISPOST) {
      TSVIO input_vio = cdata.grq->readio.vio;

      // Look for data and if we find any, consume.
      if (TSVIOBufferGet(input_vio)) {
        TSIOBufferReader reader = cdata.grq->readio.reader;
        int64_t          n      = TSIOBufferReaderAvail(reader);
        if (n > 0) {
          TSIOBufferReaderConsume(reader, n);
          TSVIONDoneSet(input_vio, TSVIONDoneGet(input_vio) + n);
          cdata.grq->bytesSeen += n;
        }

        if (TSVIONTodoGet(input_vio) > 0 && cdata.grq->bytesSeen < cdata.grq->contentLength) {
          // signal that we can accept more data.
          TSVIOReenable(input_vio);

        } else {
          if (GeneratorPOSTResponse(cdata.grq) != TS_SUCCESS) {
            VERROR("failure writing response");
            return TS_EVENT_ERROR;
          }
        }
      } else { // buffer gone, we're done.
        VDEBUG("upstream buffer disappeared - %d bytes", cdata.grq->bytesSeen);
      }

      return TS_EVENT_NONE;
    }

    if (cdata.grq->status != TS_HTTP_STATUS_NONE && cdata.grq->status != TS_HTTP_STATUS_OK) {
      if (GeneratorWriteFailureResponse(cdata.grq, cdata.grq->status) != TS_SUCCESS) {
        VERROR("failure writing failure response");
        return TS_EVENT_ERROR;
      }
      return TS_EVENT_NONE;
    }

    for (TSIOBufferBlock blk = TSIOBufferReaderStart(cdata.grq->readio.reader); blk; blk = TSIOBufferBlockNext(blk)) {
      int64_t     nbytes;
      const char *ptr = TSIOBufferBlockReadStart(blk, cdata.grq->readio.reader, &nbytes);
      if (ptr == nullptr || nbytes == 0) {
        continue;
      }

      const char *end = ptr + nbytes;

      result = TSHttpHdrParseReq(rqheader.parser, rqheader.buffer, rqheader.header, &ptr, end);
      switch (result) {
      case TS_PARSE_ERROR:
        // If we got a bad request, just shut it down.
        VDEBUG("bad request on grq=%p, sending an error", cdata.grq);
        GeneratorRequestDestroy(cdata.grq, arg.vio, contp);
        return TS_EVENT_ERROR;

      case TS_PARSE_DONE:
        // Check the response.
        VDEBUG("parsed request on grq=%p, sending a response", cdata.grq);
        if (!GeneratorParseRequest(cdata.grq)) {
          VDEBUG("got bad response");
          cdata.grq->status = TS_HTTP_STATUS_NOT_FOUND;
          cdata.grq->nbytes = 0;

          cdata.grq->writeio.write(TSVIOVConnGet(arg.vio), contp);
          TSVIONBytesSet(cdata.grq->writeio.vio, 0);

          return TS_EVENT_NONE;
        }

        if (cdata.grq->flags & GeneratorRequest::ISPOST) {
          cdata.grq->status = TS_HTTP_STATUS_OK;
          cdata.grq->nbytes = 0;

          cdata.grq->writeio.write(TSVIOVConnGet(arg.vio), contp);
          TSVIONBytesSet(cdata.grq->writeio.vio, 0);
          return TS_EVENT_NONE;
        }

        // If this is a HEAD request, we don't need to send any bytes.
        if (cdata.grq->flags & GeneratorRequest::ISHEAD) {
          cdata.grq->nbytes = 0;
        }

        // Start the vconn write.
        cdata.grq->writeio.write(TSVIOVConnGet(arg.vio), contp);
        TSVIONBytesSet(cdata.grq->writeio.vio, 0);

        if (cdata.grq->delay > 0) {
          VDEBUG("delaying response by %ums", cdata.grq->delay);
          // TS_EVENT_TIMEOUT
          TSContScheduleOnPool(contp, cdata.grq->delay, TS_THREAD_POOL_NET);
          return TS_EVENT_NONE;
        }

        if (GeneratorWriteResponse(cdata.grq) != TS_SUCCESS) {
          VERROR("failure writing response");
          return TS_EVENT_ERROR;
        }
        return TS_EVENT_NONE;

      case TS_PARSE_CONT:
        break;
      }
    }

    TSReleaseAssert(result == TS_PARSE_CONT);

    // Reenable the read VIO to get more events.
    TSVIOReenable(arg.vio);
    return TS_EVENT_NONE;
  }

  case TS_EVENT_VCONN_WRITE_READY: {
    argument_type cdata = TSContDataGet(contp);

    if (cdata.grq->nbytes) {
      int64_t nbytes;

      if (cdata.grq->nbytes >= static_cast<ssize_t>(sizeof(GeneratorData))) {
        nbytes = sizeof(GeneratorData);
      } else {
        nbytes = cdata.grq->nbytes % sizeof(GeneratorData);
      }

      VIODEBUG(arg.vio, "writing %" PRId64 " bytes for grq=%p", nbytes, cdata.grq);
      nbytes = TSIOBufferWrite(cdata.grq->writeio.iobuf, GeneratorData, nbytes);

      cdata.grq->nbytes -= nbytes;
      TSStatIntIncrement(StatCountBytes, nbytes);

      // Update the number of bytes to write.
      TSVIONBytesSet(arg.vio, TSVIONBytesGet(arg.vio) + nbytes);
      TSVIOReenable(arg.vio);
    }

    return TS_EVENT_NONE;
  }

  case TS_EVENT_ERROR:
  case TS_EVENT_VCONN_EOS: {
    argument_type cdata = TSContDataGet(contp);

    VIODEBUG(arg.vio, "received EOS or ERROR for grq=%p", cdata.grq);
    GeneratorRequestDestroy(cdata.grq, arg.vio, contp);
    return event == TS_EVENT_ERROR ? TS_EVENT_ERROR : TS_EVENT_NONE;
  }

  case TS_EVENT_VCONN_READ_COMPLETE:
    // We read data forever, so we should never get a READ_COMPLETE.
    VIODEBUG(arg.vio, "unexpected TS_EVENT_VCONN_READ_COMPLETE");
    return TS_EVENT_NONE;

  case TS_EVENT_VCONN_WRITE_COMPLETE: {
    argument_type cdata = TSContDataGet(contp);

    // If we still have bytes to write, kick off a new write operation, otherwise
    // we are done and we can shut down the VC.
    if (cdata.grq->nbytes) {
      cdata.grq->writeio.write(TSVIOVConnGet(arg.vio), contp);
      TSVIONBytesSet(cdata.grq->writeio.vio, cdata.grq->nbytes);
    } else {
      VIODEBUG(arg.vio, "TS_EVENT_VCONN_WRITE_COMPLETE %" PRId64 " todo", TSVIONTodoGet(arg.vio));
      GeneratorRequestDestroy(cdata.grq, arg.vio, contp);
    }

    return TS_EVENT_NONE;
  }

  case TS_EVENT_TIMEOUT: {
    // Our response delay expired, so write the headers now, which
    // will also trigger the read+write event flow.
    argument_type cdata = TSContDataGet(contp);
    if (GeneratorWriteResponse(cdata.grq) != TS_SUCCESS) {
      VERROR("failure writing response");
      return TS_EVENT_ERROR;
    }
    return TS_EVENT_NONE;
  }

  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), event, arg.ptr);
    return TS_EVENT_ERROR;

  default:
    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), event, arg.ptr);
    return TS_EVENT_ERROR;
  }
}

// Little helper function, to turn off the cache on requests which aren't cacheable to begin with.
// This helps performance, a lot.
static void
CheckCacheable(TSHttpTxn txnp, TSMLoc url, TSMBuffer bufp)
{
  int         pathsz = 0;
  const char *path   = TSUrlPathGet(bufp, url, &pathsz);

  if (path && (pathsz >= 8) && (0 == memcmp(path, "nocache/", 8))) {
    // It's not cacheable, so, turn off the cache. This avoids major serialization and performance issues.
    VDEBUG("turning off the cache, uncacheable");
    TSHttpTxnConfigIntSet(txnp, TS_CONFIG_HTTP_CACHE_HTTP, 0);
  }
}

// Handle events that occur on the TSHttpTxn.
static int
GeneratorTxnHook(TSCont contp, TSEvent event, void *edata)
{
  argument_type arg(edata);

  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, TSHttpEventNameLookup(event), event, edata);

  switch (event) {
  case TS_EVENT_HTTP_READ_REQUEST_HDR: {
    TSMBuffer recp;
    TSMLoc    url_loc;
    TSMLoc    hdr_loc;

    if (TSHttpTxnClientReqGet(arg.txn, &recp, &hdr_loc) != TS_SUCCESS) {
      VERROR("failed to get client request handle");
      break;
    }
    if (TSHttpHdrUrlGet(recp, hdr_loc, &url_loc) != TS_SUCCESS) {
      VERROR("failed to get URI handle");
      break;
    }
    CheckCacheable(arg.txn, url_loc, recp);
    TSHandleMLocRelease(recp, hdr_loc, url_loc);
    TSHandleMLocRelease(recp, TS_NULL_MLOC, hdr_loc);
    break;
  }

  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE: {
    int status;

    if (TSHttpTxnCacheLookupStatusGet(arg.txn, &status) == TS_SUCCESS && status != TS_CACHE_LOOKUP_HIT_FRESH) {
      // This transaction is going to be a cache miss, so intercept it.
      VDEBUG("intercepting origin server request for txn=%p", arg.txn);
      TSHttpTxnServerIntercept(TSContCreate(GeneratorInterceptHook, TSMutexCreate()), arg.txn);
    }
    break;
  }

  default:
    VERROR("unexpected event %s (%d)", TSHttpEventNameLookup(event), event);
    break;
  }

  TSHttpTxnReenable(arg.txn, TS_EVENT_HTTP_CONTINUE);
  return TS_EVENT_NONE;
}

static void
GeneratorInitialize()
{
  TxnHook = TSContCreate(GeneratorTxnHook, nullptr);
  memset(GeneratorData, 'x', sizeof(GeneratorData));

  if (TSStatFindName("generator.response_bytes", &StatCountBytes) == TS_ERROR) {
    StatCountBytes = TSStatCreate("generator.response_bytes", TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
  }

  if (TSStatFindName("generator.response_count", &StatCountResponses) == TS_ERROR) {
    StatCountResponses =
      TSStatCreate("generator.response_count", TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_COUNT);
  }
}

void
TSPluginInit(int /* argc */, const char * /* argv */[])
{
  TSPluginRegistrationInfo info;

  info.plugin_name   = (char *)PLUGIN;
  info.vendor_name   = (char *)"Apache Software Foundation";
  info.support_email = (char *)"dev@trafficserver.apache.org";

  if (TSPluginRegister(&info) != TS_SUCCESS) {
    VERROR("plugin registration failed\n");
  }

  GeneratorInitialize();

  // We want to check early on if the request is cacheable or not, and if it's not cacheable,
  // we benefit signifciantly from turning off the cache completely.
  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TxnHook);

  // Wait until after the cache lookup to decide whether to
  // intercept a request. For cache hits we will never intercept.
  TSHttpHookAdd(TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, TxnHook);
}

TSReturnCode
TSRemapInit(TSRemapInterface * /* api_info */, char * /* errbuf */, int /* errbuf_size */)
{
  GeneratorInitialize();
  return TS_SUCCESS;
}

TSRemapStatus
TSRemapDoRemap(void * /* ih */, TSHttpTxn txn, TSRemapRequestInfo *rri)
{
  const TSHttpStatus txnstat = TSHttpTxnStatusGet(txn);
  if (txnstat != TS_HTTP_STATUS_NONE && txnstat != TS_HTTP_STATUS_OK) {
    VDEBUG("transaction status_code=%d already set; skipping processing", static_cast<int>(txnstat));
    return TSREMAP_NO_REMAP;
  }

  // Check if we should turn off the cache before doing anything else ...
  CheckCacheable(txn, rri->requestUrl, rri->requestBufp);
  TSHttpTxnHookAdd(txn, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, TxnHook);
  return TSREMAP_NO_REMAP; // This plugin never rewrites anything.
}

TSReturnCode
TSRemapNewInstance(int /* argc */, char * /* argv */[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
{
  *ih = nullptr;
  return TS_SUCCESS;
}

void
TSRemapDeleteInstance(void * /* ih */)
{
}
