/* tiff loading for GIMP
 *  -Peter Mattis
 *
 * The TIFF loading code has been completely revamped by Nick Lamb
 * njl195@zepler.org.uk -- 18 May 1998
 * And it now gains support for tiles (and doubtless a zillion bugs)
 * njl195@zepler.org.uk -- 12 June 1999
 * LZW patent fuss continues :(
 * njl195@zepler.org.uk -- 20 April 2000
 * The code for this filter is based on "tifftopnm" and "pnmtotiff",
 *  2 programs that are a part of the netpbm package.
 * khk@khk.net -- 13 May 2000
 * Added support for ICCPROFILE tiff tag. If this tag is present in a
 * TIFF file, then a parasite is created and vice versa.
 * peter@kirchgessner.net -- 29 Oct 2002
 * Progress bar only when run interactive
 * Added support for layer offsets - pablo.dangelo@web.de -- 7 Jan 2004
 * Honor EXTRASAMPLES tag while loading images with alphachannel
 * pablo.dangelo@web.de -- 16 Jan 2004
 */

#include "config.h"

#include <errno.h>
#include <string.h>

#include <tiffio.h>

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include "file-tiff-io.h"

static gboolean tiff_file_size_error = FALSE;

typedef struct
{
  GFile         *file;
  GObject       *stream;
  GInputStream  *input;
  GOutputStream *output;
  gboolean       can_seek;

  gchar         *buffer;
  gsize          allocated;
  gsize          used;
  gsize          position;
} TiffIO;


static TIFFExtendProc parent_extender;

static void      tiff_io_warning       (const gchar *module,
                                        const gchar *fmt,
                                        va_list      ap) G_GNUC_PRINTF (2, 0);
static void      tiff_io_error         (const gchar *module,
                                        const gchar *fmt,
                                        va_list      ap) G_GNUC_PRINTF (2, 0);
static tsize_t   tiff_io_read          (thandle_t    handle,
                                        tdata_t      buffer,
                                        tsize_t      size);
static tsize_t   tiff_io_write         (thandle_t    handle,
                                        tdata_t      buffer,
                                        tsize_t      size);
static toff_t    tiff_io_seek          (thandle_t    handle,
                                        toff_t       offset,
                                        gint         whence);
static gint      tiff_io_close         (thandle_t    handle);
static toff_t    tiff_io_get_file_size (thandle_t    handle);
static void      register_geotags      (TIFF        *tif);

static void
register_geotags (TIFF *tif)
{
  static gboolean geotifftags_registered = FALSE;

  if (geotifftags_registered)
    return;

  geotifftags_registered = TRUE;

  TIFFMergeFieldInfo (tif, geotifftags_fieldinfo, (sizeof (geotifftags_fieldinfo) / sizeof (geotifftags_fieldinfo[0])));

  if (parent_extender)
    (*parent_extender) (tif);
}

static TiffIO tiff_io = { 0, };


