/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2014 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include <glib/gi18n.h>

#include "flatpak-table-printer.h"
#include "flatpak-utils-private.h"

#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>


typedef struct
{
  char    *text;
  int      align;
  gboolean span;
} Cell;

static void
free_cell (gpointer data)
{
  Cell *cell = data;

  g_free (cell->text);
  g_free (cell);
}

typedef struct
{
  GPtrArray *cells;
  char      *key;
} Row;

static void
free_row (gpointer data)
{
  Row *row = data;

  g_ptr_array_free (row->cells, TRUE);
  g_free (row->key);
  g_free (row);
}

typedef struct
{
  char                *title;
  gboolean             expand;
  FlatpakEllipsizeMode ellipsize;
  gboolean             skip_unique;
  char                *skip_unique_str;
  gboolean             skip;
} TableColumn;

static void
free_column (gpointer data)
{
  TableColumn *column = data;

  g_free (column->title);
  g_free (column->skip_unique_str);
  g_free (column);
}

struct FlatpakTablePrinter
{
  GPtrArray *columns;
  GPtrArray *rows;
  GHashTable *rows_ht;
  char      *key;
  GPtrArray *current;
  int        n_columns;
};

FlatpakTablePrinter *
flatpak_table_printer_new (void)
{
  FlatpakTablePrinter *printer = g_new0 (FlatpakTablePrinter, 1);

  printer->columns = g_ptr_array_new_with_free_func (free_column);
  printer->rows = g_ptr_array_new_with_free_func ((GDestroyNotify) free_row);
  printer->rows_ht = g_hash_table_new (g_str_hash, g_str_equal);
  printer->current = g_ptr_array_new_with_free_func (free_cell);

  return printer;
}

void
flatpak_table_printer_free (FlatpakTablePrinter *printer)
{
  g_ptr_array_free (printer->columns, TRUE);
  g_ptr_array_free (printer->rows, TRUE);
  g_hash_table_destroy (printer->rows_ht);
  g_ptr_array_free (printer->current, TRUE);
  g_free (printer->key);
  g_free (printer);
}

static TableColumn *
peek_table_column (FlatpakTablePrinter *printer,
                   int                  column)
{
  if (column < printer->columns->len)
    return g_ptr_array_index (printer->columns, column);

  return NULL;
}

static TableColumn *
get_table_column (FlatpakTablePrinter *printer,
                  int                  column)
{
  TableColumn *col = NULL;

  if (column < printer->columns->len)
    col = g_ptr_array_index (printer->columns, column);

  if (col == NULL)
    {
      col = g_new0 (TableColumn, 1);
      g_ptr_array_insert (printer->columns, column, col);
    }

  return col;
}

void
flatpak_table_printer_set_column_title (FlatpakTablePrinter *printer,
                                        int                  column,
                                        const char          *text)
{
  TableColumn *col = get_table_column (printer, column);

  col->title = g_strdup (text);
}

void
flatpak_table_printer_set_columns (FlatpakTablePrinter *printer,
                                   Column              *columns,
                                   gboolean             defaults)
{
  int i;

  for (i = 0; columns[i].name; i++)
    {
      flatpak_table_printer_set_column_title (printer, i, _(columns[i].title));
      flatpak_table_printer_set_column_expand (printer, i, columns[i].expand);
      flatpak_table_printer_set_column_ellipsize (printer, i, columns[i].ellipsize);
      if (defaults && columns[i].skip_unique_if_default)
        flatpak_table_printer_set_column_skip_unique (printer, i, TRUE);
    }
}

void
flatpak_table_printer_add_aligned_column (FlatpakTablePrinter *printer,
                                          const char          *text,
                                          int                  align)
{
  Cell *cell = g_new0 (Cell, 1);

  cell->text = text ? g_strdup (text) : g_strdup ("");
  cell->align = align;
  g_ptr_array_add (printer->current, cell);
}

void
flatpak_table_printer_add_span (FlatpakTablePrinter *printer,
                                const char          *text)
{
  Cell *cell = g_new0 (Cell, 1);

  cell->text = text ? g_strdup (text) : g_strdup ("");
  cell->align = -1;
  cell->span = TRUE;
  g_ptr_array_add (printer->current, cell);
}

static const char *
find_decimal_point (const char *text)
{
  struct lconv *locale_data;

  locale_data = localeconv ();
  return strstr (text, locale_data->decimal_point);
}

