/* babl - dynamically extendable universal pixel conversion library.
 * Copyright (C) 2005, Øyvind Kolås.
 *
 * 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 3 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/>.
 */


#include "config.h"
#include <string.h>
#include <stdarg.h>
#include <math.h>
#define NEEDS_BABL_DB
#include "babl-internal.h"
#include "babl-db.h"
#include "babl-ref-pixels.h"

static Babl *
conversion_new (const char    *name,
                int            id,
                Babl          *source,
                Babl          *destination,
                BablFuncLinear linear,
                BablFuncPlane  plane,
                BablFuncPlanar planar,
                void          *user_data)
{
  Babl *babl = NULL;

  babl_assert (source->class_type ==
               destination->class_type);

  babl                = babl_malloc (sizeof (BablConversion) + strlen (name) + 1);
  babl->instance.name = (char *) babl + sizeof (BablConversion);
  strcpy (babl->instance.name, name);

  if (linear)
    {
      babl->class_type                 = BABL_CONVERSION_LINEAR;
      babl->conversion.function.linear = linear;
    }
  else if (plane)
    {
      babl->class_type                = BABL_CONVERSION_PLANE;
      babl->conversion.function.plane = plane;
    }
  else if (planar)
    {
      babl->class_type                 = BABL_CONVERSION_PLANAR;
      babl->conversion.function.planar = planar;
    }
  switch (source->class_type)
    {
      case BABL_TYPE:
        if (linear) /* maybe linear could take a special argument, passed as an
                       additional key/value pair in the constructor. To cast it
                       as a generic N-element conversion, thus making it applicable
                       to being generic for any within model conversion of plain
                       buffers.
                     */
          {
            babl_fatal ("linear conversions not supported for %s",
                        babl_class_name (source->class_type));
          }
        else if (planar)
          {
            babl_fatal ("planar conversions not supported for %s",
                        babl_class_name (source->class_type));
          }
        break;

      case BABL_MODEL:
        if (plane)
          {
            babl_fatal ("plane conversions not supported for %s",
                        babl_class_name (source->class_type));
          }
        break;

      case BABL_FORMAT:
        break;

      default:
        babl_fatal ("%s unexpected", babl_class_name (babl->class_type));
        break;
    }

  babl->instance.id            = id;
  babl->conversion.source      = source;
  babl->conversion.destination = destination;
  babl->conversion.error       = -1.0;
  babl->conversion.cost        = 69L;

  babl->conversion.pixels      = 0;
  babl->conversion.processings = 0;

  babl->conversion.data = user_data;

  if (babl->class_type == BABL_CONVERSION_LINEAR &&
      BABL (babl->conversion.source)->class_type == BABL_MODEL)
    {
      const Babl *src_format = NULL;
      const Babl *dst_format = NULL;
      if (BABL (babl->conversion.source) == babl_model_from_id (BABL_RGBA))
        {
          src_format = babl_format_from_id (BABL_RGBA_DOUBLE);
          dst_format = babl_format_with_model_as_type (
            BABL (babl->conversion.destination),
            babl_type_from_id (BABL_DOUBLE));
        }
      else if (BABL (babl->conversion.destination) == babl_model_from_id (BABL_RGBA))
        {
          src_format = babl_format_with_model_as_type (
            BABL (babl->conversion.source),
            babl_type_from_id (BABL_DOUBLE));
          dst_format = babl_format_from_id (BABL_RGBA_DOUBLE);
        }
      else
        {
          babl_fatal ("neither source nor destination model is RGBA (requirement might be temporary)");
        }
      babl_conversion_new (
        src_format,
        dst_format,
        "linear", linear,
        "data", user_data,
        NULL);
      babl->conversion.error = 0.0;
    }

  return babl;
}

static char buf[512] = "";
static int collisions = 0;
static char *
create_name (Babl *source, Babl *destination, int type)
{
  if (babl_extender ())
    {
      snprintf (buf, 512 - 1, "%s %i: %s%s to %s",
                BABL (babl_extender ())->instance.name,
                collisions,
                type == BABL_CONVERSION_LINEAR ? "" :
                type == BABL_CONVERSION_PLANE ? "plane " :
                type == BABL_CONVERSION_PLANAR ? "planar " : "Eeeek! ",
                source->instance.name,
                destination->instance.name);
      buf[511] = '\0';
    }
  else
    {
      snprintf (buf, 512 - 1, "%s %s to %s",
                type == BABL_CONVERSION_LINEAR ? "" :
                type == BABL_CONVERSION_PLANE ? "plane " :
                type == BABL_CONVERSION_PLANAR ? "planar " : "Eeeek! ",
                source->instance.name,
                destination->instance.name);
      buf[511] = '\0';
    }
  return buf;
}

