/* The GIMP -- an image manipulation program
 * Copyright (C) 1995, 1996, 1997 Spencer Kimball and Peter Mattis
 * Copyright (C) 1997 Josh MacDonald
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "config.h"

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <glib.h>

#ifdef G_OS_WIN32
#include <direct.h>		/* For _mkdir() */
#define mkdir(path,mode) _mkdir(path)
#endif

#include <glib-object.h>

#include "libgimpmath/gimpmath.h"

#include "core/core-types.h"

#include "base/temp-buf.h"

#include "core/gimpimage.h"

#include "file-utils.h"
#include "plug_in.h"


static PlugInProcDef *
file_proc_find_by_name (GSList      *procs,
		        const gchar *filename,
		        gboolean     skip_magic)
{
  GSList *p;
  gchar  *ext = strrchr (filename, '.');

  if (ext)
    ext++;

  for (p = procs; p; p = g_slist_next (p))
    {
      PlugInProcDef *proc = p->data;
      GSList        *prefixes;

      if (skip_magic && proc->magics_list)
	continue;

      for (prefixes = proc->prefixes_list;
	   prefixes;
	   prefixes = g_slist_next (prefixes))
	{
	  if (strncmp (filename, prefixes->data, strlen (prefixes->data)) == 0)
	    return proc;
	}
     }

  for (p = procs; p; p = g_slist_next (p))
    {
      PlugInProcDef *proc = p->data;
      GSList        *extensions;

      for (extensions = proc->extensions_list;
	   ext && extensions;
	   extensions = g_slist_next (extensions))
	{
	  gchar *p1 = ext;
	  gchar *p2 = (gchar *) extensions->data;

          if (skip_magic && proc->magics_list)
	    continue;

	  while (*p1 && *p2)
	    {
	      if (tolower (*p1) != tolower (*p2))
		break;

	      p1++;
	      p2++;
	    }

	  if (!(*p1) && !(*p2))
	    return proc;
	}
    }

  return NULL;
}

PlugInProcDef *
file_proc_find (GSList      *procs,
		const gchar *filename)
{
  PlugInProcDef *file_proc;
  PlugInProcDef *size_matched_proc = NULL;
  GSList        *all_procs         = procs;
  FILE          *ifp               = NULL;
  gint           head_size         = -2;
  gint           size_match_count  = 0;
  gint           match_val;
  guchar         head[256];

  /* First, check magicless prefixes/suffixes */
  if ( (file_proc = file_proc_find_by_name (all_procs, filename, TRUE)) != NULL)
    return file_proc;

  /* Then look for magics */
  while (procs)
    {
      file_proc = procs->data;
      procs = procs->next;

      if (file_proc->magics_list)
        {
          if (head_size == -2)
            {
              head_size = 0;
              if ((ifp = fopen (filename, "rb")) != NULL)
                head_size = fread ((gchar *) head, 1, sizeof (head), ifp);
            }
          if (head_size >= 4)
            {
              match_val = file_check_magic_list (file_proc->magics_list,
                                                 head_size, head, ifp);
              if (match_val == 2)  /* size match ? */
                { /* Use it only if no other magic matches */
                  size_match_count++;
                  size_matched_proc = file_proc;
                }
              else if (match_val)
                {
                  fclose (ifp);
                  return (file_proc);
                }
            }
        }
    }
  if (ifp) fclose (ifp);
  if (size_match_count == 1)
    return (size_matched_proc);

  /* As a last ditch, try matching by name */
  return file_proc_find_by_name (all_procs, filename, FALSE);
}

static void
file_convert_string (gchar *instr,
		     gchar *outmem,
		     gint   maxmem,
		     gint  *nmem)
{
  /* Convert a string in C-notation to array of char */
  guchar *uin = (guchar *) instr;
  guchar *uout = (guchar *) outmem;
  guchar  tmp[5], *tmpptr;
  gint    k;

  while ((*uin != '\0') && ((((char *)uout) - outmem) < maxmem))
    {
      if (*uin != '\\')   /* Not an escaped character ? */
        {
          *(uout++) = *(uin++);
          continue;
        }
      if (*(++uin) == '\0')
        {
          *(uout++) = '\\';
          break;
        }
      switch (*uin)
        {
          case '0':  case '1':  case '2':  case '3': /* octal */
            for (tmpptr = tmp; (tmpptr-tmp) <= 3;)
              {
                *(tmpptr++) = *(uin++);
                if (   (*uin == '\0') || (!isdigit (*uin))
                    || (*uin == '8') || (*uin == '9'))
                  break;
              }
            *tmpptr = '\0';
            sscanf ((char *)tmp, "%o", &k);
            *(uout++) = k;
            break;

          case 'a': *(uout++) = '\a'; uin++; break;
          case 'b': *(uout++) = '\b'; uin++; break;
          case 't': *(uout++) = '\t'; uin++; break;
          case 'n': *(uout++) = '\n'; uin++; break;
          case 'v': *(uout++) = '\v'; uin++; break;
          case 'f': *(uout++) = '\f'; uin++; break;
          case 'r': *(uout++) = '\r'; uin++; break;

          default : *(uout++) = *(uin++); break;
        }
    }
  *nmem = ((gchar *) uout) - outmem;
}