TIFF *
tiff_open (GFile        *file,
           const gchar  *mode,
           GError      **error)
{
  TIFFSetWarningHandler ((TIFFErrorHandler) tiff_io_warning);
  TIFFSetErrorHandler ((TIFFErrorHandler) tiff_io_error);

  parent_extender = TIFFSetTagExtender (register_geotags);

  tiff_io.file = file;

  if (! strcmp (mode, "r"))
    {
      tiff_io.input = G_INPUT_STREAM (g_file_read (file, NULL, error));
      if (! tiff_io.input)
        return NULL;

      tiff_io.stream = G_OBJECT (tiff_io.input);
    }
#ifdef TIFF_VERSION_BIG
  else if(! strcmp (mode, "w") || ! strcmp (mode, "w8"))
#else
  else if(! strcmp (mode, "w"))
#endif
    {
      tiff_io.output = G_OUTPUT_STREAM (g_file_replace (file,
                                                        NULL, FALSE,
                                                        G_FILE_CREATE_NONE,
                                                        NULL, error));
      if (! tiff_io.output)
        return NULL;

      tiff_io.stream = G_OBJECT (tiff_io.output);
    }
  else if(! strcmp (mode, "a"))
    {
      GIOStream *iostream = G_IO_STREAM (g_file_open_readwrite (file, NULL,
                                                                error));
      if (! iostream)
        return NULL;

      tiff_io.input  = g_io_stream_get_input_stream (iostream);
      tiff_io.output = g_io_stream_get_output_stream (iostream);
      tiff_io.stream = G_OBJECT (iostream);
    }
  else
    {
      g_assert_not_reached ();
    }

#if 0
#warning FIXME !can_seek code is broken
  tiff_io.can_seek = g_seekable_can_seek (G_SEEKABLE (tiff_io.stream));
#endif
  tiff_io.can_seek = TRUE;

  return TIFFClientOpen ("file-tiff", mode,
                         (thandle_t) &tiff_io,
                         tiff_io_read,
                         tiff_io_write,
                         tiff_io_seek,
                         tiff_io_close,
                         tiff_io_get_file_size,
                         NULL, NULL);
}

gboolean
tiff_got_file_size_error (void)
{
  return tiff_file_size_error;
}

void
tiff_reset_file_size_error (void)
{
  tiff_file_size_error = FALSE;
}

static void
tiff_io_warning (const gchar *module,
                 const gchar *fmt,
                 va_list      ap)
{
  gint tag = 0;

  /* Between libtiff 3.7.0beta2 and 4.0.0alpha. */
  if (! strcmp (fmt, "%s: unknown field with tag %d (0x%x) encountered") ||
      /* Before libtiff 3.7.0beta2. */
      ! strcmp (fmt, "%.1000s: unknown field with tag %d (0x%x) encountered"))
    {
      va_list ap_test;

      G_VA_COPY (ap_test, ap);

      va_arg (ap_test, const char *); /* ignore first arg */

      tag = va_arg (ap_test, int);

      va_end (ap_test);
    }
  /* for older versions of libtiff? */
  else if (! strcmp (fmt, "unknown field with tag %d (0x%x) ignored") ||
           /* Since libtiff 4.0.0alpha. */
           ! strcmp (fmt, "Unknown field with tag %d (0x%x) encountered") ||
           /* Since libtiff 4.3.0rc1. */
           ! strcmp (fmt, "Unknown field with tag %u (0x%x) encountered"))
    {
      va_list ap_test;

      G_VA_COPY (ap_test, ap);

      tag = va_arg (ap_test, int);

      va_end (ap_test);
    }
  else if (! strcmp (fmt, "Incorrect value for \"%s\"; tag ignored"))
    {
      va_list ap_test;
      const char *stag;

      G_VA_COPY (ap_test, ap);

      stag = va_arg (ap_test, const char *);

      if (! strcmp (stag, "RichTIFFIPTC"))
        {
          gchar *msg = g_strdup_vprintf (fmt, ap);

          /* This is an error in Adobe products. Just report to stderr. */
          g_printerr ("[%s] %s\n", module, msg);
          g_free (msg);

          return;
        }

      va_end (ap_test);
    }

  /* Workaround for: http://bugzilla.gnome.org/show_bug.cgi?id=131975
   * Ignore the warnings about unregistered private tags (>= 32768).
   */
  if (tag >= 32768)
    return;

  /* Other unknown fields are only reported to stderr. */
  if (tag > 0)
    {
      gchar *msg = g_strdup_vprintf (fmt, ap);

      g_printerr ("%s\n", msg);
      g_free (msg);

      return;
    }
  else if (! strcmp (module, "TIFFReadDirectory") &&
           ! strcmp (fmt,
                     "Sum of Photometric type-related color channels and ExtraSamples doesn't match SamplesPerPixel."
                     " Defining non-color channels as ExtraSamples."))
    {
      /* We will process this issue in our code. Just report to stderr. */
      g_printerr ("%s: [%s] %s\n", G_STRFUNC, module, fmt);

      return;
    }
  else if (! strcmp (module, "Fax4Decode") ||
           g_str_has_prefix (module, "Fax3Decode"))
    {
      /* Certain corrupt TIFF Fax images can produce a large amount of
       * warnings which can cause GIMP to run out of GDI resources on
       * Windows and eventually crash.
       * The real problem seems to be that the amount of error console
       * messages does not have a limit.
       * See e.g. the first page of m1-8110934bb3b18d0e87ccc1ddfc5f0107.tif
       * from imagetestsuite. LibTiff does not return -1 from
       * ReadScanline, presumably because for fax images it's not
       * unreasonable to expect certain lines to fail.
       * Let's just only report to stderr in this case. */
      gchar *msg = g_strdup_vprintf (fmt, ap);

      g_printerr ("LibTiff warning: [%s] %s\n", module, msg);
      g_free (msg);

      return;
    }

  g_logv (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, fmt, ap);
}

