/*!
 * @file message.c
 *
 * vim: expandtab:ts=2:sts=2:sw=2
 *
 * @authors
 * Copyright (C) 2020 Anoxinon e.V. 
 *
 * @copyright 
 * This file is part of xmppc.
 *
 * xmppc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * xmppc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 *
 * German
 *
 * Diese Datei ist Teil von xmppc.
 *
 * xmppc ist Freie Software: Sie können es unter den Bedingungen
 * der GNU General Public License, wie von der Free Software Foundation,
 * Version 3 der Lizenz oder (nach Ihrer Wahl) jeder neueren
 * veröffentlichten Version, weiter verteilen und/oder modifizieren.
 *
 * xmppc wird in der Hoffnung, dass es nützlich sein wird, aber
 * OHNE JEDE GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite
 * Gewährleistung der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK.
 * Siehe die GNU General Public License für weitere Details.
 *
 * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem
 * Programm erhalten haben. Wenn nicht, siehe <https://www.gnu.org/licenses/>.
 */

#include "message.h"
#include "glib.h"
#include "stdbool.h"
#include "stdlib.h"
#include "string.h"

#define MAM_NS "urn:xmpp:mam:2"
#define FORWARD_NS "urn:xmpp:forward:0"
#define RSM_NS "http://jabber.org/protocol/rsm"
#define SID_NS "urn:xmpp:sid:0"

#define MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS 15
#define MESSAGE_ARCHIVE_LOOKUP_POLL_MS 1000

typedef struct {
  xmppc_t *xmppc;
  char *to;
  char *message_id;
  char *start;
  char *queryid;
  char *archive_id;
  gint64 deadline_us;
  bool done;
} message_archive_lookup_t;

static void _message_send_text(xmppc_t *xmppc, char* to, char* message);
static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id);
static void _message_send_archive_query(message_archive_lookup_t *lookup);
static int _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata);
static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata);
static bool _message_archive_result_matches(message_archive_lookup_t *lookup, xmpp_stanza_t *result);
static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id);
static void _message_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text);
static void _message_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type);
static char *_message_lookup_start_time(void);
static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup, bool delete_id_handler);
static void _message_archive_lookup_free(message_archive_lookup_t *lookup);
static void _message_print_sent(message_archive_lookup_t *lookup);
static void _message_print_escaped_attr(const char *text);

void message_execute_command(xmppc_t *xmppc, int argc, char *argv[]) {
  if( argc == 3 && ( strcmp("chat", argv[0] ) == 0)) {
    _message_send_text(xmppc, argv[1],argv[2]);
  } else {
    logError(xmppc, "Befehl unbekannt");
    xmpp_disconnect(xmppc->conn);
  }
}

void _message_send_text(xmppc_t *xmppc, char* to, char* text) {
  xmpp_conn_t *conn = xmppc->conn;
  xmpp_ctx_t *ctx = xmpp_conn_get_context(conn);
  xmpp_stanza_t *message;
  char* id = xmpp_uuid_gen(xmppc->ctx);
  message = xmpp_message_new(ctx, "chat", to, id);
  int res = xmpp_message_set_body(message, text);
  if(res == 0) {
    xmpp_stanza_t *origin = xmpp_stanza_new(ctx);
    xmpp_stanza_set_name(origin, "origin-id");
    xmpp_stanza_set_ns(origin, SID_NS);
    xmpp_stanza_set_attribute(origin, "id", id);
    xmpp_stanza_add_child(message, origin);
    xmpp_stanza_release(origin);

    xmpp_send(conn, message);
    _message_lookup_archive_id(xmppc, to, id);
  } else {
    xmpp_disconnect(conn);
  }
  xmpp_stanza_release(message);
  free(id);
}

static void _message_lookup_archive_id(xmppc_t *xmppc, const char *to, const char *message_id) {
  message_archive_lookup_t *lookup = g_new0(message_archive_lookup_t, 1);
  lookup->xmppc = xmppc;
  lookup->to = g_strdup(to);
  lookup->message_id = g_strdup(message_id);
  lookup->start = _message_lookup_start_time();
  lookup->deadline_us = g_get_monotonic_time() + ((gint64)MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * G_USEC_PER_SEC);

  xmpp_handler_add(xmppc->conn, _message_archive_result_handler, NULL, "message", NULL, lookup);
  xmpp_timed_handler_add(xmppc->conn, _message_archive_lookup_timeout, MESSAGE_ARCHIVE_LOOKUP_TIMEOUT_SECONDS * 1000, lookup);
  _message_send_archive_query(lookup);
}