static gint
file_check_single_magic (gchar  *offset,
                         gchar  *type,
                         gchar  *value,
                         gint    headsize,
                         guchar *file_head,
                         FILE   *ifp)

{
  /* Return values are 0: no match, 1: magic match, 2: size match */
  glong   offs;
  gulong  num_testval, num_operatorval;
  gulong  fileval;
  gint    numbytes, k, c = 0, found = 0;
  gchar  *num_operator_ptr, num_operator, num_test;
  guchar  mem_testval[256];

  /* Check offset */
  if (sscanf (offset, "%ld", &offs) != 1) return (0);
  if (offs < 0) return (0);

  /* Check type of test */
  num_operator_ptr = NULL;
  num_operator = '\0';
  num_test = '=';
  if (strncmp (type, "byte", 4) == 0)
    {
      numbytes = 1;
      num_operator_ptr = type+4;
    }
  else if (strncmp (type, "short", 5) == 0)
    {
      numbytes = 2;
      num_operator_ptr = type+5;
    }
  else if (strncmp (type, "long", 4) == 0)
    {
      numbytes = 4;
      num_operator_ptr = type+4;
    }
  else if (strncmp (type, "size", 4) == 0)
    {
      numbytes = 5;
    }
  else if (strcmp (type, "string") == 0)
    {
      numbytes = 0;
    }
  else return (0);

  /* Check numerical operator value if present */
  if (num_operator_ptr && (*num_operator_ptr == '&'))
    {
      if (isdigit (num_operator_ptr[1]))
        {
          if (num_operator_ptr[1] != '0')      /* decimal */
            sscanf (num_operator_ptr+1, "%ld", &num_operatorval);
          else if (num_operator_ptr[2] == 'x') /* hexadecimal */
            sscanf (num_operator_ptr+3, "%lx", &num_operatorval);
          else                                 /* octal */
            sscanf (num_operator_ptr+2, "%lo", &num_operatorval);
          num_operator = *num_operator_ptr;
        }
    }

  if (numbytes > 0)   /* Numerical test ? */
    {
      /* Check test value */
      if ((value[0] == '=') || (value[0] == '>') || (value[0] == '<'))
      {
        num_test = value[0];
        value++;
      }
      if (!isdigit (value[0])) return (0);

      /* 
       * to anybody reading this: is strtol's parsing behaviour (e.g. "0x" prefix)
       * broken on some systems or why do we do the base detection ourselves?
       * */
      if (value[0] != '0')      /* decimal */
        num_testval = strtol(value, NULL, 10);
      else if (value[1] == 'x') /* hexadecimal */
        num_testval = (unsigned long)strtoul(value+2, NULL, 16);
      else                      /* octal */
        num_testval = strtol(value+1, NULL, 8);

      fileval = 0;
      if (numbytes == 5)    /* Check for file size ? */
        {
	  struct stat buf;
	  
          if (fstat (fileno (ifp), &buf) < 0) return (0);
          fileval = buf.st_size;
        }
      else if (offs + numbytes <= headsize)  /* We have it in memory ? */
        {
          for (k = 0; k < numbytes; k++)
          fileval = (fileval << 8) | (long)file_head[offs+k];
        }
      else   /* Read it from file */
        {
          if (fseek (ifp, offs, SEEK_SET) < 0) return (0);
          for (k = 0; k < numbytes; k++)
            fileval = (fileval << 8) | (c = getc (ifp));
          if (c == EOF) return (0);
        }
      if (num_operator == '&')
        fileval &= num_operatorval;

      if (num_test == '<')
        found = (fileval < num_testval);
      else if (num_test == '>')
        found = (fileval > num_testval);
      else
        found = (fileval == num_testval);

      if (found && (numbytes == 5)) found = 2;
    }
  else if (numbytes == 0) /* String test */
    {
      file_convert_string ((char *)value, (char *)mem_testval,
                           sizeof (mem_testval), &numbytes);
      if (numbytes <= 0) return (0);

      if (offs + numbytes <= headsize)  /* We have it in memory ? */
        {
          found = (memcmp (mem_testval, file_head+offs, numbytes) == 0);
        }
      else   /* Read it from file */
        {
          if (fseek (ifp, offs, SEEK_SET) < 0) return (0);
          found = 1;
          for (k = 0; found && (k < numbytes); k++)
            {
              c = getc (ifp);
              found = (c != EOF) && (c == (int)mem_testval[k]);
            }
        }
    }

  return found;
}