void
flatpak_table_printer_add_decimal_column (FlatpakTablePrinter *printer,
                                          const char          *text)
{
  const char *decimal;
  int align = -1;

  decimal = find_decimal_point (text);
  if (decimal)
    align = decimal - text;

  flatpak_table_printer_add_aligned_column (printer, text, align);
}

void
flatpak_table_printer_add_column (FlatpakTablePrinter *printer,
                                  const char          *text)
{
  flatpak_table_printer_add_aligned_column (printer, text, -1);
}

void
flatpak_table_printer_take_column (FlatpakTablePrinter *printer,
                                   char                *text)
{
  flatpak_table_printer_add_aligned_column (printer, text, -1);
  g_free (text);
}

void
flatpak_table_printer_add_column_len (FlatpakTablePrinter *printer,
                                      const char          *text,
                                      gsize                len)
{
  Cell *cell = g_new0 (Cell, 1);

  cell->text = text ? g_strndup (text, len) : g_strdup ("");
  cell->align = -1;
  g_ptr_array_add (printer->current, cell);
}

void
flatpak_table_printer_append_with_comma (FlatpakTablePrinter *printer,
                                         const char          *text)
{
  Cell *cell;
  char *new;

  g_assert (printer->current->len > 0);

  cell = g_ptr_array_index (printer->current, printer->current->len - 1);

  if (cell->text[0] != 0)
    new = g_strconcat (cell->text, ",", text, NULL);
  else
    new = g_strdup (text);

  g_free (cell->text);
  cell->text = new;
}

void
flatpak_table_printer_append_with_comma_printf (FlatpakTablePrinter *printer,
                                                const char          *format,
                                                ...)
{
  va_list var_args;
  g_autofree char *s = NULL;

  va_start (var_args, format);
  s = g_strdup_vprintf (format, var_args);
  va_end (var_args);

  flatpak_table_printer_append_with_comma (printer, s);
}

void
flatpak_table_printer_set_key (FlatpakTablePrinter *printer, const char *key)
{
  printer->key = g_strdup (key);
}

static gint
cmp_row (gconstpointer _row_a,
         gconstpointer _row_b,
         gpointer      user_data)
{
  const Row *row_a = *(const Row **) _row_a;
  const Row *row_b = *(const Row **) _row_b;
  GCompareFunc cmp = user_data;

  if (row_a == row_b || (row_a->key == NULL && row_b->key == NULL))
    return 0;
  if (row_a->key == NULL)
    return -1;
  if (row_b->key == NULL)
    return 1;

  return cmp (row_a->key, row_b->key);
}

void
flatpak_table_printer_sort (FlatpakTablePrinter *printer, GCompareFunc cmp)
{
  g_ptr_array_sort_with_data (printer->rows, cmp_row, cmp);
}

int
flatpak_table_printer_lookup_row (FlatpakTablePrinter *printer, const char *key)
{
  gpointer value;

  if (g_hash_table_lookup_extended (printer->rows_ht, key, NULL, &value))
    return GPOINTER_TO_INT(value);

  return -1;
}

void
flatpak_table_printer_finish_row (FlatpakTablePrinter *printer)
{
  Row *row;
  int row_nr = flatpak_table_printer_get_current_row (printer);

  if (printer->current->len == 0)
    return; /* Ignore empty rows */

  printer->n_columns = MAX (printer->n_columns, printer->current->len);
  row = g_new0 (Row, 1);
  row->cells = g_steal_pointer (&printer->current);
  row->key = g_steal_pointer (&printer->key);
  g_ptr_array_add (printer->rows, row);
  if (row->key)
    g_hash_table_insert (printer->rows_ht, row->key, GINT_TO_POINTER (row_nr));
  printer->current = g_ptr_array_new_with_free_func (free_cell);
}

/* Return how many terminal rows we produced (with wrapping to columns)
 * while skipping 'skip' many of them. 'skip' is updated to reflect
 * how many we skipped.
 */
static int
print_row (GString *row_s, gboolean bold, int *skip, int columns)
{
  int rows;
  const char *p, *end;
  int n_chars;

  g_strchomp (row_s->str);
  n_chars = cell_width (row_s->str);
  if (n_chars > 0)
    rows = (n_chars + columns - 1) / columns;
  else
    rows = 1;

  p = row_s->str;
  end = row_s->str + strlen (row_s->str);
  while (*skip > 0 && p <= end)
    {
      (*skip)--;
      p = cell_advance (p, columns);
    }

  if (p < end || p == row_s->str)
    {
      if (bold)
        g_print (FLATPAK_ANSI_BOLD_ON "%s" FLATPAK_ANSI_BOLD_OFF, p);
      else
        g_print ("%s", p);
    }
  g_string_truncate (row_s, 0);

  return rows;
}