static void _message_send_archive_query(message_archive_lookup_t *lookup) {
  xmppc_t *xmppc = lookup->xmppc;
  xmpp_ctx_t *ctx = xmppc->ctx;
  char* id = xmpp_uuid_gen(ctx);
  g_free(lookup->queryid);
  lookup->queryid = g_strdup(id);

  xmpp_stanza_t *iq = xmpp_iq_new(ctx, "set", id);
  xmpp_stanza_t *query = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(query, "query");
  xmpp_stanza_set_ns(query, MAM_NS);
  xmpp_stanza_set_attribute(query, "queryid", id);

  xmpp_stanza_t* x = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(x,"x");
  xmpp_stanza_set_ns(x, "jabber:x:data");
  xmpp_stanza_set_type(x,"submit");
  xmpp_stanza_add_child(query,x);

  _message_add_field(ctx, x, "FORM_TYPE", MAM_NS, "hidden");
  _message_add_field(ctx, x, "with", lookup->to, NULL);
  _message_add_field(ctx, x, "start", lookup->start, NULL);

  xmpp_stanza_t *set = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(set, "set");
  xmpp_stanza_set_ns(set, RSM_NS);
  _message_add_text_child(ctx, set, "max", "50");
  _message_add_text_child(ctx, set, "before", "");
  xmpp_stanza_add_child(query, set);

  xmpp_stanza_add_child(iq, query);
  xmpp_id_handler_add(xmppc->conn, _message_archive_query_finished, id, lookup);
  xmpp_send(xmppc->conn, iq);

  xmpp_stanza_release(iq);
  xmpp_stanza_release(query);
  xmpp_stanza_release(x);
  xmpp_stanza_release(set);
  free(id);
}

static int _message_archive_result_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
  xmpp_stanza_t* result = xmpp_stanza_get_child_by_name(stanza,"result");

  if (!lookup->done && result && _message_archive_result_matches(lookup, result)) {
    const char *archive_id = xmpp_stanza_get_attribute(result, "id");
    if (archive_id) {
      g_free(lookup->archive_id);
      lookup->archive_id = g_strdup(archive_id);
    }
  }

  return 1;
}

static int _message_archive_query_finished(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) {
  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;
  const char *type = xmpp_stanza_get_type(stanza);

  if (lookup->done) {
    return 0;
  }

  if (lookup->archive_id) {
    _message_finish_sent(conn, lookup, false);
    return 0;
  }

  if (type && strcmp(type, "error") == 0) {
    logError(lookup->xmppc, "MAM archive lookup failed\n");
    _message_finish_sent(conn, lookup, false);
    return 0;
  }

  if (g_get_monotonic_time() < lookup->deadline_us) {
    xmpp_timed_handler_add(conn, _message_archive_query_poll, MESSAGE_ARCHIVE_LOOKUP_POLL_MS, lookup);
  } else {
    logError(lookup->xmppc, "MAM archive lookup timed out\n");
    _message_finish_sent(conn, lookup, false);
  }

  return 0;
}

static int _message_archive_query_poll(xmpp_conn_t *const conn, void *const userdata) {
  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;

  if (lookup->done) {
    return 0;
  }

  if (g_get_monotonic_time() >= lookup->deadline_us) {
    logError(lookup->xmppc, "MAM archive lookup timed out\n");
    _message_finish_sent(conn, lookup, false);
    return 0;
  }

  _message_send_archive_query(lookup);
  return 0;
}

static int _message_archive_lookup_timeout(xmpp_conn_t *const conn, void *const userdata) {
  message_archive_lookup_t *lookup = (message_archive_lookup_t *)userdata;

  if (!lookup->done) {
    logError(lookup->xmppc, "MAM archive lookup timed out\n");
    _message_finish_sent(conn, lookup, true);
  }

  return 0;
}

static bool _message_archive_result_matches(message_archive_lookup_t *lookup, xmpp_stanza_t *result) {
  const char *ns = xmpp_stanza_get_ns(result);
  if (!ns || strcmp(MAM_NS, ns) != 0) {
    return false;
  }

  const char *queryid = xmpp_stanza_get_attribute(result, "queryid");
  if (queryid && lookup->queryid && strcmp(queryid, lookup->queryid) != 0) {
    return false;
  }

  xmpp_stanza_t* forwarded = xmpp_stanza_get_child_by_ns(result, FORWARD_NS);
  xmpp_stanza_t* message = forwarded ? xmpp_stanza_get_child_by_name(forwarded, "message") : NULL;
  if (!message) {
    return false;
  }

  const char *message_id = xmpp_stanza_get_id(message);
  if (message_id && strcmp(message_id, lookup->message_id) == 0) {
    return true;
  }

  return _message_has_origin_id(message, lookup->message_id);
}

