/*******************************************************************************
 mod_unified_remix.cpp

 mod_unified_remix - An Apache module for Unified Remix

 Copyright (C) 2015-2026 CodeShop B.V.

 For licensing see the LICENSE file
******************************************************************************/

#include <httpd.h>
#include <http_core.h>
#include <http_config.h>
#include <http_protocol.h>
#include <http_log.h>
#include <http_request.h> // for ap_update_mtime
#include <apr_version.h>
#include <apr_strings.h>  // for apr_itoa, apr_pstrcat
#include <apr_buckets.h>

#include <mod_streaming_export.h>
#include <unified_remix.h>
#include <mp4_process.h>
#include <output_bucket.h>
#include <headers_t.h>

#include <apr_buckets_usp.h>
#include <log_util.h>
#include <subrequest.h>

typedef struct
{
  mp4_global_context_t const* global_context;
  char const* license;
} server_config_t;

typedef struct
{
  int usp_enable_subreq;

  char* s3_secret_key;
  char* s3_access_key;
  char* s3_region;
  char* s3_security_token;
  int s3_use_headers;

  char* transcode_proxy_pass;
} dir_config_t;

static void* create_server_config(apr_pool_t* pool, server_rec* svr);
static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv);
static void* create_dir_config(apr_pool_t* pool, char* dummy);
static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv);

static void register_hooks(apr_pool_t *p);
static char const* set_usp_license_key(cmd_parms* cmd, void* cfg,
                                       char const* arg);

#if defined(__GNUC__)
// Suppress unavoidable gcc and clang warnings in command_rec initialization.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif // __GNUC__

static command_rec const remix_cmds[] =
{
  AP_INIT_RAW_ARGS("UspLicenseKey",
    reinterpret_cast<cmd_func>(set_usp_license_key),
    NULL,
    RSRC_CONF|ACCESS_CONF,
    "Set to USP license key"),

  AP_INIT_FLAG("UspEnableSubreq",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_enable_subreq)),
    ACCESS_CONF,
    "Set to 'on' to enable use of subrequests"),

  AP_INIT_TAKE1("S3SecretKey",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_secret_key)),
    ACCESS_CONF,
    "Pass the Amazon S3 Secret Key"),

  AP_INIT_TAKE1("S3AccessKey",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_access_key)),
    ACCESS_CONF,
    "Pass the Amazon S3 Access Key)"),

  AP_INIT_TAKE1("S3Region",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_region)),
    ACCESS_CONF,
    "Pass the Amazon S3 Region"),

  AP_INIT_TAKE1("S3SecurityToken",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_security_token)),
    ACCESS_CONF,
    "Pass the Amazon S3 Security Token"),

  AP_INIT_FLAG("S3UseHeaders",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_use_headers)),
    ACCESS_CONF,
    "Set to 'on' to use headers for Amazon S3 authentication"),

  AP_INIT_TAKE1("TranscodeProxyPass",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, transcode_proxy_pass)),
    ACCESS_CONF,
    "Pass transcoding to proxy"),

  { NULL }
};

#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif // __GNUC__

extern "C" {

#if defined(APLOG_USE_MODULE)
APLOG_USE_MODULE(unified_remix);
#endif

static const char remix_filter_name[]="REMIX";

struct module_struct MOD_STREAMING_DLL_EXPORT unified_remix_module =
{
  STANDARD20_MODULE_STUFF,
  create_dir_config,
  merge_dir_config,
  create_server_config,
  merge_server_config,
  remix_cmds,
  register_hooks,
  AP_MODULE_FLAG_ALWAYS_MERGE
};

typedef struct remix_ctx
{
  apr_bucket_brigade* bb;
} remix_ctx;

} // extern C definitions

static void* create_server_config(apr_pool_t* pool, server_rec* /* svr */)
{
  auto* config = static_cast<server_config_t*>(
    apr_pcalloc(pool, sizeof(server_config_t)));

  config->global_context = NULL;
  config->license = NULL;

  return config;
}