static void
tiff_io_error (const gchar *module,
               const gchar *fmt,
               va_list      ap)
{
  gchar *msg;

  /* Workaround for: http://bugzilla.gnome.org/show_bug.cgi?id=132297
   * Ignore the errors related to random access and JPEG compression
   */
  if (! strcmp (fmt, "Compression algorithm does not support random access"))
    return;

  msg = g_strdup_vprintf (fmt, ap);

#ifdef TIFF_VERSION_BIG
  if (g_strcmp0 (fmt, "Maximum TIFF file size exceeded") == 0)
    /* @module in my tests were "TIFFAppendToStrip" but I wonder if
     * this same error could not happen with other "modules".
     */
    tiff_file_size_error = TRUE;
  else
    /* Easier for debugging to at least print messages on stderr. */
    g_printerr ("LibTiff error: [%s] %s\n", module, msg);
#endif

  g_log (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "%s", msg);
  g_free (msg);
}

static tsize_t
tiff_io_read (thandle_t handle,
              tdata_t   buffer,
              tsize_t   size)
{
  TiffIO *io    = (TiffIO *) handle;
  GError *error = NULL;
  gssize  read  = -1;

  if (io->can_seek)
    {
      gsize bytes_read = 0;

      if (! g_input_stream_read_all (io->input,
                                     (void *) buffer, (gsize) size,
                                     &bytes_read,
                                     NULL, &error))
        {
          g_printerr ("%s", error->message);
          g_clear_error (&error);
        }

      read = bytes_read;
    }
  else
    {
      if (io->position + size > io->used)
        {
          gsize missing;
          gsize bytes_read;

          missing = io->position + size - io->used;

          if (io->used + missing > io->allocated)
            {
              gchar *new_buffer;
              gsize  new_size = 1;
              gsize  needed;

              needed = io->used + missing - io->allocated;
              while (new_size < io->allocated + needed)
                new_size *= 2;

              new_buffer = g_try_realloc (io->buffer, new_size);
              if (! new_buffer)
                return -1;

              io->buffer    = new_buffer;
              io->allocated = new_size;
            }

          if (! g_input_stream_read_all (io->input,
                                         (void *) (io->buffer + io->used),
                                         missing,
                                         &bytes_read, NULL, &error))
            {
              g_printerr ("%s", error->message);
              g_clear_error (&error);
            }

          io->used += bytes_read;
        }

      g_assert (io->position + size <= io->used);

      memcpy (buffer, io->buffer + io->position, size);
      io->position += size;

      read = size;
    }

  return (tsize_t) read;
}