static void
string_add_spaces (GString *str, int count)
{
  while (count-- > 0)
    g_string_append_c (str, ' ');
}

static gboolean
column_is_unique (FlatpakTablePrinter *printer, int col)
{
  TableColumn *column = get_table_column (printer, col);
  char *first_row = column->skip_unique_str;
  int i;

  for (i = 0; i < printer->rows->len; i++)
    {
      Row *row = g_ptr_array_index (printer->rows, i);
      if (col >= row->cells->len)
        continue;

      Cell *cell = g_ptr_array_index (row->cells, col);

      if (i == 0 && first_row == NULL)
        first_row = cell->text;
      else
        {
          if (g_strcmp0 (first_row, cell->text) != 0)
            return FALSE;
        }
    }

  return TRUE;
}

/*
 * This variant of flatpak_table_printer_print() takes a window width
 * and returns the number of rows that are generated by printing the
 * table to that width. It also takes a number of (terminal) rows
 * to skip at the beginning of the table.
 *
 * Care is taken to do the right thing if the skipping ends
 * in the middle of a wrapped table row.
 *
 * Note that unlike flatpak_table_printer_print(), this function does
 * not add a newline after the last table row.
 */
void
flatpak_table_printer_print_full (FlatpakTablePrinter *printer,
                                  int                  skip,
                                  int                  columns,
                                  int                 *table_height,
                                  int                 *table_width)
{
  g_autofree int *widths = NULL;
  g_autofree int *lwidths = NULL;
  g_autofree int *rwidths = NULL;
  g_autofree int *shrinks = NULL;
  g_autoptr(GString) row_s = g_string_new ("");
  int i, j;
  int rows = 0;
  int total_skip = skip;
  int width;
  int expand_columns;
  int shrink_columns;
  gboolean has_title;
  int expand_by, expand_extra;

  if (printer->current->len != 0)
    flatpak_table_printer_finish_row (printer);

  widths = g_new0 (int, printer->n_columns);
  lwidths = g_new0 (int, printer->n_columns);
  rwidths = g_new0 (int, printer->n_columns);
  shrinks = g_new0 (int, printer->n_columns);

  for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
    {
      TableColumn *col = g_ptr_array_index (printer->columns, i);

      if (col->skip_unique && column_is_unique (printer, i))
        col->skip = TRUE;
    }

  has_title = FALSE;
  for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
    {
      TableColumn *col = g_ptr_array_index (printer->columns, i);

      if (col->skip)
        continue;

      if (col->title)
        {
          widths[i] = MAX (widths[i], cell_width (col->title));
          has_title = TRUE;
        }
    }

  for (i = 0; i < printer->rows->len; i++)
    {
      Row *row = g_ptr_array_index (printer->rows, i);

      for (j = 0; j < row->cells->len; j++)
        {
          Cell *cell = g_ptr_array_index (row->cells, j);
          TableColumn *col = peek_table_column (printer, j);

          if (col && col->skip)
            continue;

          if (cell->span)
            width = 0;
          else
            width = cell_width (cell->text);
          widths[j] = MAX (widths[j], width);
          if (cell->align >= 0)
            {
              lwidths[j] = MAX (lwidths[j], cell->align);
              rwidths[j] = MAX (rwidths[j], width - cell->align);
            }
        }
    }

  width = printer->n_columns - 1;
  for (i = 0; i < printer->n_columns; i++)
    width += widths[i];

  expand_columns = 0;
  shrink_columns = 0;
  for (i = 0; i < printer->columns->len; i++)
    {
      TableColumn *col = g_ptr_array_index (printer->columns, i);
      if (col && col->skip)
        continue;
      if (col && col->expand)
        expand_columns++;
      if (col && col->ellipsize)
        shrink_columns++;
    }

  expand_by = 0;
  expand_extra = 0;
  if (expand_columns > 0)
    {
      int excess = CLAMP (columns - width, 0, width / 2);
      expand_by = excess / expand_columns;
      expand_extra = excess % expand_columns;
      width += excess;
    }

  if (shrink_columns > 0)
    {
      int shortfall = MAX (width - columns, 0);
      int last;
      if (shortfall > 0)
        {
          int shrinkable = 0;
          int leftover = shortfall;

          /* We're distributing the shortfall so that wider columns
           * shrink proportionally more than narrower ones, while
           * avoiding to ellipsize the titles.
           */
          for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
            {
              TableColumn *col = g_ptr_array_index (printer->columns, i);
              gboolean ellipsize = col ? col->ellipsize : FALSE;

              if (col && col->skip)
                continue;

              if (!ellipsize)
                continue;

              if (col && col->title)
                shrinkable += MAX (0, widths[i] - cell_width (col->title));
              else
                shrinkable += MAX (0, widths[i] - 5);
            }

          for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
            {
              TableColumn *col = g_ptr_array_index (printer->columns, i);
              gboolean ellipsize = col ? col->ellipsize : FALSE;

              if (col && col->skip)
                continue;

              if (ellipsize)
                {
                  int sh;
                  if (col && col->title)
                    sh = MAX (0, widths[i] - cell_width (col->title));
                  else
                    sh = MAX (0, widths[i] - 5);
                  shrinks[i] = MIN (shortfall * (sh / (double) shrinkable), widths[i]);
                  leftover -= shrinks[i];
                }
            }

          last = leftover + 1;
          while (leftover > 0 && leftover < last)
            {
              last = leftover;
              for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
                {
                  TableColumn *col = g_ptr_array_index (printer->columns, i);
                  gboolean ellipsize = col ? col->ellipsize : FALSE;

                  if (col && col->skip)
                    continue;

                  if (ellipsize && shrinks[i] < widths[i])
                    {
                      shrinks[i]++;
                      leftover--;
                    }
                  if (leftover == 0)
                    break;
                }
            }
        }

      for (i = 0; i < printer->n_columns; i++)
        width -= shrinks[i];
    }

  if (flatpak_fancy_output () && has_title)
    {
      int grow = expand_extra;
      for (i = 0; i < printer->columns->len && i < printer->n_columns; i++)
        {
          TableColumn *col = g_ptr_array_index (printer->columns, i);
          char *title = col && col->title ? col->title : "";
          gboolean expand = col ? col->expand : FALSE;
          gboolean ellipsize = col ? col->ellipsize : FALSE;
          int len = widths[i];
          g_autofree char *freeme = NULL;

          if (col && col->skip)
            continue;

          if (expand_by > 0 && expand)
            {
              len += expand_by;
              if (grow > 0)
                {
                  len++;
                  grow--;
                }
            }

          if (shrinks[i] > 0 && ellipsize)
            {
              len -= shrinks[i];
              freeme = title = ellipsize_string (title, len);
            }

          if (i > 0)
            g_string_append_c (row_s, ' ');
          g_string_append (row_s, title);
          string_add_spaces (row_s, len - cell_width (title));
        }
      rows += print_row (row_s, TRUE, &skip, columns);
    }

  for (i = 0; i < printer->rows->len; i++)
    {
      Row *row = g_ptr_array_index (printer->rows, i);
      int grow = expand_extra;

      if (rows > total_skip)
        g_print ("\n");

      for (j = 0; j < row->cells->len; j++)
        {
          TableColumn *col = peek_table_column (printer, j);
          gboolean expand = col ? col->expand : FALSE;
          gboolean ellipsize = col ? col->ellipsize : FALSE;
          Cell *cell = g_ptr_array_index (row->cells, j);
          char *text = cell->text;
          int len = widths[j];
          g_autofree char *freeme = NULL;

          if (col && col->skip)
            continue;

          if (expand_by > 0 && expand)
            {
              len += expand_by;
              if (grow > 0)
                {
                  len++;
                  grow--;
                }
            }

          if (shrinks[j] > 0 && ellipsize)
            {
              len -= shrinks[j];
              freeme = text = ellipsize_string_full (text, len, col->ellipsize);
            }

          if (flatpak_fancy_output ())
            {
              if (j > 0)
                g_string_append_c (row_s, ' ');
              if (cell->span)
                g_string_append (row_s, cell->text);
              else if (cell->align < 0)
                {
                  g_string_append (row_s, text);
                  string_add_spaces (row_s, len - cell_width (text));
                }
              else
                {
                  string_add_spaces (row_s, lwidths[j] - cell->align);
                  g_string_append (row_s, text);
                  string_add_spaces (row_s, widths[j] - (lwidths[j] - cell->align)  - cell_width (text));
                }
            }
          else
            g_string_append_printf (row_s, "%s%s", cell->text, (j < row->cells->len - 1) ? "\t" : "");
        }
      rows += print_row (row_s, FALSE, &skip, columns);
    }

  if (table_width)
    *table_width = width;
  if (table_height)
    *table_height = rows;
}