static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<server_config_t*>(basev);
  auto* add = static_cast<server_config_t*>(addv);
  auto* res = static_cast<server_config_t*>(
    apr_pcalloc(pool, sizeof(server_config_t)));

  res->license = add->license == NULL ? base->license : add->license;

  return res;
}

static void* create_dir_config(apr_pool_t* pool, char* /* dummy */)
{
  auto* config = static_cast<dir_config_t*>(
    apr_pcalloc(pool, sizeof(dir_config_t)));

  config->usp_enable_subreq = -1;
  config->s3_secret_key = NULL;
  config->s3_access_key = NULL;
  config->s3_region = NULL;
  config->s3_security_token = NULL;
  config->s3_use_headers = -1;
  config->transcode_proxy_pass = NULL;

  return config;
}

static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<dir_config_t*>(basev);
  auto* add = static_cast<dir_config_t*>(addv);
  auto* res = static_cast<dir_config_t*>(
    apr_pcalloc(pool, sizeof(dir_config_t)));

  res->usp_enable_subreq = add->usp_enable_subreq == -1 ?
    base->usp_enable_subreq : add->usp_enable_subreq;
  res->s3_secret_key = add->s3_secret_key == NULL ?
    base->s3_secret_key : add->s3_secret_key;
  res->s3_access_key = add->s3_access_key == NULL ?
    base->s3_access_key : add->s3_access_key;
  res->s3_region = add->s3_region == NULL ?
    base->s3_region : add->s3_region;
  res->s3_security_token = add->s3_security_token == NULL ?
    base->s3_security_token : add->s3_security_token;
  res->s3_use_headers = add->s3_use_headers == -1 ?
    base->s3_use_headers : add->s3_use_headers;
  res->transcode_proxy_pass = add->transcode_proxy_pass == NULL ?
    base->transcode_proxy_pass : add->transcode_proxy_pass;

  return res;
}

static apr_status_t cleanup_libfmp4(void* data)
{
  auto const* global_context = static_cast<mp4_global_context_t const*>(data);
  libfmp4_global_exit(global_context);

  return APR_SUCCESS;
}

static int remix_post_config(apr_pool_t* p, apr_pool_t* /* plog */,
                             apr_pool_t* /* ptemp */, server_rec* s)
{
  char const* src = "apache mod_unified_remix";
  char const* version = X_MOD_SMOOTH_STREAMING_VERSION;

  mp4_global_context_t* global_context = NULL;
  server_config_t* conf = NULL;
  char const* license = NULL;
  void* tmp = NULL;
  char const* key = "mp4_global_context";

#if 1 // prevent double initialization
  apr_pool_userdata_get(&tmp, key, s->process->pool);
  if (tmp == NULL)
  {
    apr_pool_userdata_set(reinterpret_cast<void const*>(1), key,
                          apr_pool_cleanup_null, s->process->pool);
    return OK;
  }
#endif

  global_context = libfmp4_global_init();
  apr_pool_cleanup_register(p, reinterpret_cast<void const*>(global_context),
                            cleanup_libfmp4, apr_pool_cleanup_null);

  for (; s; s = s->next)
  {
    conf = static_cast<server_config_t*>(
      ap_get_module_config(s->module_config, &unified_remix_module));

    conf->global_context = global_context; // set pointer

    if (conf->license) // find license key
    {
      license = conf->license;
    }
  }

  char const* policy_check_result =
    libfmp4_load_license(global_context, src, version, license);

  if(policy_check_result != NULL)
  {
    ap_log_error(APLOG_MARK, APLOG_CRIT, APR_SUCCESS, s, "%s",
                 policy_check_result);
  }
  else if(license)
  {
    ap_log_error(APLOG_MARK, APLOG_NOTICE, APR_SUCCESS, s,
                 "License key found: %s", license);
  }

  return OK;
}

