/*
 * Copyright (C) 2025 The Phosh Developers
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "cbd-service-providers"

#include "cbd-channel-manager.h"
#include "cbd-service-providers.h"
#include "lcb-enums.h"

#include <gio/gio.h>
#include <gmobile.h>

typedef enum {
  PARSER_TOPLEVEL = 0,
  PARSER_COUNTRY,
  PARSER_CBS,
  PARSER_LEVEL,
  PARSER_DONE,
  PARSER_ERROR
} GsdParseContextState;


typedef struct {
  GMarkupParseContext  *ctx;
  char                 *country;
  char                  buffer[4096];

  char                 *text_buffer;
  GsdParseContextState  state;

  gboolean              country_found;
  LcbSeverityLevel      current_level;
  GArray               *channels;
} CbdParseContext;


typedef struct
{
  GAsyncResult *res;
  GMainLoop *loop;
} GetChannelsSyncData;


static void
cbd_parse_context_free (CbdParseContext *parse_context)
{
  g_markup_parse_context_free (parse_context->ctx);
  g_free (parse_context->country);
  g_clear_pointer (&parse_context->channels, g_array_unref);
  g_clear_pointer (&parse_context->text_buffer, g_free);

  g_free (parse_context);
}


static void
parser_toplevel_start (CbdParseContext  *parse_context,
                       const char       *name,
                       const char      **attribute_names,
                       const char      **attribute_values)
{
  if (g_str_equal (name, "serviceproviders")) {
    for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
      if (g_str_equal (attribute_names[i], "format")) {
        if (!g_str_equal (attribute_values[i], "2.0")) {
          g_warning ("Mobile broadband provider database format '%s' not supported.",
                     attribute_values[i]);
          parse_context->state = PARSER_ERROR;
          break;
        }
      }
    }
  } else if (g_str_equal (name, "country")) {
    parse_context->state = PARSER_COUNTRY;
    parse_context->country_found = FALSE;

    for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
      if (g_str_equal (attribute_names[i], "code")) {
        if (g_str_equal (attribute_values[i], parse_context->country)) {
          parse_context->country_found = TRUE;
          break;
        }
      }
    }
  }
}


static void
parser_country_start (CbdParseContext  *parse_context,
                      const char       *name,
                      const char      **attribute_names,
                      const char      **attribute_values)
{
  if (g_str_equal (name, "cbs"))
    parse_context->state = PARSER_CBS;
}


static LcbSeverityLevel
type_to_level (const char *type)
{
  g_debug ("%s", type);

  if (g_str_equal (type, "presidential")) {
    return LCB_SEVERITY_LEVEL_PRESIDENTIAL;
  } else if (g_str_equal (type, "extreme")) {
    return LCB_SEVERITY_LEVEL_EXTREME;
  } else if (g_str_equal (type, "severe")) {
    return LCB_SEVERITY_LEVEL_SEVERE;
  } else if (g_str_equal (type, "public-safety")) {
    return LCB_SEVERITY_LEVEL_PUBLIC_SAFETY;
  } else if (g_str_equal (type, "amber")) {
    return LCB_SEVERITY_LEVEL_AMBER;
  } else if (g_str_equal (type, "test")) {
    return LCB_SEVERITY_LEVEL_TEST;
  }

  return LCB_SEVERITY_LEVEL_UNKNOWN;
}


static void
parser_cbs_start (CbdParseContext *parse_context,
                  const char      *name,
                  const char     **attribute_names,
                  const char     **attribute_values)
{
  const char *type = NULL;

  parse_context->current_level = LCB_SEVERITY_LEVEL_UNKNOWN;
  if (g_str_equal (name, "level")) {
    for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
      if (g_str_equal (attribute_names[i], "type")) {
        type = attribute_values[i];
        parse_context->current_level = type_to_level (type);
        break;
      }
    }
    parse_context->state = PARSER_LEVEL;
  }
}


static void
parser_level_start (CbdParseContext *parse_context,
                    const char      *name,
                    const char     **attribute_names,
                    const char     **attribute_values)
{
  if (g_str_equal (name, "channels")) {
    const char *start_str = NULL, *end_str = NULL;

    for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) {
      if (g_str_equal (attribute_names[i], "start"))
        start_str = attribute_values[i];
      else if (g_str_equal (attribute_names[i], "end"))
        end_str = attribute_values[i];

      if (parse_context->country_found && start_str && end_str) {
        gint64 start, end;
        CbdChannelsRange ch;

        start = g_ascii_strtoll (start_str, NULL, 10);
        if (start > G_MAXINT16) {
          g_warning ("%" G_GINT64_FORMAT " is not a valid channel number", start);
          continue;
        } else
          ch.start = start;

        end = g_ascii_strtoll (end_str, NULL, 10);
        if (end > G_MAXINT16) {
          g_warning ("%" G_GINT64_FORMAT " is not a valid channel number", end);
          continue;
        } else
          ch.end = end;

        ch.level = parse_context->current_level;
        g_array_append_val (parse_context->channels, ch);
        break;
      }
    }
  }
}


static void
parser_start_element (GMarkupParseContext *context,
                      const char          *element_name,
                      const char         **attribute_names,
                      const char         **attribute_values,
                      gpointer             user_data,
                      GError             **error)
{
  CbdParseContext *parse_context = user_data;

  g_clear_pointer (&parse_context->text_buffer, g_free);

  switch (parse_context->state) {
  case PARSER_TOPLEVEL:
    parser_toplevel_start (parse_context, element_name, attribute_names, attribute_values);
    break;
  case PARSER_COUNTRY:
    parser_country_start (parse_context, element_name, attribute_names, attribute_values);
    break;
  case PARSER_CBS:
    parser_cbs_start (parse_context, element_name, attribute_names, attribute_values);
    break;
  case PARSER_LEVEL:
    parser_level_start (parse_context, element_name, attribute_names, attribute_values);
    break;
  case PARSER_ERROR:
    break;
  case PARSER_DONE:
    break;
  }
}


static void
parser_country_end (CbdParseContext *parse_context, const char *name)
{
  if (g_str_equal (name, "country")) {
    g_clear_pointer (&parse_context->text_buffer, g_free);
    parse_context->state = PARSER_TOPLEVEL;
  }
}


static void
parser_cbs_end (CbdParseContext *parse_context, const char *name)
{
  if (g_str_equal (name, "cbs")) {
    g_clear_pointer (&parse_context->text_buffer, g_free);
    parse_context->state = PARSER_COUNTRY;
  }

  if (parse_context->country_found)
    parse_context->state = PARSER_DONE;
}


static void
parser_level_end (CbdParseContext *parse_context, const char *name)
{
  if (g_str_equal (name, "level")) {
    parse_context->current_level = LCB_SEVERITY_LEVEL_UNKNOWN;
    g_clear_pointer (&parse_context->text_buffer, g_free);
    parse_context->state = PARSER_CBS;
  }
}


static void
parser_end_element (GMarkupParseContext *context,
                    const char          *element_name,
                    gpointer             user_data,
                    GError             **error)
{
  CbdParseContext *parse_context = user_data;

  switch (parse_context->state) {
  case PARSER_TOPLEVEL:
    break;
  case PARSER_COUNTRY:
    parser_country_end (parse_context, element_name);
    break;
  case PARSER_CBS:
    parser_cbs_end (parse_context, element_name);
    break;
  case PARSER_LEVEL:
    parser_level_end (parse_context, element_name);
    break;
  case PARSER_ERROR:
    break;
  case PARSER_DONE:
    break;
  }
}


static void
parser_text (GMarkupParseContext *context,
             const char          *text,
             gsize                text_len,
             gpointer             user_data,
             GError             **error)
{
  CbdParseContext *parse_context = user_data;

  g_free (parse_context->text_buffer);
  parse_context->text_buffer = g_strdup (text);
}


static const GMarkupParser parser = {
  .start_element = parser_start_element,
  .end_element   = parser_end_element,
  .text          = parser_text,
  .passthrough   = NULL,
  .error         = NULL,
};


static void read_next_chunk (GInputStream *stream, GTask *task);


static void
on_stream_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  GInputStream *stream = G_INPUT_STREAM (source_object);
  g_autoptr (GTask) task = G_TASK (user_data);
  CbdParseContext *parse_context = g_task_get_task_data (task);
  gssize len;
  GError *error = NULL;

  len = g_input_stream_read_finish (stream, res, &error);
  if (len == -1) {
    g_prefix_error (&error, "Error reading service provider database: ");
    g_task_return_error (task, error);
    return;
  }

  if (len == 0) {
    g_set_error (&error,
                 G_IO_ERROR,
                 G_IO_ERROR_FAILED,
                 "Information for '%s' not found in service provider database",
                 parse_context->country);
    g_task_return_error (task, error);
    return;
  }

  if (!g_markup_parse_context_parse (parse_context->ctx, parse_context->buffer, len, &error)) {
    g_prefix_error (&error, "Error parsing service provider database: ");
    g_task_return_error (task, error);
    return;
  }

  if (parse_context->state == PARSER_DONE) {
    g_task_return_pointer (task,
                           g_steal_pointer (&parse_context->channels),
                           (GDestroyNotify)g_hash_table_unref);
    return;
  }

  read_next_chunk (stream, g_steal_pointer (&task));
}


static void
read_next_chunk (GInputStream *stream, GTask *task)
{
  CbdParseContext *parse_context = g_task_get_task_data (task);

  g_input_stream_read_async (stream,
                             parse_context->buffer,
                             sizeof (parse_context->buffer),
                             G_PRIORITY_DEFAULT,
                             g_task_get_cancellable (task),
                             on_stream_read_ready,
                             task);
}


static void
on_file_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  g_autoptr (GTask) task = G_TASK (user_data);
  g_autoptr (GFileInputStream) stream = NULL;
  GError *error = NULL;
  GFile *file = G_FILE (source_object);

  stream = g_file_read_finish (file, res, &error);
  if (!stream) {
    g_prefix_error (&error, "Error opening service provider database: ");
    g_task_return_error (task, error);
    return;
  }

  read_next_chunk (G_INPUT_STREAM (stream), g_steal_pointer (&task));
}


GArray *
cbd_service_provider_get_channels_finish (GAsyncResult      *res,
                                          GError           **error)
{
  g_assert (G_IS_TASK (res));
  g_assert (g_task_get_source_tag(G_TASK (res)) == cbd_service_providers_get_channels);

  return g_task_propagate_pointer (G_TASK (res), error);
}


void
cbd_service_providers_get_channels (const char          *serviceproviders,
                                    const char          *country,
                                    GCancellable        *cancellable,
                                    GAsyncReadyCallback  callback,
                                    gpointer             user_data)
{
  g_autoptr (GFile) file = NULL;
  g_autoptr (GTask) task = g_task_new (NULL, cancellable, callback, user_data);
  CbdParseContext *parse_context = g_new0 (CbdParseContext, 1);

  g_assert (serviceproviders);
  g_assert (country);

  parse_context->country = g_strdup (country);
  parse_context->ctx = g_markup_parse_context_new (&parser, 0, parse_context, NULL);
  parse_context->channels = g_array_new (FALSE, FALSE, sizeof (CbdChannelsRange));

  g_task_set_task_data (task, parse_context, (GDestroyNotify)cbd_parse_context_free);
  g_task_set_source_tag (task, cbd_service_providers_get_channels);

  file = g_file_new_for_path (serviceproviders);
  g_file_read_async (file, G_PRIORITY_DEFAULT,
                     cancellable,
                     on_file_read_ready,
                     g_steal_pointer (&task));
}


static void
on_get_channels_ready (GObject *object, GAsyncResult *res, gpointer user_data)
{
  GetChannelsSyncData *data = user_data;

  g_assert (data->res == NULL);
  data->res = g_object_ref (res);
  g_main_loop_quit (data->loop);
}


GArray *
cbd_service_provider_get_channels_sync (const char *serviceproviders,
                                        const char *country,
                                        GError    **error)
{
  GArray *channels;
  GetChannelsSyncData data;
  g_autoptr (GMainContext) context = g_main_context_new ();
  g_autoptr (GMainLoop) loop = NULL;

  g_main_context_push_thread_default (context);
  loop = g_main_loop_new (context, FALSE);

  data = (GetChannelsSyncData) {
    .loop = loop,
    .res = NULL,
  };

  cbd_service_providers_get_channels (serviceproviders,
                                      country,
                                      NULL,
                                      on_get_channels_ready,
                                      &data);
  g_main_loop_run (data.loop);

  channels = cbd_service_provider_get_channels_finish (data.res, error);

  g_clear_object (&data.res);
  g_main_context_pop_thread_default (context);

  return channels;
}
