/**
  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 <iostream>
#include <string_view>
#include "tscpp/api/GlobalPlugin.h"
#include "tscpp/api/TransactionPlugin.h"
#include "tscpp/api/TransformationPlugin.h"
#include "tscpp/api/GzipInflateTransformation.h"
#include "tscpp/api/GzipDeflateTransformation.h"
#include "tscpp/api/PluginInit.h"
#include "tscpp/api/Logger.h"

using namespace atscppapi;
using namespace atscppapi::transformations;
using std::string;

#define TAG "gzip_transformation"

namespace
{
GlobalPlugin *plugin;
}

/*
 * Note, the GzipInflateTransformation and GzipDeflateTransformation do not
 * check headers to determine if the content was gzipped and it doesn't check
 * headers to make sure the client supports gzip, this is entirely up to the plugin
 * to verify that the content-encoding is gzipped, it's also up to the client
 * to make sure the user's accept-encoding supports gzip.
 *
 * Read this example very carefully, in this example we modify the Accept-Encoding
 * header to the origin to ensure that we will be returned gzipped or identity encoding
 * content. If we receive gzipped content we will inflate it, we will apply our transformation
 * and if the client supports gzip we will deflate the content that we transformed. Finally,
 * if the user supported gzip (then they got gzip) and we must make sure the content-encoding
 * header is correctly set on the way out.
 */

class Helpers
{
public:
  static bool
  clientAcceptsGzip(Transaction &transaction)
  {
    return transaction.getClientRequest().getHeaders().values("Accept-Encoding").find("gzip") != string::npos;
  }

  static bool
  serverReturnedGzip(Transaction &transaction)
  {
    return transaction.getServerResponse().getHeaders().values("Content-Encoding").find("gzip") != string::npos;
  }

  enum ContentType {
    UNKNOWN    = 0,
    TEXT_HTML  = 1,
    TEXT_PLAIN = 2,
  };

  static ContentType
  getContentType(Transaction &transaction)
  {
    if (transaction.getServerResponse().getHeaders().values("Content-Type").find("text/html") != string::npos) {
      return TEXT_HTML;
    } else if (transaction.getServerResponse().getHeaders().values("Content-Type").find("text/plain") != string::npos) {
      return TEXT_PLAIN;
    } else {
      return UNKNOWN;
    }
  }
};

class SomeTransformationPlugin : public TransformationPlugin
{
public:
  SomeTransformationPlugin(Transaction &transaction)
    : TransformationPlugin(transaction, RESPONSE_TRANSFORMATION), transaction_(transaction)
  {
    registerHook(HOOK_SEND_RESPONSE_HEADERS);
  }

  void
  handleSendResponseHeaders(Transaction &transaction) override
  {
    TS_DEBUG(TAG, "Added X-Content-Transformed header");
    transaction.getClientResponse().getHeaders()["X-Content-Transformed"] = "1";
    transaction.resume();
  }

  void
  consume(std::string_view data) override
  {
    produce(data);
  }

  void
  handleInputComplete() override
  {
    Helpers::ContentType content_type = Helpers::getContentType(transaction_);
    if (content_type == Helpers::TEXT_HTML) {
      TS_DEBUG(TAG, "Adding an HTML comment at the end of the page");
      produce("\n<br /><!-- Gzip Transformation Plugin Was Here -->");
    } else if (content_type == Helpers::TEXT_PLAIN) {
      TS_DEBUG(TAG, "Adding a text comment at the end of the page");
      produce("\nGzip Transformation Plugin Was Here");
    } else {
      TS_DEBUG(TAG, "Unable to add TEXT or HTML comment because content type was not text/html or text/plain.");
    }
    setOutputComplete();
  }

  ~SomeTransformationPlugin() override {}

private:
  Transaction &transaction_;
};

class GlobalHookPlugin : public GlobalPlugin
{
public:
  GlobalHookPlugin()
  {
    registerHook(HOOK_SEND_REQUEST_HEADERS);
    registerHook(HOOK_READ_RESPONSE_HEADERS);
    registerHook(HOOK_SEND_RESPONSE_HEADERS);
  }

  void
  handleSendRequestHeaders(Transaction &transaction) override
  {
    // Since we can only decompress gzip we will change the accept encoding header
    // to gzip, even if the user cannot accept gzipped content we will return to them
    // uncompressed content in that case since we have to be able to transform the content.
    string original_accept_encoding = transaction.getServerRequest().getHeaders().values("Accept-Encoding");

    // Make sure it's done on the server request to avoid clobbering the clients original accept encoding header.
    transaction.getServerRequest().getHeaders()["Accept-Encoding"] = "gzip";
    TS_DEBUG(TAG, "Changed the servers request accept encoding header from \"%s\" to gzip", original_accept_encoding.c_str());

    transaction.resume();
  }

  void
  handleReadResponseHeaders(Transaction &transaction) override
  {
    TS_DEBUG(TAG, "Determining if we need to add an inflate transformation or a deflate transformation..");
    // We're guaranteed to have been returned either gzipped content or Identity.

    if (Helpers::serverReturnedGzip(transaction)) {
      // If the returned content was gzipped we will inflate it so we can transform it.
      TS_DEBUG(TAG, "Creating Inflate Transformation because the server returned gzipped content");
      transaction.addPlugin(new GzipInflateTransformation(transaction, TransformationPlugin::RESPONSE_TRANSFORMATION));
    }

    transaction.addPlugin(new SomeTransformationPlugin(transaction));

    // Even if the server didn't return gzipped content, if the user supports it we will gzip it.
    if (Helpers::clientAcceptsGzip(transaction)) {
      TS_DEBUG(TAG, "The client supports gzip so we will deflate the content on the way out.");
      transaction.addPlugin(new GzipDeflateTransformation(transaction, TransformationPlugin::RESPONSE_TRANSFORMATION));
    }
    transaction.resume();
  }

  void
  handleSendResponseHeaders(Transaction &transaction) override
  {
    // If the client supported gzip then we can guarantee they are receiving gzip since regardless of the
    // origins content-encoding we returned gzip, so let's make sure the content-encoding header is correctly
    // set to gzip or identity.
    if (Helpers::clientAcceptsGzip(transaction)) {
      TS_DEBUG(TAG, "Setting the client response content-encoding to gzip since the user supported it, that's what they got.");
      transaction.getClientResponse().getHeaders()["Content-Encoding"] = "gzip";
    } else {
      TS_DEBUG(TAG, "Setting the client response content-encoding to identity since the user didn't support gzip");
      transaction.getClientResponse().getHeaders()["Content-Encoding"] = "identity";
    }
    transaction.resume();
  }
};

void
TSPluginInit(int argc ATSCPPAPI_UNUSED, const char *argv[] ATSCPPAPI_UNUSED)
{
  if (!RegisterGlobalPlugin("CPP_Example_GzipTransformation", "apache", "dev@trafficserver.apache.org")) {
    return;
  }
  TS_DEBUG(TAG, "TSPluginInit");
  plugin = new GlobalHookPlugin();
}