gint
file_check_magic_list (GSList *magics_list,
		       gint    headsize,
		       guchar *head,
		       FILE   *ifp)

{
  /* Return values are 0: no match, 1: magic match, 2: size match */
  gchar *offset;
  gchar *type;
  gchar *value;
  gint   and   = 0;
  gint   found = 0;
  gint   match_val;

  while (magics_list)
    {
      if ((offset = (gchar *)magics_list->data) == NULL) break;
      if ((magics_list = magics_list->next) == NULL) break;
      if ((type = (gchar *)magics_list->data) == NULL) break;
      if ((magics_list = magics_list->next) == NULL) break;
      if ((value = (gchar *)magics_list->data) == NULL) break;
      magics_list = magics_list->next;

      match_val = file_check_single_magic (offset, type, value,
                                           headsize, head, ifp);
      if (and)
	found = found && match_val;
      else
	found = match_val;

      and = (strchr (offset, '&') != NULL);

      if ((!and) && found)
	return match_val;
    }

  return 0;
}

TempBuf *
make_thumb_tempbuf (GimpImage *gimage)
{
  gint w, h;

  if (gimage->width<=80 && gimage->height<=60)
    {
      w = gimage->width;
      h = gimage->height;
    }
  else
    {
      /* Ratio molesting to fit within .xvpic thumbnail size limits */
      if (60 * gimage->width < 80 * gimage->height)
	{
	  h = 60;
	  w = (60 * gimage->width) / gimage->height;
	  if (w == 0)
	    w = 1;
	}
      else
	{
	  w = 80;
	  h = (80 * gimage->height) / gimage->width;
	  if (h == 0)
	    h = 1;
	}
    }

  /*printf("tn: %d x %d -> ", w, h);fflush(stdout);*/

  return gimp_viewable_get_preview (GIMP_VIEWABLE (gimage), w, h);
}

/* The readXVThumb function source may be re-used under
   the XFree86-style license. <adam@gimp.org> */
guchar *
readXVThumb (const gchar  *fnam,
	     gint         *w,
	     gint         *h,
	     gchar       **imginfo /* caller frees if != NULL */)
{
  FILE *fp;
  const gchar *P7_332 = "P7 332";
  gchar P7_buf[7];
  gchar linebuf[200];
  guchar *buf;
  gint twofivefive;
  void *ptr;

  *w = *h = 0;
  *imginfo = NULL;

  fp = fopen (fnam, "rb");
  if (!fp)
    return NULL;

  fread (P7_buf, 6, 1, fp);

  if (strncmp(P7_buf, P7_332, 6)!=0)
    {
      g_warning ("Thumbnail doesn't have the 'P7 332' header.");
      fclose (fp);
      return NULL;
    }

  /*newline*/
  fread (P7_buf, 1, 1, fp);

  do
    {
      ptr = fgets(linebuf, 199, fp);
      if ((strncmp(linebuf, "#IMGINFO:", 9) == 0) &&
	  (linebuf[9] != '\0') &&
	  (linebuf[9] != '\n'))
	{
	  if (linebuf[strlen(linebuf)-1] == '\n')
	    linebuf[strlen(linebuf)-1] = '\0';

	  if (linebuf[9] != '\0')
	    {
	      if (*imginfo)
		g_free(*imginfo);
	      *imginfo = g_strdup (&linebuf[9]);
	    }
	}
    }
  while (ptr && linebuf[0]=='#'); /* keep throwing away comment lines */

  if (!ptr)
    {
      /* g_warning("Thumbnail ended - not an image?"); */
      fclose (fp);
      return NULL;
    }

  sscanf(linebuf, "%d %d %d\n", w, h, &twofivefive);

  if (twofivefive!=255)
    {
      g_warning ("Thumbnail is of funky depth.");
      fclose (fp);
      return NULL;
    }

  if ((*w)<1 || (*h)<1 || (*w)>80 || (*h)>60)
    {
      g_warning ("Thumbnail size bad.  Corrupted?");
      fclose (fp);
      return NULL;
    }

  buf = g_malloc ((*w) * (*h));

  fread (buf, (*w) * (*h), 1, fp);

  fclose (fp);
  
  return buf;
}