static bool _message_has_origin_id(xmpp_stanza_t *message, const char *id) {
  for (xmpp_stanza_t *child = xmpp_stanza_get_children(message); child; child = xmpp_stanza_get_next(child)) {
    const char *name = xmpp_stanza_get_name(child);
    const char *ns = xmpp_stanza_get_ns(child);
    const char *origin_id = xmpp_stanza_get_attribute(child, "id");

    if (name && ns && origin_id && strcmp(name, "origin-id") == 0 && strcmp(ns, SID_NS) == 0 && strcmp(origin_id, id) == 0) {
      return true;
    }
  }

  return false;
}

static void _message_add_text_child(xmpp_ctx_t *ctx, xmpp_stanza_t *parent, const char *name, const char *text) {
  xmpp_stanza_t *child = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(child, name);

  xmpp_stanza_t *body = xmpp_stanza_new(ctx);
  xmpp_stanza_set_text(body, text ? text : "");
  xmpp_stanza_add_child(child, body);
  xmpp_stanza_release(body);

  xmpp_stanza_add_child(parent, child);
  xmpp_stanza_release(child);
}

static void _message_add_field(xmpp_ctx_t *ctx, xmpp_stanza_t *x, const char *var, const char *value, const char *type) {
  xmpp_stanza_t* field = xmpp_stanza_new(ctx);
  xmpp_stanza_set_name(field,"field");
  xmpp_stanza_set_attribute(field, "var", var);
  if (type) {
    xmpp_stanza_set_type(field, type);
  }

  _message_add_text_child(ctx, field, "value", value);
  xmpp_stanza_add_child(x, field);
  xmpp_stanza_release(field);
}

static char *_message_lookup_start_time(void) {
  GDateTime *now = g_date_time_new_now_utc();
  GDateTime *then = g_date_time_add_seconds(now, -30);
  char *formatted = g_date_time_format(then, "%Y-%m-%dT%H:%M:%SZ");
  g_date_time_unref(then);
  g_date_time_unref(now);
  return formatted;
}

static void _message_finish_sent(xmpp_conn_t *conn, message_archive_lookup_t *lookup, bool delete_id_handler) {
  if (lookup->done) {
    return;
  }

  lookup->done = true;
  _message_print_sent(lookup);
  xmpp_handler_delete(conn, _message_archive_result_handler);
  xmpp_timed_handler_delete(conn, _message_archive_query_poll);
  xmpp_timed_handler_delete(conn, _message_archive_lookup_timeout);
  if (delete_id_handler && lookup->queryid) {
    xmpp_id_handler_delete(conn, _message_archive_query_finished, lookup->queryid);
  }
  _message_archive_lookup_free(lookup);
  xmpp_disconnect(conn);
}

static void _message_archive_lookup_free(message_archive_lookup_t *lookup) {
  g_free(lookup->to);
  g_free(lookup->message_id);
  g_free(lookup->start);
  g_free(lookup->queryid);
  g_free(lookup->archive_id);
  g_free(lookup);
}

static void _message_print_sent(message_archive_lookup_t *lookup) {
  printf("<sent archive-id=\"");
  _message_print_escaped_attr(lookup->archive_id ? lookup->archive_id : "");
  printf("\" message-id=\"");
  _message_print_escaped_attr(lookup->message_id);
  printf("\" to=\"");
  _message_print_escaped_attr(lookup->to);
  printf("\"/>\n");
  fflush(stdout);
}

static void _message_print_escaped_attr(const char *text) {
  const unsigned char *p = (const unsigned char *)(text ? text : "");

  while (*p) {
    switch (*p) {
      case '&':
        printf("&amp;");
        break;
      case '<':
        printf("&lt;");
        break;
      case '>':
        printf("&gt;");
        break;
      case '"':
        printf("&quot;");
        break;
      case '\n':
        printf("&#10;");
        break;
      case '\r':
        printf("&#13;");
        break;
      case '\t':
        printf("&#9;");
        break;
      default:
        if (*p < 0x20) {
          printf("&#x%02X;", *p);
        } else {
          putchar(*p);
        }
        break;
    }
    p++;
  }
}