void
flatpak_table_printer_print (FlatpakTablePrinter *printer)
{
  flatpak_table_printer_print_full (printer, 0, 80, NULL, NULL);
  g_print ("\n");
}

int
flatpak_table_printer_get_current_row (FlatpakTablePrinter *printer)
{
  return printer->rows->len;
}

static void
set_cell (FlatpakTablePrinter *printer,
          int                  r,
          int                  c,
          const char          *text,
          int                  align,
          int                  append)
{
  Row *row;
  Cell *cell;
  char *old;

  row = (Row *) g_ptr_array_index (printer->rows, r);

  g_assert (row);

  cell = (Cell *) g_ptr_array_index (row->cells, c);
  g_assert (cell);

  old = cell->text;
  if (old != NULL && append)
    {
      if (append == 2 && *old != 0)
        cell->text = g_strconcat (old, ", ", text, NULL);
      else
        cell->text = g_strconcat (old, text, NULL);
    }
  else
    cell->text = g_strdup (text);
  cell->align = align;

  g_free (old);
}

void
flatpak_table_printer_set_cell (FlatpakTablePrinter *printer,
                                int                  r,
                                int                  c,
                                const char          *text)
{
  set_cell (printer, r, c, text, -1, 0);
}

void
flatpak_table_printer_append_cell (FlatpakTablePrinter *printer,
                                   int                  r,
                                   int                  c,
                                   const char          *text)
{
  set_cell (printer, r, c, text, -1, 1);
}