gboolean
file_save_thumbnail (GimpImage   *gimage,
		     const gchar *full_source_filename,
		     TempBuf     *tempbuf)
{
  gint               i,j;
  gint               w,h;
  guchar            *tbd;
  gchar             *pathname;
  gchar             *filename;
  gchar             *xvpathname;
  gchar             *thumbnailname;
  GimpImageBaseType  basetype;
  FILE              *fp;
  struct stat        statbuf;

  if (stat (full_source_filename, &statbuf) != 0)
    {
      return FALSE;
    }

  pathname = g_path_get_dirname (full_source_filename);
  filename = g_path_get_basename (full_source_filename);

  xvpathname = g_strconcat (pathname, G_DIR_SEPARATOR_S, ".xvpics",
			    NULL);

  thumbnailname = g_strconcat (xvpathname, G_DIR_SEPARATOR_S,
			       filename,
			       NULL);

  tbd = temp_buf_data (tempbuf);

  w = tempbuf->width;
  h = tempbuf->height;
  /*printf("tn: %d x %d\n", w, h);fflush(stdout);*/

  mkdir (xvpathname, 0755);

  fp = fopen (thumbnailname, "wb");
  g_free (pathname);
  g_free (filename);
  g_free (xvpathname);
  g_free (thumbnailname);

  if (fp)
    {
      basetype = gimp_image_base_type (gimage);

      fprintf (fp,
	       "P7 332\n#IMGINFO:%dx%d %s (%d %s)\n"
	       "#END_OF_COMMENTS\n%d %d 255\n",
	       gimage->width, gimage->height,
	       (basetype == RGB) ? "RGB" :
	       (basetype == GRAY) ? "Greyscale" :
	       (basetype == INDEXED) ? "Indexed" :
	       "(UNKNOWN COLOUR TYPE)",
	       (int)statbuf.st_size,
	       (statbuf.st_size == 1) ? "byte" : "bytes",
	       w, h);

      switch (basetype)
	{
	case INDEXED:
	case RGB:
	  for (i=0; i<h; i++)
	    {
	      /* Do a cheap unidirectional error-spread to look better */
	      gint rerr=0, gerr=0, berr=0, a;

	      for (j=0; j<w; j++)
		{
		  gint32 r,g,b;

		  if (128 & *(tbd + 3))
		    {
		      r = *(tbd++) + rerr;
		      g = *(tbd++) + gerr;
		      b = *(tbd++) + berr;
		      tbd++;
		    }
		  else
		    {
		      a = (( (i^j) & 4 ) << 5) | 64; /* cute. */
		      r = a + rerr;
		      g = a + gerr;
		      b = a + berr;
		      tbd += 4;
		    }

		  r = CLAMP0255 (r);
		  g = CLAMP0255 (g);
		  b = CLAMP0255 (b);

		  fputc(((r>>5)<<5) | ((g>>5)<<2) | (b>>6), fp);

		  rerr = r - ( (r>>5) * 255 ) / 7;
		  gerr = g - ( (g>>5) * 255 ) / 7;
		  berr = b - ( (b>>6) * 255 ) / 3;
		}
	    }
	  break;

	case GRAY:
	  for (i=0; i<h; i++)
	    {
	      /* Do a cheap unidirectional error-spread to look better */
	      gint b3err=0, b2err=0, v, a;

	      for (j=0; j<w; j++)
		{
		  gint32 b3, b2;

		  v = *(tbd++);
		  a = *(tbd++);

		  if (!(128 & a))
		    v = (( (i^j) & 4 ) << 5) | 64;

		  b2 = v + b2err;
		  b3 = v + b3err;

		  b2 = CLAMP0255 (b2);
		  b3 = CLAMP0255 (b3);

		  fputc(((b3>>5)<<5) | ((b3>>5)<<2) | (b2>>6), fp);

		  b2err = b2 - ( (b2>>6) * 255 ) / 3;
		  b3err = b3 - ( (b3>>5) * 255 ) / 7;
		}
	    }
	  break;

	default:
	  g_warning("UNKNOWN GIMAGE TYPE IN THUMBNAIL SAVE");
	  break;
	}

      fclose (fp);
    }
  else /* Error writing thumbnail */
    {
      return FALSE;
    }

  return TRUE;
}
