/*
 * FPrint Image
 * Copyright (C) 2007 Daniel Drake <dsd@gentoo.org>
 * Copyright (C) 2019 Benjamin Berg <bberg@redhat.com>
 *
 * This library 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define FP_COMPONENT "image"

#include "fpi-compat.h"
#include "fpi-image.h"
#include "fpi-log.h"

#include <nbis.h>

/**
 * SECTION: fp-image
 * @title: FpImage
 * @short_description: Internal Image handling routines
 *
 * Some devices will provide the image data corresponding to a print
 * this object allows accessing this data.
 */

G_DEFINE_TYPE (FpImage, fp_image, G_TYPE_OBJECT)

enum {
  PROP_0,
  PROP_WIDTH,
  PROP_HEIGHT,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

FpImage *
fp_image_new (gint width, gint height)
{
  return g_object_new (FP_TYPE_IMAGE,
                       "width", width,
                       "height", height,
                       NULL);
}

static void
fp_image_finalize (GObject *object)
{
  FpImage *self = (FpImage *) object;

  g_clear_pointer (&self->data, g_free);
  g_clear_pointer (&self->binarized, g_free);
  g_clear_pointer (&self->minutiae, g_ptr_array_unref);

  G_OBJECT_CLASS (fp_image_parent_class)->finalize (object);
}

static void
fp_image_constructed (GObject *object)
{
  FpImage *self = (FpImage *) object;

  self->data = g_malloc0 (self->width * self->height);
}

static void
fp_image_get_property (GObject    *object,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  FpImage *self = FP_IMAGE (object);

  switch (prop_id)
    {
    case PROP_WIDTH:
      g_value_set_uint (value, self->width);
      break;

    case PROP_HEIGHT:
      g_value_set_uint (value, self->height);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
fp_image_set_property (GObject      *object,
                       guint         prop_id,
                       const GValue *value,
                       GParamSpec   *pspec)
{
  FpImage *self = FP_IMAGE (object);

  switch (prop_id)
    {
    case PROP_WIDTH:
      self->width = g_value_get_uint (value);
      break;

    case PROP_HEIGHT:
      self->height = g_value_get_uint (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
fp_image_class_init (FpImageClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = fp_image_finalize;
  object_class->constructed = fp_image_constructed;
  object_class->set_property = fp_image_set_property;
  object_class->get_property = fp_image_get_property;

  properties[PROP_WIDTH] =
    g_param_spec_uint ("width",
                       "Width",
                       "The width of the image",
                       0,
                       G_MAXUINT16,
                       0,
                       G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);

  properties[PROP_HEIGHT] =
    g_param_spec_uint ("height",
                       "Height",
                       "The height of the image",
                       0,
                       G_MAXUINT16,
                       0,
                       G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
fp_image_init (FpImage *self)
{
}

typedef struct
{
  GAsyncReadyCallback user_cb;
  struct fp_minutiae *minutiae;
  gint                width, height;
  gdouble             ppmm;
  FpiImageFlags       flags;
  guchar             *image;
  guchar             *binarized;
} DetectMinutiaeData;

static void
fp_image_detect_minutiae_free (DetectMinutiaeData *data)
{
  g_clear_pointer (&data->image, g_free);
  g_clear_pointer (&data->minutiae, free_minutiae);
  g_clear_pointer (&data->binarized, g_free);
  g_free (data);
}

static void
fp_image_detect_minutiae_cb (GObject      *source_object,
                             GAsyncResult *res,
                             gpointer      user_data)
{
  GTask *task = G_TASK (res);
  FpImage *image;
  DetectMinutiaeData *data = g_task_get_task_data (task);

  if (!g_task_had_error (task))
    {
      gint i;
      image = FP_IMAGE (source_object);

      image->flags = data->flags;

      g_clear_pointer (&image->data, g_free);
      image->data = g_steal_pointer (&data->image);

      g_clear_pointer (&image->binarized, g_free);
      image->binarized = g_steal_pointer (&data->binarized);

      g_clear_pointer (&image->minutiae, g_ptr_array_unref);
      image->minutiae = g_ptr_array_new_full (data->minutiae->num,
                                              (GDestroyNotify) free_minutia);

      for (i = 0; i < data->minutiae->num; i++)
        g_ptr_array_add (image->minutiae,
                         g_steal_pointer (&data->minutiae->list[i]));

      /* Don't let it delete anything. */
      data->minutiae->num = 0;
    }

  if (data->user_cb)
    data->user_cb (source_object, res, user_data);
}

static void
vflip (guint8 *data, gint width, gint height)
{
  int data_len = width * height;
  unsigned char rowbuf[width];
  int i;

  for (i = 0; i < height / 2; i++)
    {
      int offset = i * width;
      int swap_offset = data_len - (width * (i + 1));

      /* copy top row into buffer */
      memcpy (rowbuf, data + offset, width);

      /* copy lower row over upper row */
      memcpy (data + offset, data + swap_offset, width);

      /* copy buffer over lower row */
      memcpy (data + swap_offset, rowbuf, width);
    }
}

static void
hflip (guint8 *data, gint width, gint height)
{
  unsigned char rowbuf[width];
  int i, j;

  for (i = 0; i < height; i++)
    {
      int offset = i * width;

      memcpy (rowbuf, data + offset, width);
      for (j = 0; j < width; j++)
        data[offset + j] = rowbuf[width - j - 1];
    }
}

static void
invert_colors (guint8 *data, gint width, gint height)
{
  int data_len = width * height;
  int i;

  for (i = 0; i < data_len; i++)
    data[i] = 0xff - data[i];
}

static void
fp_image_detect_minutiae_thread_func (GTask        *task,
                                      gpointer      source_object,
                                      gpointer      task_data,
                                      GCancellable *cancellable)
{
  g_autoptr(GTimer) timer = NULL;
  DetectMinutiaeData *data = task_data;
  struct fp_minutiae *minutiae = NULL;
  g_autofree gint *direction_map = NULL;
  g_autofree gint *low_contrast_map = NULL;
  g_autofree gint *low_flow_map = NULL;
  g_autofree gint *high_curve_map = NULL;
  g_autofree gint *quality_map = NULL;
  g_autofree guchar *bdata = NULL;
  gint map_w, map_h;
  gint bw, bh, bd;
  gint r;
  g_autofree LFSPARMS *lfsparms = NULL;

  /* Normalize the image first */
  if (data->flags & FPI_IMAGE_H_FLIPPED)
    hflip (data->image, data->width, data->height);

  if (data->flags & FPI_IMAGE_V_FLIPPED)
    vflip (data->image, data->width, data->height);

  if (data->flags & FPI_IMAGE_COLORS_INVERTED)
    invert_colors (data->image, data->width, data->height);

  data->flags &= ~(FPI_IMAGE_H_FLIPPED | FPI_IMAGE_V_FLIPPED | FPI_IMAGE_COLORS_INVERTED);

  lfsparms = g_memdup2 (&g_lfsparms_V2, sizeof (LFSPARMS));
  lfsparms->remove_perimeter_pts = data->flags & FPI_IMAGE_PARTIAL ? TRUE : FALSE;

  timer = g_timer_new ();
  r = get_minutiae (&minutiae, &quality_map, &direction_map,
                    &low_contrast_map, &low_flow_map, &high_curve_map,
                    &map_w, &map_h, &bdata, &bw, &bh, &bd,
                    data->image, data->width, data->height, 8,
                    data->ppmm, lfsparms);
  g_timer_stop (timer);
  fp_dbg ("Minutiae scan completed in %f secs", g_timer_elapsed (timer, NULL));

  data->binarized = g_steal_pointer (&bdata);
  data->minutiae = minutiae;

  if (r)
    {
      fp_err ("get minutiae failed, code %d", r);
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Minutiae scan failed with code %d", r);
      g_object_unref (task);
      return;
    }

  if (!data->minutiae || data->minutiae->num == 0)
    {
      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
                               "No minutiae found");
      g_object_unref (task);
      return;
    }

  g_task_return_boolean (task, TRUE);
  g_object_unref (task);
}

/**
 * fp_image_get_height:
 * @self: A #FpImage
 *
 * Gets the pixel height of an image.
 *
 * Returns: the height of the image
 */
guint
fp_image_get_height (FpImage *self)
{
  return self->height;
}

/**
 * fp_image_get_width:
 * @self: A #FpImage
 *
 * Gets the pixel width of an image.
 *
 * Returns: the width of the image
 */
guint
fp_image_get_width (FpImage *self)
{
  return self->width;
}

/**
 * fp_image_get_ppmm:
 * @self: A #FpImage
 *
 * Gets the resolution of the image. Note that this is assumed to
 * be fixed to 500 points per inch (~19.685 p/mm) for most drivers.
 *
 * Returns: the resolution of the image in points per millimeter
 */
gdouble
fp_image_get_ppmm (FpImage *self)
{
  return self->ppmm;
}

/**
 * fp_image_get_data:
 * @self: A #FpImage
 * @len: (out) (optional): Return location for length or %NULL
 *
 * Gets the greyscale data for an image. This data must not be modified or
 * freed.
 *
 * Returns: (transfer none) (array length=len): The image data
 */
const guchar *
fp_image_get_data (FpImage *self, gsize *len)
{
  if (len)
    *len = self->width * self->height;

  return self->data;
}

/**
 * fp_image_get_binarized:
 * @self: A #FpImage
 * @len: (out) (optional): Return location for length or %NULL
 *
 * Gets the binarized data for an image. This data must not be modified or
 * freed. You need to first detect the minutiae using
 * fp_image_detect_minutiae().
 *
 * Returns: (transfer none) (array length=len): The binarized image data
 */
const guchar *
fp_image_get_binarized (FpImage *self, gsize *len)
{
  if (len && self->binarized)
    *len = self->width * self->height;

  return self->binarized;
}

/**
 * fp_image_get_minutiae:
 * @self: A #FpImage
 *
 * Gets the minutiae for an image. This data must not be modified or
 * freed. You need to first detect the minutiae using
 * fp_image_detect_minutiae().
 *
 * Returns: (transfer none) (element-type FpMinutia): The detected minutiae
 */
GPtrArray *
fp_image_get_minutiae (FpImage *self)
{
  return self->minutiae;
}

/**
 * fp_image_detect_minutiae:
 * @self: A #FpImage
 * @cancellable: a #GCancellable, or %NULL
 * @callback: the function to call on completion
 * @user_data: the data to pass to @callback
 *
 * Detects the minutiae found in an image.
 */
void
fp_image_detect_minutiae (FpImage            *self,
                          GCancellable       *cancellable,
                          GAsyncReadyCallback callback,
                          gpointer            user_data)
{
  GTask *task;
  DetectMinutiaeData *data = g_new0 (DetectMinutiaeData, 1);

  task = g_task_new (self, cancellable, fp_image_detect_minutiae_cb, user_data);

  data->image = g_malloc (self->width * self->height);
  memcpy (data->image, self->data, self->width * self->height);
  data->flags = self->flags;
  data->width = self->width;
  data->height = self->height;
  data->ppmm = self->ppmm;
  data->user_cb = callback;

  g_task_set_task_data (task, data, (GDestroyNotify) fp_image_detect_minutiae_free);
  g_task_run_in_thread (task, fp_image_detect_minutiae_thread_func);
}

/**
 * fp_image_detect_minutiae_finish:
 * @self: A #FpImage
 * @result: A #GAsyncResult
 * @error: Return location for errors, or %NULL to ignore
 *
 * Finish minutiae detection in an image
 *
 * Returns: %TRUE on success
 */
gboolean
fp_image_detect_minutiae_finish (FpImage      *self,
                                 GAsyncResult *result,
                                 GError      **error)
{
  return g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * fp_minutia_get_coords:
 * @min: A #FpMinutia
 * @x: (out): x position in image
 * @y: (out): y position in image
 *
 * Returns the coordinates of the found minutia. This is only useful for
 * debugging purposes and the API is not considered stable for production.
 */
void
fp_minutia_get_coords (FpMinutia *min, gint *x, gint *y)
{
  if (x)
    *x = min->x;
  if (y)
    *y = min->y;
}