const Babl *
babl_conversion_new (const void *first_arg,
                     ...)
{
  va_list        varg;
  Babl          *babl;

  int            id       = 0;
  BablFuncLinear linear   = NULL;
  BablFuncPlane  plane    = NULL;
  BablFuncPlanar planar   = NULL;
  int            type     = 0;
  int            got_func = 0;
  const char    *arg      = first_arg;
  void          *user_data= NULL;

  Babl          *source;
  Babl          *destination;

  char          *name;

  va_start (varg, first_arg);
  source      = (Babl *) arg;
  destination = va_arg (varg, Babl *);
  arg         = va_arg (varg, char *);

  assert (BABL_IS_BABL (source));
  assert (BABL_IS_BABL (destination));


  while (arg)
    {
      if (!strcmp (arg, "id"))
        {
          id = va_arg (varg, int);
        }

      else if (!strcmp (arg, "data"))
        {
          user_data = va_arg (varg, void*);
        }

      else if (!strcmp (arg, "linear"))
        {
          if (got_func++)
            {
              babl_fatal ("already got a conversion func\n");
            }
          linear = va_arg (varg, BablFuncLinear);
        }

      else if (!strcmp (arg, "plane"))
        {
          if (got_func++)
            {
              babl_fatal ("already got a conversion func\n");
            }
          plane = va_arg (varg, BablFuncPlane);
        }

      else if (!strcmp (arg, "planar"))
        {
          if (got_func++)
            {
              babl_fatal ("already got a conversion func\n");
            }
          planar = va_arg (varg, BablFuncPlanar);
        }

      else
        {
          babl_fatal ("unhandled argument '%s'", arg);
        }

      arg = va_arg (varg, char *);
    }

  va_end (varg);

  assert (source);
  assert (destination);

  if (linear)
    {
      type = BABL_CONVERSION_LINEAR;
    }
  else if (plane)
    {
      type = BABL_CONVERSION_PLANE;
    }
  else if (planar)
    {
      type = BABL_CONVERSION_PLANAR;
    }

  collisions = 0;
  name = create_name (source, destination, type);

  babl = babl_db_exist (db, id, name);
  while (babl)
    {
      /* we allow multiple conversions to be registered per extender, each
         of them ending up with their own unique name
       */
      collisions++;
      name = create_name (source, destination, type);
      babl = babl_db_exist (db, id, name);
    }

  babl = conversion_new (name, id, source, destination, linear, plane, planar, 
                         user_data);

  /* Since there is not an already registered instance by the required
   * id/name, inserting newly created class into database.
   */
  babl_db_insert (db, babl);
  if (!source->type.from_list)
    source->type.from_list = babl_list_init_with_size (BABL_CONVERSIONS);
  babl_list_insert_last (source->type.from_list, babl);
  return babl;
}

static long
babl_conversion_linear_process (BablConversion *conversion,
                                const void     *source,
                                void           *destination,
                                long            n)
{
  return conversion->function.linear (source, destination, n, conversion->data);
}

static long
babl_conversion_plane_process (BablConversion *conversion,
                               const void     *source,
                               void           *destination,
                               int             src_pitch,
                               int             dst_pitch,
                               long            n)
{
  return conversion->function.plane (source, destination,
                                     src_pitch, dst_pitch,
                                     n,
                                     conversion->data);
}

static long
babl_conversion_planar_process (BablConversion *conversion,
                                BablImage      *source,
                                BablImage      *destination,
                                long            n)
{
#ifdef USE_ALLOCA
  const char **src_data = alloca (sizeof (void *) * source->components);
  char **dst_data = alloca (sizeof (void *) * destination->components);
#else
  const char  *src_data[BABL_MAX_COMPONENTS];
  char  *dst_data[BABL_MAX_COMPONENTS];
#endif

  memcpy (src_data, source->data, sizeof (void *) * source->components);
  memcpy (dst_data, destination->data, sizeof (void *) * destination->components);

  return conversion->function.planar (source->components,
                                      src_data,
                                      source->pitch,
                                      destination->components,
                                      dst_data,
                                      destination->pitch,
                                      n,
                                      conversion->data);
}

long
babl_conversion_process (const Babl *babl,
                         const char *source,
                         char       *destination,
                         long        n)
{
  BablConversion *conversion = (BablConversion *) babl;

  babl_assert (BABL_IS_BABL (conversion));

  switch (BABL (conversion)->class_type)
    {
      case BABL_CONVERSION_PLANE:
      {
        const void *src_data  = NULL;
        void *dst_data  = NULL;
        int   src_pitch = 0;
        int   dst_pitch = 0;

        if (BABL_IS_BABL (source))
          {
            BablImage *img;

            img       = (BablImage *) source;
            src_data  = img->data[0];
            src_pitch = img->pitch[0];
          }
        if (BABL_IS_BABL (destination))
          {
            BablImage *img = (BablImage *) destination;

            dst_data  = img->data[0];
            dst_pitch = img->pitch[0];
          }

        if (!src_data)
          src_data = source;
        if (!src_pitch)
          src_pitch = BABL (conversion->source)->type.bits / 8;
        if (!dst_data)
          dst_data = destination;
        if (!dst_pitch)
          dst_pitch = BABL (conversion->destination)->type.bits / 8;

        babl_conversion_plane_process (conversion,
                                       src_data, dst_data,
                                       src_pitch, dst_pitch,
                                       n);
      }
        break;

      case BABL_CONVERSION_PLANAR:
        babl_assert (BABL_IS_BABL (source));
        babl_assert (BABL_IS_BABL (destination));

        babl_conversion_planar_process (conversion,
                                        (BablImage *) source,
                                        (BablImage *) destination,
                                        n);
        break;

      case BABL_CONVERSION_LINEAR:
        /* the assertions relied on a babl_malloc structure
         *
         * babl_assert (!BABL_IS_BABL (source));
           babl_assert (!BABL_IS_BABL (destination));*/

        babl_conversion_linear_process (conversion,
                                        source,
                                        destination,
                                        n);
        break;

      default:
        babl_log ("args=(%s, %p, %p, %li) unhandled conversion type: %s",
                  conversion->instance.name, source, destination, n,
                  babl_class_name (conversion->instance.class_type));
        return 0;
        break;
    }

  conversion->processings++;
  conversion->pixels += n;
  return n;
}