static void log_error_callback(void* context, fmp4_log_level_t fmp4_level,
                               char const* msg, size_t size)
{
  auto const* request = static_cast<request_rec const*>(context);

  int ap_level = fmp4_log_level_to_apache_log_level(fmp4_level);
  int clamped_size = size < INT_MAX ? static_cast<int>(size) : INT_MAX;
  ap_log_rerror(APLOG_MARK, ap_level, 0, request, "%.*s", clamped_size, msg);
}

static apr_status_t mp4_process_context_cleanup(void* data)
{
  auto const* context = static_cast<mp4_process_context_t const*>(data);
  mp4_process_context_exit(context);

  return APR_SUCCESS;
}

static char const proxy_scheme[] = "proxy:";
static size_t const proxy_scheme_len = sizeof proxy_scheme - 1;

// transform SMIL to a remixed MP4.
static apr_status_t transform(ap_filter_t* f)
{
  request_rec* r = f->r;
  auto* ctx = static_cast<remix_ctx*>(f->ctx);
//  conn_rec *c = r->connection;
  apr_status_t rv = APR_SUCCESS;

  // create an fmp4 context.
  auto* server_conf = static_cast<server_config_t*>(
    ap_get_module_config(r->server->module_config, &unified_remix_module));

  auto* dir_conf = static_cast<dir_config_t*>(
    ap_get_module_config(r->per_dir_config, &unified_remix_module));

  mp4_process_context_t* context = mp4_process_context_init(server_conf->global_context);
  apr_pool_cleanup_register(r->pool, context, mp4_process_context_cleanup,
    apr_pool_cleanup_null);
  ap_set_module_config(r->request_config, &unified_remix_module, context);

  char const* filename = r->filename;

  mp4_process_context_set_log_error_callback(context, log_error_callback, r);

  // if the SMIL is proxied, then remove the magic string "proxy:".
  if(strncmp(r->filename, proxy_scheme, proxy_scheme_len) == 0)
  {
    filename = r->filename + proxy_scheme_len;
  }

  if (dir_conf->usp_enable_subreq > 0)
  {
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r,
      "enabling download via subrequest, hostname=%s uri=%s, filename=%s "
      "args=%s", r->hostname, r->uri, r->filename, r->args);
    mp4_process_context_set_download_callback(context, subreq_download, r);
  }

  mp4_process_context_set_s3_parameters(context, dir_conf->s3_secret_key,
    dir_conf->s3_access_key, dir_conf->s3_region, dir_conf->s3_security_token,
    dir_conf->s3_use_headers == -1 ? 0 : dir_conf->s3_use_headers);
  mp4_process_context_set_transcode_proxy_pass(context,
    dir_conf->transcode_proxy_pass);

  // convert the Apache brigade to our bucket structure.
  rv = apache_brigade_to_usp_buckets(
    ctx->bb, mp4_process_context_get_buckets(context));
  if(rv != APR_SUCCESS)
  {
    return rv;
  }

  // prepare the Apache brigade for the output.
  apr_brigade_cleanup(ctx->bb);

  // TODO: we won't work on HEAD requests. We would like to, but the request
  // method is passed to ProxyPass. Since we are transforming the content
  // in an output filter, we should change the HEAD to GET to fetch the
  // content from the proxy.
  //
  // Note that the HEAD headers we return are still the same as the GET headers.
  // Most notably these are the headers used for caching: "ETag" and
  // "Cache-Control" (since they are correctly set by the SMIL origin).
  // The exception is the "Content-Length", this is not returned.
  if(!r->header_only)
  {
    // create a remixed MP4 from the given SMIL input.
    int http_status = unified_remix(context, filename);

    fmp4_result result = mp4_process_context_get_result(context);
    if(result != FMP4_OK)
    {
      char const* result_text = mp4_process_context_get_result_text(context);
      ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_SUCCESS, r,
                    "unified_remix(%s) returned: %s %s",
                    r->filename,
                    fmp4_result_to_string(result),
                    result_text);

      r->status = http_status;

      return OK;
    }
  }

  // Content-Type
  {
    // since we transform smil to mp4, let's set the new type.
    headers_t* headers = mp4_process_context_get_headers(context);
    ap_set_content_type(r, headers_get_content_type(headers));
  }

  // Content-Length
  {
    // HTTP response filters modifying the length of the body they process must
    // unset the Content-Length header.

    // The header must be unset before any output is sent from the filter. Also
    // for the core content length filter to work, the first brigade passed to
    // the next filter must include the EOS.
    apr_table_unset(r->headers_out, "Content-Length");
  }

  // convert our bucket structure to the Apache brigade.
  rv = usp_buckets_to_apache_brigade(
    mp4_process_context_get_buckets(context), ctx->bb, r);
  if(rv != APR_SUCCESS)
  {
    return rv;
  }

  return APR_SUCCESS;
}