static tsize_t
tiff_io_write (thandle_t handle,
               tdata_t   buffer,
               tsize_t   size)
{
  TiffIO *io      = (TiffIO *) handle;
  GError *error   = NULL;
  gssize  written = -1;

  if (io->can_seek)
    {
      gsize bytes_written = 0;

      if (! g_output_stream_write_all (io->output,
                                       (void *) buffer, (gsize) size,
                                       &bytes_written,
                                       NULL, &error))
        {
          g_printerr ("%s", error->message);
          g_clear_error (&error);
        }

      written = bytes_written;
    }
  else
    {
      if (io->position + size > io->allocated)
        {
          gchar *new_buffer;
          gsize  new_size;

          new_size = io->position + size;

          new_buffer = g_try_realloc (io->buffer, new_size);
          if (! new_buffer)
            return -1;

          io->buffer   = new_buffer;
          io->allocated = new_size;
        }

      g_assert (io->position + size <= io->allocated);

      memcpy (io->buffer + io->position, buffer, size);
      io->position += size;

      io->used = MAX (io->used, io->position);

      written = size;
    }

  return (tsize_t) written;
}

static GSeekType
lseek_to_seek_type (gint whence)
{
  switch (whence)
    {
    default:
    case SEEK_SET:
      return G_SEEK_SET;

    case SEEK_CUR:
      return G_SEEK_CUR;

    case SEEK_END:
      return G_SEEK_END;
    }
}

static toff_t
tiff_io_seek (thandle_t handle,
              toff_t    offset,
              gint      whence)
{
  TiffIO   *io       = (TiffIO *) handle;
  GError   *error    = NULL;
  gboolean  sought   = FALSE;
  goffset   position = -1;

  if (io->can_seek)
    {
      sought = g_seekable_seek (G_SEEKABLE (io->stream),
                                (goffset) offset, lseek_to_seek_type (whence),
                                NULL, &error);
      if (sought)
        {
          position = g_seekable_tell (G_SEEKABLE (io->stream));
        }
      else
        {
          g_printerr ("%s", error->message);
          g_clear_error (&error);
        }
    }
  else
    {
      switch (whence)
        {
        default:
        case SEEK_SET:
          if (offset <= io->used)
            position = io->position = offset;
          break;

        case SEEK_CUR:
          if (io->position + offset <= io->used)
            position = io->position += offset;
          break;

        case G_SEEK_END:
          if (io->used + offset <= io->used)
            position = io->position = io->used + offset;
          break;
        }
    }

  return (toff_t) position;
}

static gint
tiff_io_close (thandle_t handle)
{
  TiffIO   *io     = (TiffIO *) handle;
  GError   *error  = NULL;
  gboolean  closed = FALSE;

  if (io->input && ! io->output)
    {
      closed = g_input_stream_close (io->input, NULL, &error);
    }
  else
    {
      if (! io->can_seek && io->buffer && io->allocated)
        {
          if (! g_output_stream_write_all (io->output,
                                           (void *) io->buffer,
                                           io->allocated,
                                           NULL, NULL, &error))
            {
              g_printerr ("%s", error->message);
              g_clear_error (&error);
            }
        }

      if (io->input)
        {
          closed = g_io_stream_close (G_IO_STREAM (io->stream), NULL, &error);
        }
      else
        {
          closed = g_output_stream_close (io->output, NULL, &error);
        }
    }

  if (! closed)
    {
      g_printerr ("%s", error->message);
      g_clear_error (&error);
    }

  g_object_unref (io->stream);
  io->stream = NULL;
  io->input  = NULL;
  io->output = NULL;

  g_free (io->buffer);
  io->buffer = NULL;

  io->allocated = 0;
  io->used      = 0;
  io->position  = 0;

  return closed ? 0 : -1;
}

static toff_t
tiff_io_get_file_size (thandle_t handle)
{
  TiffIO    *io    = (TiffIO *) handle;
  GError    *error = NULL;
  GFileInfo *info;
  goffset    size = 0;

  info = g_file_query_info (io->file,
                            G_FILE_ATTRIBUTE_STANDARD_SIZE,
                            G_FILE_QUERY_INFO_NONE,
                            NULL, &error);
  if (! info)
    {
      g_printerr ("%s", error->message);
      g_clear_error (&error);
    }
  else
    {
      size = g_file_info_get_size (info);
      g_object_unref (info);
    }

  return (toff_t) size;
}