void
flatpak_table_printer_append_cell_with_comma (FlatpakTablePrinter *printer,
                                              int                  r,
                                              int                  c,
                                              const char          *text)
{
  set_cell (printer, r, c, text, -1, 2);
}

void
flatpak_table_printer_append_cell_with_comma_unique (FlatpakTablePrinter *printer,
                                                     int                  r,
                                                     int                  c,
                                                     const char          *text)
{
  Row *row;
  Cell *cell;

  row = (Row *) g_ptr_array_index (printer->rows, r);
  g_assert (row);
  cell = (Cell *) g_ptr_array_index (row->cells, c);
  g_assert (cell);

  /* Look for existing text in comma separated text */
  if (cell->text != NULL && *text != 0)
    {
      gsize len = strlen (text);
      const char *match = cell->text;
      while ((match = strstr (match, text)) != NULL)
        {
          if (match[len] == 0 || match[len] == ',' )
            return; /* Already in string, do nothing */
          /* Look for next match */
          match = match + len;
        }
    }

  set_cell (printer, r, c, text, -1, 2);
}

void
flatpak_table_printer_set_decimal_cell (FlatpakTablePrinter *printer,
                                        int                  r,
                                        int                  c,
                                        const char          *text)
{
  int align = -1;
  const char *decimal = find_decimal_point (text);

  if (decimal)
    align = decimal - text;

  set_cell (printer, r, c, text, align, 0);
}

void
flatpak_table_printer_set_column_expand (FlatpakTablePrinter *printer,
                                         int                  column,
                                         gboolean             expand)
{
  TableColumn *col = get_table_column (printer, column);

  col->expand = expand;
}

void
flatpak_table_printer_set_column_ellipsize (FlatpakTablePrinter *printer,
                                            int                  column,
                                            FlatpakEllipsizeMode mode)
{
  TableColumn *col = get_table_column (printer, column);

  col->ellipsize = mode;
}

/* Specifies that the column should be skipped if all values are the same */
void
flatpak_table_printer_set_column_skip_unique (FlatpakTablePrinter *printer,
                                              int                  column,
                                              gboolean             skip_unique)
{
  TableColumn *col = get_table_column (printer, column);

  col->skip_unique = skip_unique;
}

/* This modifies set_column_skip_unique to also require that the
 * unique value of the column must be this particular string. Useful if you
 * want to e.g. skip the arch list if everything is for the primary arch, but
 * not if everything is for a non-standard arch.
 */
void
flatpak_table_printer_set_column_skip_unique_string (FlatpakTablePrinter *printer,
                                                     int                  column,
                                                     const char          *str)
{
  TableColumn *col = get_table_column (printer, column);

  g_assert (col->skip_unique_str == NULL);

  col->skip_unique_str = g_strdup (str);
}