static apr_status_t remix_out_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
  request_rec *r = f->r;
  auto* ctx = static_cast<remix_ctx*>(f->ctx);
  conn_rec *c = r->connection;
  apr_status_t rv = APR_SUCCESS;

  // Module version info
  apr_table_setn(r->headers_out, "X-REMIX", fmp4_version_string());

  // Output filters should not pass empty brigades down the filter chain,
  // but should be tolerant of being passed empty brigades.
  if(APR_BRIGADE_EMPTY(bb))
  {
    return APR_SUCCESS;
  }

  // first time in? create a context.
  if(!ctx)
  {
    // we won't work on subrequests.
    if(!ap_is_initial_req(r))
    {
      ap_remove_output_filter(f);
      return ap_pass_brigade(f->next, bb);
    }

    f->ctx = apr_pcalloc(r->pool, sizeof(remix_ctx));
    ctx = static_cast<remix_ctx*>(f->ctx);
    ctx->bb = apr_brigade_create(r->pool, c->bucket_alloc);
  }

  while(rv == APR_SUCCESS && !APR_BRIGADE_EMPTY(bb))
  {
    apr_bucket* e = APR_BRIGADE_FIRST(bb);

    if(APR_BUCKET_IS_EOS(e))
    {
      ap_remove_output_filter(f);

      // transform SMIL to a remixed MP4.
      transform(f);

      // add the EOS before passing the brigade to the next filter.
      APR_BUCKET_REMOVE(e);
      APR_BRIGADE_INSERT_TAIL(ctx->bb, e);

      rv = ap_pass_brigade(f->next, ctx->bb);

      // pass any stray buckets after the EOS down the stack.
      if(rv == APR_SUCCESS && !APR_BRIGADE_EMPTY(bb))
      {
        rv = ap_pass_brigade(f->next, bb);
        apr_brigade_cleanup(bb);
      }
    } else
    if(APR_BUCKET_IS_METADATA(e))
    {
      // metadata buckets are preserved as is.
      APR_BUCKET_REMOVE(e);
      APR_BRIGADE_INSERT_TAIL(ctx->bb, e);
    } else
    {
      const char *data;
      apr_size_t len;

      rv = apr_bucket_read(e, &data, &len, APR_BLOCK_READ);
      apr_brigade_write(ctx->bb, NULL, NULL, data, len);

      apr_bucket_delete(e);
    }
  }

  return rv;
}

static char const* set_usp_license_key(cmd_parms* cmd, void* /* cfg */,
                                       char const* arg)
{
  server_rec* s = cmd->server;
  auto* conf = static_cast<server_config_t*>(
    ap_get_module_config(s->module_config, &unified_remix_module));

  conf->license = arg;

  return NULL;
}

static void register_hooks(apr_pool_t* /* p */)
{
  /* module initializer */
  ap_hook_post_config(remix_post_config, NULL, NULL, APR_HOOK_MIDDLE);

  subreq_init();

  ap_register_output_filter(remix_filter_name, remix_out_filter, NULL,
                            AP_FTYPE_RESOURCE);
}

// End Of File