long
babl_conversion_cost (BablConversion *conversion)
{
  if (!conversion)
    return 100000000L;
  if (conversion->error == -1.0)
    babl_conversion_error (conversion);
  return conversion->cost;
}

double
babl_conversion_error (BablConversion *conversion)
{
  Babl *fmt_source;
  Babl *fmt_destination;

  const Babl *fmt_rgba_double = babl_format_new (babl_model ("RGBA"),
                                                 babl_type ("double"),
                                                 babl_component ("R"),
                                                 babl_component ("G"),
                                                 babl_component ("B"),
                                                 babl_component ("A"),
                                                 NULL);

  double  error       = 0.0;
  long    ticks_start = 0;
  long    ticks_end   = 0;

  const int test_pixels = babl_get_num_conversion_test_pixels ();
  const double *test = babl_get_conversion_test_pixels ();

  void   *source;
  void   *destination;
  double *destination_rgba_double;
  void   *ref_destination;
  double *ref_destination_rgba_double;

  Babl   *fish_rgba_to_source;
  Babl   *fish_reference;
  Babl   *fish_destination_to_rgba;

  if (!conversion)
    return 0.0;

  if (conversion->error != -1.0)  /* double conversion against a set value should work */
    {
      return conversion->error;
    }

  fmt_source      = BABL (conversion->source);
  fmt_destination = BABL (conversion->destination);

  fish_rgba_to_source      = babl_fish_reference (fmt_rgba_double, fmt_source);
  fish_reference           = babl_fish_reference (fmt_source, fmt_destination);
  fish_destination_to_rgba = babl_fish_reference (fmt_destination, fmt_rgba_double);

  if (fmt_source == fmt_destination)
    {
      conversion->error = 0.0;
      return 0.0;
    }

  if (!(fmt_source->instance.id != BABL_RGBA &&
        fmt_destination->instance.id != BABL_RGBA &&
        fmt_source->instance.id != BABL_DOUBLE &&
        fmt_destination->instance.id != BABL_DOUBLE &&
        fmt_source->class_type == BABL_FORMAT &&
        fmt_destination->class_type == BABL_FORMAT))
    {
      conversion->error = 0.000042;
    }

  source                      = babl_calloc (test_pixels, fmt_source->format.bytes_per_pixel);
  destination                 = babl_calloc (test_pixels, fmt_destination->format.bytes_per_pixel);
  ref_destination             = babl_calloc (test_pixels, fmt_destination->format.bytes_per_pixel);
  destination_rgba_double     = babl_calloc (test_pixels, fmt_rgba_double->format.bytes_per_pixel);
  ref_destination_rgba_double = babl_calloc (test_pixels, fmt_rgba_double->format.bytes_per_pixel);

  babl_process (fish_rgba_to_source,
                test, source, test_pixels);

  ticks_start = babl_ticks ();
  babl_process (babl_fish_simple (conversion),
                source, destination, test_pixels);
  ticks_end = babl_ticks ();

  babl_process (fish_reference,
                source, ref_destination, test_pixels);

  babl_process (fish_destination_to_rgba,
                ref_destination, ref_destination_rgba_double, test_pixels);
  babl_process (fish_destination_to_rgba,
                destination, destination_rgba_double, test_pixels);

  error = babl_rel_avg_error (destination_rgba_double,
                              ref_destination_rgba_double,
                              test_pixels * 4);

  fish_rgba_to_source->fish.processings--;
  fish_reference->fish.processings--;
  fish_destination_to_rgba->fish.processings -= 2;

  fish_rgba_to_source->fish.pixels      -= test_pixels;
  fish_reference->fish.pixels           -= test_pixels;
  fish_destination_to_rgba->fish.pixels -= 2 * test_pixels;

  babl_free (source);
  babl_free (destination);
  babl_free (destination_rgba_double);
  babl_free (ref_destination);
  babl_free (ref_destination_rgba_double);

  conversion->error = error;
  conversion->cost  = babl_process_cost (ticks_start, ticks_end);

  return error;
}

BABL_CLASS_IMPLEMENT (conversion)
