/**************************************************************************

   Fotoxx      edit photos and manage collections

   Copyright 2007 2008 2009 2010 2011 2012 Michael Cornelison
   Source URL: http://kornelix.squarespace.com/fotoxx
   Contact: kornelix2@googlemail.com
   
   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 3 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, see http://www.gnu.org/licenses/.

***************************************************************************/

#define EX extern                                                          //  enable extern declarations
#include "fotoxx.h"


/**************************************************************************

   Fotoxx image editor - image metadata functions, EXIF/IPTC etc.

***************************************************************************/

int   get_mouse_tag(GtkTextView *, int px, int py, cchar *);               //  get tag selected by mouse
int   add_tag(char *tag, char *taglist, int maxcc);                        //  add tag if unique and enough space
int   del_tag(char *tag, char *taglist);                                   //  remove tag from tag list
int   add_recentag(char *tag);                                             //  add tag to recent tags, keep most recent
void  load_deftags();                                                      //  tags_defined file >> tags_deftags[]
void  save_deftags();                                                      //  tags_deftags[] >> defined_tags file
int   find_deftag(char *tag);                                              //  find tag in tags_deftags[]
int   add_deftag(char *catg, char *tag);                                   //  add tag to tags_deftags[]
int   del_deftag(char *tag);                                               //  remove tag from tags_deftags[]
void  deftags_stuff(zdialog *zd);                                          //  tags_deftags[] >> dialog widget "deftags"

char     tags_date[12] = "";                                               //  image date, yyyymmdd
char     tags_prdate[12] = "";                                             //  previous image date read or set
char     tags_stars = '0';                                                 //  image rating in stars, '0' to '5'
char     tags_cliktag[tagcc] = "";                                         //  tag clicked by mouse
char     tags_clikcatg[tagcc] = "";                                        //  category clicked by mouse
char    *tags_deftags[maxtagcats];                                         //  defined tags: catg: tag1, tag2, ... tagN,
char     tags_deftext[tagTcc] = "";                                        //  defined tags as flat text buffer
char     tags_filetags[tagFcc] = "";                                       //  tags for current image file
char     tags_recentags[tagRcc] = "";                                      //  recently added tags list
char     tags_batchAddTags[tagMcc] = "";                                   //  batch add tags list
char     tags_searchtags[tagScc] = "";                                     //  search tags list
char     tags_searchtext[tagScc] = "";                                     //  search comments & captions word list
char     tags_searchfiles[tagScc] = "";                                    //  search files list
char     tags_comments[tagFcc];                                            //  image comments
char     tags_caption[tagFcc];                                             //  image caption


/**************************************************************************/

//  View and edit IPTC Caption and EXIF UserComment data

void m_edit_cctext(GtkWidget *, cchar *menu)                               //  2 functions combined v.11.09
{
   int      edit_cctext_dialog_event(zdialog *zd, cchar *event);
   char     text[tagFcc];
   cchar    *title = ZTX("Edit Caption and Comments");

   if (menu) zfuncs::F1_help_topic = "edit_cctext";

   if (! curr_file) return;

   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }
   
   if (! zdeditcctext)                                                     //  popup dialog if not already
   {   
      zdeditcctext = zdialog_new(title,mWin,Bapply,Bcancel,null);
      zdialog *zd = zdeditcctext;
      zdialog_add_widget(zd,"hbox","hbcap","dialog");
      zdialog_add_widget(zd,"label","labcap","hbcap","Caption","space=5");
      zdialog_add_widget(zd,"frame","framecap","hbcap",0,"expand");
      zdialog_add_widget(zd,"edit","textcap","framecap",0,"space=5|wrap");    
      zdialog_add_widget(zd,"hbox","hbcom","dialog");
      zdialog_add_widget(zd,"label","labcom","hbcom","Comments","space=5");
      zdialog_add_widget(zd,"frame","framecom","hbcom",0,"expand");
      zdialog_add_widget(zd,"edit","textcom","framecom",0,"space=5|wrap");    
      zdialog_resize(zd,300,0);
      zdialog_help(zd,"edit_cctext");                                      //  zdialog help topic        v.11.08
      zdialog_run(zd,edit_cctext_dialog_event);
   }
   
   load_fileinfo(curr_file);                                               //  get current caption and comments
   repl_1str(tags_caption,text,"\\n","\n");                                //  replace "\n" with real newlines
   zdialog_stuff(zdeditcctext,"textcap",text);                             //  stuff into dialog
   repl_1str(tags_comments,text,"\\n","\n");                               //  replace "\n" with real newlines
   zdialog_stuff(zdeditcctext,"textcom",text);                             //  stuff into dialog

   return;
}


//  dialog event and completion callback function

int  edit_cctext_dialog_event(zdialog *zd, cchar *event)
{
   char     text[tagFcc];
   
   if (! zd->zstat) return 0;
   
   else if (zd->zstat == 1)                                                //  apply, save text
   {
      zd->zstat = 0;                                                       //  keep dialog active
      if (is_syncbusy()) return 0;                                         //  must wait for file sync      v.11.11
      zdialog_fetch(zd,"textcap",text,tagFcc);                             //  get new cctext
      repl_1str(text,tags_caption,"\n","\\n");                             //  replace newlines with "\n"
      zdialog_fetch(zd,"textcom",text,tagFcc);                             //  get new cctext
      repl_1str(text,tags_comments,"\n","\\n");                            //  replace newlines with "\n"
      Ftagschanged = 1;
      save_fileinfo(curr_file);                                            //  save in image file
   }

   else zdialog_free(zdeditcctext);                                        //  cancel

   return 1;
}


/**************************************************************************/

//  edit tags menu function

void m_edit_tags(GtkWidget *, cchar *menu)
{
   void  edittags_fixwidget(zdialog *, cchar * widgetname);                //  fix tags widget for selecting with mouse
   int   edittags_dialog_event(zdialog *zd, cchar *event);

   char        *ppv, starsN[12];

   if (menu) zfuncs::F1_help_topic = "edit_tags";

   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }
   
   if (! curr_file) return;

   if (! zdedittags)                                                       //  (re) start tag edit dialog 
   {
      zdedittags = zdialog_new(ZTX("Edit Tags"),mWin,Bapply,Bcancel,null);

      zdialog_add_widget(zdedittags,"hbox","hb1","dialog",0,"space=5");
      zdialog_add_widget(zdedittags,"label","labfile","hb1",ZTX("file:"),"space=5");
      zdialog_add_widget(zdedittags,"label","file","hb1");

      zdialog_add_widget(zdedittags,"hbox","hb2","dialog",0,"space=5");
      zdialog_add_widget(zdedittags,"label","lab21","hb2",ZTX("image date (yyyymmdd)"),"space=5");
      zdialog_add_widget(zdedittags,"entry","date","hb2",0,"scc=12");
      zdialog_add_widget(zdedittags,"button","prdate","hb2",ZTX("use last"),"space=5");

      zdialog_add_widget(zdedittags,"hbox","hb3","dialog",0,"space=5");
      zdialog_add_widget(zdedittags,"label","labstars","hb3",ZTX("image stars"),"space=5");
      zdialog_add_widget(zdedittags,"vbox","vb3","hb3");
      zdialog_add_widget(zdedittags,"hbox","hb31","vb3",0,"homog");
      zdialog_add_widget(zdedittags,"hbox","hb32","vb3",0,"homog");
      zdialog_add_widget(zdedittags,"label","lab30","hb31","0");
      zdialog_add_widget(zdedittags,"label","lab31","hb31","1");
      zdialog_add_widget(zdedittags,"label","lab32","hb31","2");
      zdialog_add_widget(zdedittags,"label","lab33","hb31","3");
      zdialog_add_widget(zdedittags,"label","lab34","hb31","4");
      zdialog_add_widget(zdedittags,"label","lab35","hb31","5");
      zdialog_add_widget(zdedittags,"radio","stars0","hb32",0);
      zdialog_add_widget(zdedittags,"radio","stars1","hb32",0);
      zdialog_add_widget(zdedittags,"radio","stars2","hb32",0);
      zdialog_add_widget(zdedittags,"radio","stars3","hb32",0);
      zdialog_add_widget(zdedittags,"radio","stars4","hb32",0);
      zdialog_add_widget(zdedittags,"radio","stars5","hb32",0);

      zdialog_add_widget(zdedittags,"hbox","hb4","dialog",0,"space=5");
      zdialog_add_widget(zdedittags,"label","lab4","hb4",ZTX("current tags"),"space=5");
      zdialog_add_widget(zdedittags,"frame","frame4","hb4",0,"expand");
      zdialog_add_widget(zdedittags,"edit","filetags","frame4",0,"expand|wrap");    //  v.11.06

      zdialog_add_widget(zdedittags,"hbox","hb5","dialog",0,"space=5");
      zdialog_add_widget(zdedittags,"label","recent","hb5",ZTX("recent tags"),"space=5");
      zdialog_add_widget(zdedittags,"frame","frame5","hb5",0,"expand");
      zdialog_add_widget(zdedittags,"edit","recentags","frame5",0,"expand|wrap");   //  v.11.06

      zdialog_add_widget(zdedittags,"hbox","hb8","dialog");
      zdialog_add_widget(zdedittags,"label","labdeftags","hb8",ZTX("defined tags"),"space=5");
      zdialog_add_widget(zdedittags,"hbox","hb9","dialog",0,"expand");
      zdialog_add_widget(zdedittags,"frame","frame8","hb9",0,"space=5|expand");
      zdialog_add_widget(zdedittags,"scrwin","scrwin8","frame8",0,"expand");
      zdialog_add_widget(zdedittags,"edit","deftags","scrwin8",0,"expand|wrap");    //  v.11.06

      zdialog_resize(zdedittags,500,500);                                  //  run dialog
      zdialog_help(zdedittags,"edit_tags");                                //  zdialog help topic        v.11.08
      zdialog_run(zdedittags,edittags_dialog_event);
      
      edittags_fixwidget(zdedittags,"filetags");                           //  setup for mouse tag selection
      edittags_fixwidget(zdedittags,"recentags");
      edittags_fixwidget(zdedittags,"deftags");

      load_deftags();                                                      //  stuff defined tags into dialog
      deftags_stuff(zdedittags);
   }

   load_fileinfo(curr_file);                                               //  get EXIF/IPTC data

   ppv = (char *) strrchr(curr_file,'/');
   zdialog_stuff(zdedittags,"file",ppv+1);                                 //  stuff dialog file name

   zdialog_stuff(zdedittags,"date",tags_date);                             //  stuff dialog data
   sprintf(starsN,"stars%c",tags_stars);
   zdialog_stuff(zdedittags,starsN,1);
   zdialog_stuff(zdedittags,"filetags",tags_filetags);
   zdialog_stuff(zdedittags,"recentags",tags_recentags);

   return;
}


//  setup tag display widget for tag selection using mouse clicks

void edittags_fixwidget(zdialog *zd, cchar * widgetname)
{
   void  edittags_mouse(GtkTextView *, GdkEventButton *, cchar *);         //  select tag via mouse click

   GtkWidget         *widget;
   GdkWindow         *gdkwin;

   widget = zdialog_widget(zd,widgetname);                                 //  make widget wrap text
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                    //  disable widget editing

   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),TEXTWIN);       //  cursor for tag selection
   gdk_window_set_cursor(gdkwin,arrowcursor);

   gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK);                    //  connect mouse-click event
   G_SIGNAL(widget,"button-press-event",edittags_mouse,widgetname);
}


//  edit tags mouse-click event function
//  get clicked tag and add to or remove from file tags and recent tags

void edittags_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname)
{
   int      mpx, mpy;

   if (event->type != GDK_BUTTON_PRESS) return;
   mpx = int(event->x);                                                    //  mouse click position
   mpy = int(event->y);
   
   get_mouse_tag(widget,mpx,mpy,widgetname);                               //  tags_cliktag = clicked tag in list
   if (! *tags_cliktag) return;

   if (strEqu(widgetname,"filetags")) {                                    //  remove tag from file tags
      del_tag(tags_cliktag,tags_filetags);
      zdialog_stuff(zdedittags,"filetags",tags_filetags);                  //  update dialog widgets
   }

   if (strEqu(widgetname,"recentags")) {                                   //  add recent tag to file tags
      add_tag(tags_cliktag,tags_filetags,tagFcc);
      zdialog_stuff(zdedittags,"filetags",tags_filetags);
   }

   if (strEqu(widgetname,"deftags")) {                                     //  add defined tag to file tags
      add_tag(tags_cliktag,tags_filetags,tagFcc);
      zdialog_stuff(zdedittags,"filetags",tags_filetags);
      add_recentag(tags_cliktag);                                          //  and to recent tags
      zdialog_stuff(zdedittags,"recentags",tags_recentags);
   }
   
   *tags_cliktag = 0;
   return;
}


//  dialog event and completion callback function

int edittags_dialog_event(zdialog *zd, cchar *event)
{
   int         err;
   
   if (zd->zstat) {
      if (zd->zstat == 1) {                                                //  [apply]
         zd->zstat = 0;                                                    //  keep dialog active
         if (is_syncbusy()) return 0;                                      //  must wait for file sync      v.11.11
         save_fileinfo(curr_file);                                         //  save tag changes
         return 0;
      }

      zdialog_free(zdedittags);                                            //  cancel - kill dialog
      return 0;
   }
   
   if (strEqu(event,"date")) {                                             //  image date revised
      err = zdialog_fetch(zd,"date",tags_date,11);
      if (err) return 1;
      if (strlen(tags_date) == 4) strcat(tags_date,"0101");                //  yyyy >> yyyy0101
      if (strlen(tags_date) == 6) strcat(tags_date,"01");                  //  yyyymm >> yyyymm01
      Ftagschanged++;
   }
   
   if (strEqu(event,"prdate")) {                                           //  repeat last date used
      if (*tags_prdate) {
         zdialog_stuff(zd,"date",tags_prdate);
         strcpy(tags_date,tags_prdate);
         Ftagschanged++;
      }
   }

   if (strnEqu(event,"stars",5)) {                                         //  event = stars0 to stars5
      tags_stars = event[5];                                               //  '0' to '5'
      Ftagschanged++;
   }
   
   if (strEqu(event,"tags-changed"))                                       //  get new defined tags data
      deftags_stuff(zdedittags);                                           //      v.11.02

   return 0;
}


/**************************************************************************/

//  manage tags menu function

zdialog  *zdmanagetags = 0;

void m_manage_tags(GtkWidget *, cchar *)                                   //  v.11.02 - separate edit and manage tags
{
   void  managetags_fixwidget(zdialog *, cchar * widgetname);              //  fix tags widget for selecting with mouse
   int   managetags_dialog_event(zdialog *zd, cchar *event);

   zfuncs::F1_help_topic = "manage_tags";

   if (zdmanagetags) return;
   zdmanagetags = zdialog_new(ZTX("Manage Tags"),mWin,Bcancel,null);

   zdialog_add_widget(zdmanagetags,"hbox","hb7","dialog",0,"space=5");
   zdialog_add_widget(zdmanagetags,"label","labcatg","hb7",ZTX("category"),"space=5");
   zdialog_add_widget(zdmanagetags,"entry","catg","hb7",0,"scc=12");
   zdialog_add_widget(zdmanagetags,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zdmanagetags,"label","labtag","hb7",ZTX("tag"),"space=5");
   zdialog_add_widget(zdmanagetags,"entry","tag","hb7",0,"scc=20|expand");
   zdialog_add_widget(zdmanagetags,"label","space","hb7",0,"space=5");
   zdialog_add_widget(zdmanagetags,"button","create","hb7",ZTX("create"));
   zdialog_add_widget(zdmanagetags,"button","delete","hb7",ZTX("delete"));

   zdialog_add_widget(zdmanagetags,"hbox","hb8","dialog");
   zdialog_add_widget(zdmanagetags,"label","labdeftags","hb8",ZTX("defined tags"),"space=5");
   zdialog_add_widget(zdmanagetags,"hbox","hb9","dialog",0,"expand");
   zdialog_add_widget(zdmanagetags,"frame","frame8","hb9",0,"space=5|expand");
   zdialog_add_widget(zdmanagetags,"scrwin","scrwin8","frame8",0,"expand");
   zdialog_add_widget(zdmanagetags,"edit","deftags","scrwin8",0,"expand|wrap");     //  v.11.06

   zdialog_resize(zdmanagetags,0,400);                                     //  run dialog
   zdialog_help(zdmanagetags,"manage_tags");                               //  zdialog help topic        v.11.08
   zdialog_run(zdmanagetags,managetags_dialog_event);
   
   managetags_fixwidget(zdmanagetags,"deftags");                           //  setup for mouse tag selection

   load_deftags();                                                         //  stuff defined tags into dialog
   deftags_stuff(zdmanagetags);

   return;
}


//  setup tag display widget for tag selection using mouse clicks

void managetags_fixwidget(zdialog *zd, cchar * widgetname)
{
   void  managetags_mouse(GtkTextView *, GdkEventButton *, cchar *);       //  select tag via mouse click

   GtkWidget         *widget;
   GdkWindow         *gdkwin;

   widget = zdialog_widget(zd,widgetname);                                 //  make widget wrap text
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                    //  disable widget editing

   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),TEXTWIN);       //  cursor for tag selection
   gdk_window_set_cursor(gdkwin,arrowcursor);

   gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK);                    //  connect mouse-click event
   G_SIGNAL(widget,"button-press-event",managetags_mouse,widgetname);
}


//  manage tags mouse-click event function
//  get clicked tag category or tag name

void managetags_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname)
{
   int      mpx, mpy, cc;

   if (event->type != GDK_BUTTON_PRESS) return;
   mpx = int(event->x);                                                    //  mouse click position
   mpy = int(event->y);
   
   cc = get_mouse_tag(widget,mpx,mpy,widgetname);                          //  tags_cliktag = clicked tag in list
   
   if (! cc) {
      if (*tags_clikcatg)                                                  //  selected category >> dialog widget
         zdialog_stuff(zdmanagetags,"catg",tags_clikcatg);
      return;
   }
   
   zdialog_stuff(zdmanagetags,"tag",tags_cliktag);                         //  selected tag >> dialog widget
   return;
}


//  dialog event and completion callback function

int managetags_dialog_event(zdialog *zd, cchar *event)
{
   char        tag[tagcc], catg[tagcc];
   int         changed = 0;
   
   if (zd->zstat) {
      zdialog_free(zdmanagetags);
      return 0;
   }
   
   if (strEqu(event,"create")) {                                           //  add new tag to defined tags
      zdialog_fetch(zd,"catg",catg,tagcc);
      zdialog_fetch(zd,"tag",tag,tagcc);
      add_deftag(catg,tag);
      changed++;
   }

   if (strEqu(event,"delete")) {                                           //  remove tag from defined tags
      zdialog_fetch(zd,"tag",tag,tagcc);
      del_deftag(tag);
      changed++;
   }

   if (changed) {   
      save_deftags();                                                      //  save tag updates to file
      deftags_stuff(zdmanagetags);                                         //  update dialog "deftags" window
      if (zdedittags)                                                      //  and edit tags window if active
         zdialog_send_event(zdedittags,"tags-changed");
   }

   return 0;
}


/**************************************************************************/

//  Convert mouse click position in a tag list into the selected tag.
//  cc of selected tag is returned, or zero if no tag clicked.
//  Selected tag is returned in tags_cliktag, or null if no tag clicked.
//  If a category is clicked, it is returned in tags_clikcatg and zero is returned.

int get_mouse_tag(GtkTextView *widget, int mpx, int mpy, cchar *widgetname)
{
   GtkTextIter    iter;
   int            tbx, tby, offset, cc;
   char           *ptext, *pp1, *pp2;
   
   *tags_cliktag = *tags_clikcatg = 0;                                     //  start with no tag or category   v.11.05

   gtk_text_view_window_to_buffer_coords(widget,GTK_TEXT_WINDOW_TEXT,mpx,mpy,&tbx,&tby);
   gtk_text_view_get_iter_at_location(widget,&iter,tbx,tby);
   offset = gtk_text_iter_get_offset(&iter);                               //  graphic position in widget text

   ptext = 0;   
   if (strEqu(widgetname,"filetags")) ptext = tags_filetags;               //  get corresponding text
   if (strEqu(widgetname,"recentags")) ptext = tags_recentags;
   if (strEqu(widgetname,"batchAddTags")) ptext = tags_batchAddTags;
   if (strEqu(widgetname,"searchtags")) ptext = tags_searchtags;
   if (strEqu(widgetname,"deftags")) ptext = tags_deftext;
   if (! ptext) return 0;

   pp1 = ptext + utf8_position(ptext,offset);                              //  graphic position to byte position

   if (! *pp1 || strchr(tagdelims":\n",*pp1)) return 0;                    //  reject edge position or delimiter
   while (pp1 > ptext && ! strchr(tagdelims":\n",pp1[-1])) pp1--;          //  find start of tag
   pp2 = pp1;
   while (pp2[1] && ! strchr(tagdelims":\n",pp2[1])) pp2++;                //  find following delimiter including ":"
   while (*pp1 == ' ') pp1++;                                              //  no leading or trailing blanks
   while (*pp2 == ' ') pp2--;
   cc = pp2 - pp1 + 1;
   if (cc <= 1) return 0;                                                  //  reject tag < 2 chars.
   if (cc >= tagcc) return 0;                                              //  reject tag too big
   if (pp2[1] == ':') {
      strncpy0(tags_clikcatg,pp1,cc+1);                                    //  tags_clikcatg = selected category
      return 0;                                                            //  return 0
   }
   strncpy0(tags_cliktag,pp1,cc+1);                                        //  tags_cliktag = selected tag
   return cc;                                                              //  return cc
}


/**************************************************************************/

//  add input tag to output tag list if not already there and enough room
//  returns:   0 = added OK     1 = not unique (case ignored)
//             2 = overflow     3 = bad utf8 characters     4 = null tag

int add_tag(char *tag, char *taglist, int maxcc)
{
   char     *pp1, *pp2, tag1[tagcc], tag2[tagcc];
   int      cc, cc1, cc2;

   strncpy0(tag1,tag,tagcc);                                               //  remove leading and trailing blanks
   cc = strTrim2(tag2,tag1);
   if (! cc) return 4;

   if (utf8_check(tag2)) {                                                 //  check for valid utf8 encoding
      printf("bad utf8 characters: %s \n",tag2);
      return 3;
   }
   
   while ((pp1 = strpbrk(tag2,tagdelims":"))) *pp1 = '-';                  //  replace problem characters

   strcpy(tag,tag2);                                                       //  replace tag with sanitized version
   
   pp1 = taglist;
   cc1 = strlen(tag);

   while (true)                                                            //  check if already in tag list
   {
      while (*pp1 == ' ' || *pp1 == tagdelim1) pp1++;
      if (! *pp1) break;
      pp2 = pp1 + 1;
      while (*pp2 && *pp2 != tagdelim1) pp2++;
      cc2 = pp2 - pp1;
      if (cc2 == cc1 && strncaseEqu(tag,pp1,cc1)) return 1;
      pp1 = pp2;
   }

   cc2 = strlen(taglist);                                                  //  append to tag list if space enough
   if (cc1 + cc2 + 3 > maxcc) return 2;
   strcpy(taglist + cc2,tag);
   strcpy(taglist + cc2 + cc1, tagdelim2);                                 //  add delimiter + space

   if (taglist == tags_filetags)                                           //  image tags were changed
      Ftagschanged++;
   return 0;
}


//  remove tag from taglist, if present
//  returns: 0 if found and deleted, otherwise 1

int del_tag(char *tag, char *taglist)
{
   int         ii, ftcc, atcc, found;
   char        *temptags;
   cchar       *pp;
   
   temptags = strdupz(taglist,0,"del_tag");
   
   *taglist = 0;
   ftcc = found = 0;
   
   for (ii = 1; ; ii++)
   {
      pp = strField(temptags,tagdelims,ii);                                //  next tag
      if (! pp) {
         zfree(temptags);
         if (found && taglist == tags_filetags)                            //  image tags were changed
            Ftagschanged++;
         return 1-found;
      }
      if (*pp == ' ') continue;
      
      if (strcaseEqu(pp,tag)) {                                            //  skip matching tag
         found = 1;
         continue;
      }

      atcc = strlen(pp);                                                   //  copy non-matching tag
      strcpy(taglist + ftcc, pp);
      ftcc += atcc;
      strcpy(taglist + ftcc, tagdelim2);                                   //  + delim + blank
      ftcc += 2;
   }
}


//  add new tag to recent tags, if not already.
//  remove oldest to make space if needed.

int add_recentag(char *tag)
{
   int         err;
   char        *pp, temptags[tagRcc];

   err = add_tag(tag,tags_recentags,tagRcc);                               //  add tag to recent tags

   while (err == 2)                                                        //  overflow
   {
      strncpy0(temptags,tags_recentags,tagRcc);                            //  remove oldest to make room
      pp = strpbrk(temptags,tagdelims);
      if (! pp) return 0;
      strcpy(tags_recentags,pp+2);                                         //  delimiter + blank before tag
      err = add_tag(tag,tags_recentags,tagRcc);
   }

   return 0;
}


/**************************************************************************/

//  Load tags_defined file into tags_deftags[ii] => category: tag1, tag2, ...
//  Read search_index file and add unmatched tags: => nocatg: tag1, tag2, ...

void load_deftags()
{
   int tags_Ucomp(cchar *tag1, cchar *tag2);

   static int  Floaded = 0;
   FILE        *fid;
   int         ii, jj, ntags, err, cc, tcc;
   int         ncats, catoverflow;
   int         nocat, nocatcc;
   char        tag[tagcc], catg[tagcc];
   char        tagsbuff[tagGcc];
   char        *pp1, *pp2;
   char        ptags[tagntc][tagcc];
   
   if (Floaded) return;                                                    //  use memory tags if already there  v.11.02
   Floaded++;

   for (ii = 0; ii < maxtagcats; ii++)                                     //  clean memory
      tags_deftags[ii] = 0;

   ncats = catoverflow = 0;

   fid = fopen(tags_defined_file,"r");                                     //  read tags_defined file
   if (fid) {
      while (true) {
         pp1 = fgets_trim(tagsbuff,tagGcc,fid);
         if (! pp1) break;
         if (ncats == maxtagcats-1) goto toomanycats;
         pp2 = strchr(pp1,':');                                            //  isolate "category:"
         if (! pp2) continue;                                              //  reject bad data
         cc = pp2 - pp1 + 1;
         if (cc > tagcc-1) continue;
         strncpy0(catg,pp1,cc);                                            //  (for error message)
         if (strlen(pp1) > tagGcc-2) goto cattoobig;
         pp2++;
         while (*pp2 == ' ') pp2++;
         if (strlen(pp2) < 3) continue;
         while ((pp2 = strpbrk(pp2,tagdelims))) *pp2++ = tagdelim1;        //  force comma delimiter        v.11.02
         tags_deftags[ncats] = strdupz(pp1,0,"def_tags");                  //  tags_deftags[ii]
         ncats++;                                                          //   = category: tag1, tag2, ... tagN,
      }
      err = fclose(fid);
      if (err) goto deftagserr;
   }

   nocat = ncats;                                                          //  make last category "nocatg" for
   ncats++;                                                                //   unmatched tags in search_index file
   tags_deftags[nocat] = zmalloc(tagGcc,"deftags");
   strcpy(tags_deftags[nocat],"nocatg: ");
   nocatcc = 8;

   fid = fopen(search_index_file,"r");                                     //  read search_index file
   if (! fid) return;

   while (true)
   {
      pp1 = fgets_trim(tagsbuff,tagGcc,fid);                               //  next image file tags record
      if (! pp1) break;
      if (strnNeq(tagsbuff,"tags: ",6)) continue;
      pp1 = pp1 + 6;
      
      while (true)
      {
         while (*pp1 && strchr(tagdelims" ",*pp1)) pp1++;                  //  next image tag start
         if (! *pp1) break;
         pp2 = strpbrk(pp1,tagdelims);                                     //  end
         if (! pp2) pp2 = pp1 + strlen(pp1);  
         cc = pp2 - pp1;
         if (cc > tagcc-1) {
            pp1 = pp2;                                                     //  bugfix    v.10.9
            continue;                                                      //  ignore huge tag
         }
         
         strncpy0(tag,pp1,cc+1);                                           //  look for tag in defined tags
         err = find_deftag(tag);
         if (! err) {                                                      //  found
            pp1 = pp2;
            continue;
         }

         if (nocatcc + cc + 2 > tagGcc-2) {
            catoverflow = 1;                                               //  nocatg: length limit reached
            break;
         }
         else {
            strcpy(tags_deftags[nocat] + nocatcc, tag);                    //  append tag to list
            nocatcc += cc;
            strcpy(tags_deftags[nocat] + nocatcc, tagdelim2);              //  + delim + blank
            nocatcc += 2;
         }

         pp1 = pp2;
      }
   }

   err = fclose(fid);
   if (err) goto filetagserr;
   if (catoverflow) goto cattoobig;
   
//  parse all the tags in each category and sort in ascending order

   for (ii = 0; ii < ncats; ii++)
   {
      pp1 = tags_deftags[ii];
      pp2 = strchr(pp1,':');
      cc = pp2 - pp1 + 1;
      strncpy0(catg,pp1,cc);
      pp1 = pp2 + 1;
      while (*pp1 == ' ') pp1++;
      tcc = 0;

      for (jj = 0; jj < tagntc; jj++)
      {
         if (! *pp1) break;
         pp2 = strchr(pp1,tagdelim1);
         if (pp2) cc = pp2 - pp1;
         else cc = strlen(pp1);
         if (cc > tagcc-1) cc = tagcc-1;
         strncpy0(ptags[jj],pp1,cc+1);
         pp1 += cc + 1;
         tcc += cc;
         while (*pp1 == ' ') pp1++;
      }
      
      ntags = jj;
      if (ntags == tagntc) goto cattoobig;
      HeapSort((char *) ptags,tagcc,ntags,tags_Ucomp);

      pp1 = tags_deftags[ii];
      tcc += strlen(catg) + 2 + 2 * ntags + 2;                             //  category, all tags, delimiters
      pp2 = zmalloc(tcc,"deftags");

      tags_deftags[ii] = pp2;                                              //  swap memory
      zfree(pp1);

      strcpy(pp2,catg);
      pp2 += strlen(catg);
      strcpy(pp2,": ");                                                    //  pp2 = "category: "
      pp2 += 2;

      for (jj = 0; jj < ntags; jj++)                                       //  add the sorted tags
      {
         strcpy(pp2,ptags[jj]);                                            //  append tag + delim + blank
         pp2 += strlen(pp2);
         strcpy(pp2,tagdelim2);
         pp2 += 2;
      }
      
      *pp2 = 0;
   }
   
//  sort the categories in ascending order
//  leave "nocatg" at the end

   for (ii = 0; ii < ncats-1; ii++)
   for (jj = ii+1; jj < ncats-1; jj++)                                     //  v.11.02
   {
      pp1 = tags_deftags[ii];
      pp2 = tags_deftags[jj];
      if (strcasecmp(pp1,pp2) > 0) {
         tags_deftags[ii] = pp2;
         tags_deftags[jj] = pp1;
      }
   }

   return;

toomanycats:
   zmessLogACK(mWin,"more than %d categories",maxtagcats);
   fclose(fid);
   return;
   
cattoobig:
   zmessLogACK(mWin,"category %s is too big",catg);
   fclose(fid);
   return;

deftagserr:
   zmessLogACK(mWin,"tags_defined file error: %s",strerror(errno));
   return;

filetagserr:
   zmessLogACK(mWin,"search_index file error: %s",strerror(errno));
   return;
}


//  compare function for tag sorting
//  wcscasecmp does not work for plain ascii
//  so what happens to Chinese, etc?

int tags_Ucomp(cchar *tag1, cchar *tag2)
{
   return strcasecmp(tag1,tag2);
}


//  write tags_deftags[] memory data to the defined tags file if any changes were made

void save_deftags()
{
   int         ii, err;
   FILE        *fid;

   fid = fopen(tags_defined_file,"w");                                     //  write tags_defined file
   if (! fid) goto deftagserr;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      if (! tags_deftags[ii+1]) break;                                     //  omit last category, "nocatg"
      err = fprintf(fid,"%s\n",tags_deftags[ii]);                          //  each record: 
      if (err < 0) goto deftagserr;                                        //    category: tag1, tag2, ... tagN,
   }

   err = fclose(fid);
   if (err) goto deftagserr;
   return;
   
deftagserr:
   zmessLogACK(mWin,"tags_defined file error: %s",strerror(errno));
   return;
}


//  find a given tag in tags_deftags[]
//  return: 0 = found, 1 = not found

int find_deftag(char *tag)
{
   int      ii, cc;
   char     tag2[tagcc+4];
   char     *pp;

   if (! tag || *tag <= ' ') return 0;                                     //  bad tag

   strncpy0(tag2,tag,tagcc);                                               //  construct tag + delim + blank
   cc = strlen(tag2);
   strcpy(tag2+cc,tagdelim2);
   cc += 2;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp = tags_deftags[ii];                                               //  category: tag1, tag2, ... tagN, 
      if (! pp) return 1;                                                  //  not found

      while (pp)
      {
         pp = strcasestr(pp,tag2);                                         //  look for delim + blank + tag + delim
         if (! pp) break;
         if (strchr(tagdelims":", pp[-2])) return 0;
         pp += cc;
      }
   }

   return 1;
}


//  add new tag to tags_deftags[] >> category: tag1, tag2, ... newtag,
//  returns:   0 = added OK     1 = not unique (case ignored)
//             2 = overflow     3 = bad utf8 characters     4 = null tag
//  if tag present under another category, it is moved to new category

int add_deftag(char *catg, char *tag)
{
   int         ii, cc, cc1, cc2;
   char        tag1[tagcc], tag2[tagcc];
   char        *pp1, *pp2;
   
   strncpy0(tag1,tag,tagcc);                                               //  remove leading and trailing blanks
   cc = strTrim2(tag2,tag1);
   if (! cc) return 4;

   if (utf8_check(tag2)) {                                                 //  check for valid utf8 encoding
      printf("bad utf8 characters: %s \n",tag2);
      return 3;
   }

   while ((pp1 = strpbrk(tag2,tagdelims":"))) *pp1 = '-';                  //  replace problem characters
   
   strcpy(tag,tag2);                                                       //  replace tag with sanitized version

   del_deftag(tag);                                                        //  delete if already there

   if (! catg || *catg <= ' ') catg = (char *) "nocatg";

   cc1 = strlen(catg);

   for (ii = 0; ii < maxtagcats; ii++)                                     //  look for given category
   {
      pp1 = tags_deftags[ii];
      if (! pp1) goto newcatg;
      if (! strnEqu(catg,pp1,cc1)) continue;
      if (pp1[cc1] == ':') goto oldcatg;
   }

newcatg:
   if (ii == maxtagcats) goto toomanycats;
   cc1 = strlen(catg) + strlen(tag) + 6;
   pp1 = zmalloc(cc1,"deftags");
   *pp1 = 0;
   strncatv(pp1,cc1,catg,": ",tag,tagdelim2,null);                         //  category: + tag + delim + blank
   tags_deftags[ii] = tags_deftags[ii-1];                                  //  move "nocatg" record to next slot
   tags_deftags[ii-1] = pp1;                                               //  insert new record before
   return 0;

oldcatg:
   cc1 = strlen(tag);
   pp2 = pp1 + 2;
   while (true) {
      pp2 = strcasestr(pp2,tag);                                           //  look for delim + blank + tag + delim
      if (! pp2) break;                                                    //        or colon + blank + tag + delim
      if (strchr(tagdelims,pp2[cc]) && strchr(tagdelims":", pp2[-2]))
         return 1;                                                         //  tag not unique
      pp2 += cc1;
   }
   cc2 = strlen(pp1);                                                      //  add new tag to old record
   if (cc1 + cc2 + 4 > tagGcc) goto cattoobig;
   pp2 = strdupz(pp1,cc2+4,"def_tags");                                    //  expand string
   zfree(pp1);
   tags_deftags[ii] = pp2;
   strcpy(pp2+cc2,tag);                                                    //  old record + tag + delim + blank
   strcpy(pp2+cc2+cc1,tagdelim2);
   return 0;

toomanycats:
   zmessLogACK(mWin,"more than %d categories",maxtagcats);
   return 2;

cattoobig:
   zmessLogACK(mWin,"category is too big");
   return 2;
}


//  delete tag from defined tags list, tags_deftags[] 
//  return: 0 = found and deleted, 1 = not found

int del_deftag(char *tag)
{
   int      ii, cc;
   char     tag2[tagcc+4];
   char     *pp, *pp1, *pp2;

   if (! tag || *tag <= ' ') return 1;                                     //  bad tag

   strncpy0(tag2,tag,tagcc);                                               //  construct tag + delim + blank
   cc = strlen(tag2);
   strcpy(tag2+cc,tagdelim2);
   cc += 2;

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp = tags_deftags[ii];
      if (! pp) return 1;                                                  //  not found

      while (pp)
      {
         pp = strcasestr(pp,tag2);                                         //  look for prior delim or colon
         if (! pp) break;
         if (strchr(tagdelims":", pp[-2])) goto found;
         pp += cc;
      }
   }
   
found:
   for (pp1 = pp, pp2 = pp+cc; *pp2; pp1++, pp2++)                         //  eliminate tag, delim, blank
      *pp1 = *pp2;
   *pp1 = 0;
   
   return 0;
}


//  stuff tags_deftags[] into given text widget and format by category
//  create tags_deftext with flat list of tags for mouse clicking

void deftags_stuff(zdialog *zd)
{
   GtkWidget      *widget;
   GtkTextBuffer  *textbuff;
   GtkTextIter    iter1, iter2;
   int            ii, cc;
   char           catgname[tagcc+3];
   char           *pp1, *pp2;
   
   widget = zdialog_widget(zd,"deftags");
   wclear(widget);

   for (ii = 0; ii < maxtagcats; ii++)
   {
      pp1 = tags_deftags[ii];
      if (! pp1) break;
      pp2 = strchr(pp1,':');
      if (! pp2) continue;
      if (pp2 > pp1 + tagcc-3) continue;
      pp2 += 2;
      cc = pp2 - pp1 + 1;
      strncpy0(catgname,pp1,cc);
      wprintx(widget,0,catgname,"monospace bold 9");
      if (*pp2) wprintx(widget,0,pp2,"monospace 9");
      wprintx(widget,0,"\n");
   }
   
   textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
   gtk_text_buffer_get_bounds(textbuff,&iter1,&iter2);
   pp1 = gtk_text_buffer_get_text(textbuff,&iter1,&iter2,0);
   strncpy0(tags_deftext,pp1,tagTcc);

   return;
}


/**************************************************************************/

//  image file EXIF/IPTC data >> tags_date, tags_stars, tags_filetags, 
//                               tags_comments, tags_caption  in memory

void load_fileinfo(cchar *file)
{
   void exif_tagdate(cchar *exifdate, char *tagdate);

   int         ii, jj, cc;
   char        *pp;
   cchar       *exifkeys[5] = { exif_date_key, iptc_tags_key, iptc_rating_key, 
                                exif_comment_key, iptc_caption_key };
   char        **ppv, *imagedate, *imagetags, *imagestars, *imagecomms, *imagecapt;

   *tags_filetags = *tags_date = *tags_comments = *tags_caption = 0;
   tags_stars = '0';
   
   ppv = info_get(file,exifkeys,5);                                        //  stars rating added        v.10.0
   imagedate = ppv[0];
   imagetags = ppv[1];
   imagestars = ppv[2];
   imagecomms = ppv[3];
   imagecapt = ppv[4];

   if (imagedate) {
      exif_tagdate(imagedate,tags_date);                                   //  EXIF date/time >> yyyymmdd
      strcpy(tags_prdate,tags_date);
      zfree(imagedate);
   }

   if (imagetags)
   {
      for (ii = 1; ; ii++)
      {
         pp = (char *) strField(imagetags,tagdelims,ii);
         if (! pp) break;
         if (*pp == ' ') continue;
         cc = strlen(pp);
         if (cc >= tagcc) continue;                                        //  reject tags too big
         for (jj = 0; jj < cc; jj++)
            if (pp[jj] > 0 && pp[jj] < ' ') break;                         //  reject tags with control characters
         if (jj < cc) continue;
         add_tag(pp,tags_filetags,tagFcc);                                 //  add to file tags if unique
      }

      zfree(imagetags);
   }

   if (imagestars) {                                                       //  v.10.0
      tags_stars = *imagestars;
      if (tags_stars < '0' || tags_stars > '5') tags_stars = '0';
      zfree(imagestars);
   }
   
   if (imagecomms) {                                                       //  v.10.11
      strncpy0(tags_comments,imagecomms,tagFcc);
      zfree(imagecomms);
   }

   if (imagecapt) {                                                        //  v.10.12
      strncpy0(tags_caption,imagecapt,tagFcc);
      zfree(imagecapt);
   }

   Ftagschanged = 0;
   return;
}


//  tags_date, tags_stars, tags_filetags in memory >> image file EXIF/IPTC data
//  update defined tags file if any changes

void save_fileinfo(cchar *file)
{
   void tag_exifdate(cchar *tagdate, char *exifdate);
   
   cchar       *exifkeys[5] = { exif_date_key, iptc_tags_key, iptc_rating_key, 
                                exif_comment_key, iptc_caption_key };
   cchar       *exifdata[5];
   char        imagedate[24], sstars[4];

   if (! Ftagschanged) return;                                             //  no changes to tags etc.
   Ftagschanged = 0;

   *imagedate = 0;
   if (*tags_date) {
      tag_exifdate(tags_date,imagedate);                                   //  yyyymmdd >> EXIF date/time
      strcpy(tags_prdate,tags_date);
   }
   
   sstars[0] = tags_stars;                                                 //  string for stars rating
   sstars[1] = 0;

   exifdata[0] = imagedate;                                                //  update file EXIF/IPTC data
   exifdata[1] = tags_filetags;
   exifdata[2] = sstars;
   exifdata[3] = tags_comments;                                            //  v.10.11
   exifdata[4] = tags_caption;                                             //  v.10.12
   info_put(file,exifkeys,exifdata,5);

   update_search_index(file);                                              //  update search index file

   if (zdexifview) info_view(0);                                           //  live EXIF/IPTC update   v.10.2
   return;
}


//  update search index file (replace updated file data)                   //  overhauled    v.12.01

void update_search_index(cchar *file)
{
   char           *ppv, temp_search_index_file[200];
   char           imagedate[12], filedate[16];
   char           buff[tagrecl];
   int            err, eof = 0, fcopy = 0, comp = 0, finserted = 0;
   FILE           *fidr, *fidw;
   struct stat    statb;
   struct tm      bdt;

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11

   strcpy(temp_search_index_file,search_index_file);                       //  temp tag file
   strcat(temp_search_index_file,"_temp");

   fidw = fopen(temp_search_index_file,"w");                               //  write temp tag file (new)
   if (! fidw) goto tagserror;
   
   fidr = fopen(search_index_file,"r");                                    //  read tag file (old)
   
   if (fidr)
   {   
      while (true)                                                         //  copy search index file to temp
      {                                                                    //    file, inserting or replacing
         ppv = fgets_trim(buff,tagrecl,fidr);                              //      records for caller's file
         if (! ppv) eof = 1;

         if (strnEqu(buff,"file: ",6))                                     //  start of next file entry
         {
            comp = strcmp(file,buff+6);
            if (comp > 0) fcopy = 1;                                       //  new entry is after this one
            if (comp == 0) fcopy = 0;                                      //  new entry replaces this one
            if (comp < 0) fcopy = 1;                                       //  new entry is before this one
         }

         if (comp <= 0 && ! finserted)                                     //  new entry inserts here    v.12.01
         {
            err = fprintf(fidw,"file: %s\n",file);                         //  output filespec
            if (err <= 0) goto tagserror;

            if (*tags_date) {                                              //  image date
               strncpy(imagedate,tags_date,4);
               strncpy(imagedate+5,tags_date+4,2);                         //  yyyymmdd >> yyyy:mm:dd
               strncpy(imagedate+8,tags_date+6,2);
               imagedate[4] = imagedate[7] = ':';
               imagedate[10] = 0;
            }
            else strcpy(imagedate,"null");                                 //  missing image date

            err = stat(file,&statb);
            gmtime_r(&statb.st_mtime,&bdt);
            sprintf(filedate,"%04d%02d%02d%02d%02d%02d",                   //  file date = yyyymmddhhmmss
                     bdt.tm_year + 1900, bdt.tm_mon + 1, bdt.tm_mday,
                     bdt.tm_hour, bdt.tm_min, bdt.tm_sec);

            err = fprintf(fidw,"date: %s  %s\n",imagedate,filedate);       //  output image and file date

            if (*tags_filetags)                                            //  output filetags
               err = fprintf(fidw,"tags: %s\n",tags_filetags);
            else  err = fprintf(fidw,"tags: null" tagdelim2 "\n");         //  "null" tag if none      v.11.02
           
            err = fprintf(fidw,"stars: %c\n",tags_stars);                  //  output stars rating
            
            if (*tags_comments)                                            //  output user comments
               err = fprintf(fidw,"comms: %s\n",tags_comments);
            else err = fprintf(fidw,"comms: null \n");                     //  "null" if none

            if (*tags_caption)                                             //  output user caption    v.10.12
               err = fprintf(fidw,"capt: %s\n",tags_caption);
            else err = fprintf(fidw,"capt: null \n");                      //  "null" if none

            err = fprintf(fidw,"\n");                                      //  EOL

            finserted = 1;
         }

         if (eof) break;
         if (fcopy) fprintf(fidw,"%s\n",buff);                             //  copy this entry
      }

      err = fclose(fidr);
      if (err) goto tagserror;
   }

   err = fclose(fidw);                                                     //  close temp file
   if (err) goto tagserror;
   
   err = rename(temp_search_index_file,search_index_file);                 //  replace tags file with temp file
   if (err) goto tagserror;
   return;
   
tagserror:
   zmessLogACK(mWin,ZTX("search index file error: %s"),strerror(errno));
   return;
}


//  file is deleted from search index file

void delete_search_index(cchar *file)                                      //  overhauled  v.10.11.2
{
   char           *ppv, temp_search_index_file[200];
   char           indexbuff[tagrecl];
   char           filebuff[tagrecl], datebuff[tagrecl];
   char           tagsbuff[tagrecl], starsbuff[tagrecl];
   char           commsbuff[tagrecl], captbuff[tagrecl];
   int            err, ftf = 1;
   FILE           *fidr, *fidw;

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11

   strcpy(temp_search_index_file,search_index_file);                       //  temp tag file
   strcat(temp_search_index_file,"_temp");

   fidr = fopen(search_index_file,"r");                                    //  read tag file
   if (! fidr) return;
   
   fidw = fopen(temp_search_index_file,"w");                               //  write temp tag file
   if (! fidw) goto tagserror;
   
   while (true)                                                            //  copy search index file to temp
   {                                                                       //    file, omitting this image file
      ppv = fgets_trim(indexbuff,tagrecl,fidr);

      if (! ppv || strnEqu(indexbuff,"file: ",6))                          //  EOF or start of next file
      {
         if (! ftf && strNeq(filebuff+6,file))                             //  not 1st time and not file to omit
         {
            fprintf(fidw,"%s\n",filebuff);                                 //  output completed data for previous file
            fprintf(fidw,"%s\n",datebuff);
            fprintf(fidw,"%s\n",tagsbuff);
            fprintf(fidw,"%s\n",starsbuff);
            fprintf(fidw,"%s\n",commsbuff);
            fprintf(fidw,"%s\n",captbuff);
            fprintf(fidw,"\n");
         }
         
         if (! ppv) break;                                                 //  EOF

         ftf = 0;
         strncpy0(filebuff,indexbuff,tagrecl);                             //  next file: filespec record
         strcpy(datebuff,"date: null ");                                   //  initz. remaining data = empty
         strcpy(tagsbuff,"tags: null"tagdelim2);                           //  v.11.02
         strcpy(starsbuff,"stars: 0 ");
         strcpy(commsbuff,"comms: null ");
         strcpy(captbuff,"capt: null ");
      }
      
      if (strnEqu(indexbuff,"date: ",6))                                   //  copy whatever is found
         strncpy0(datebuff,indexbuff,tagrecl);

      if (strnEqu(indexbuff,"tags: ",6))
         strncpy0(tagsbuff,indexbuff,tagrecl);

      if (strnEqu(indexbuff,"stars: ",7))
         strncpy0(starsbuff,indexbuff,tagrecl);

      if (strnEqu(indexbuff,"comms: ",7))
         strncpy0(commsbuff,indexbuff,tagrecl);

      if (strnEqu(indexbuff,"capt: ",6))
         strncpy0(captbuff,indexbuff,tagrecl);
   }

   err = fclose(fidr);
   if (err) goto tagserror;

   err = fclose(fidw);
   if (err) goto tagserror;
   
   err = rename(temp_search_index_file,search_index_file);                 //  replace tags file with temp file
   if (err) goto tagserror;

   return;

tagserror:
   zmessLogACK(mWin,ZTX("search index file error: %s"),strerror(errno));
   return;
}


/**************************************************************************/

//  menu function - add tags to many files at once

char        **batchAddTags_filelist = 0;
int         batchAddTags_filecount = 0;
zdialog     *zdbatchAddTags = null;                                        //  batch add tags dialog

void m_batchAddTags(GtkWidget *, cchar *)                                  //  new v.9.7
{
   void  batchAddTags_fixwidget(zdialog *zd, cchar * widgetname);
   int   batchAddTags_dialog_event(zdialog *zd, cchar *event);
   
   char       *ptag, **flist, *file;
   int         ii, jj, err;

   zfuncs::F1_help_topic = "batch_add_tags";                               //  v.10.8
   
   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  v.10.5

   zdbatchAddTags = zdialog_new(ZTX("Batch Add Tags"),mWin,Bproceed,Bcancel,null);

   zdialog_add_widget(zdbatchAddTags,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zdbatchAddTags,"label","lab1","hb1",ZTX("tags to add"),"space=10");
   zdialog_add_widget(zdbatchAddTags,"frame","frame1","hb1",0,"expand");
   zdialog_add_widget(zdbatchAddTags,"edit","batchAddTags","frame1",0,"expand|wrap");     //  v.11.06

   zdialog_add_widget(zdbatchAddTags,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zdbatchAddTags,"button","createtag","hb2",ZTX("create tag"),"space=10");
   zdialog_add_widget(zdbatchAddTags,"entry","ctag","hb2",0);

   zdialog_add_widget(zdbatchAddTags,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zdbatchAddTags,"button","files","hb3",Bselectfiles,"space=10");
   zdialog_add_widget(zdbatchAddTags,"label","labcount","hb3","0 files selected","space=10");

   zdialog_add_widget(zdbatchAddTags,"hbox","space","dialog",0,"space=3");
   zdialog_add_widget(zdbatchAddTags,"hbox","hb5","dialog");
   zdialog_add_widget(zdbatchAddTags,"label","labdeftags","hb5",ZTX("defined tags"));
   zdialog_add_widget(zdbatchAddTags,"frame","frame5","dialog",0,"space=5|expand");
   zdialog_add_widget(zdbatchAddTags,"scrwin","scrwin5","frame5",0,"expand");
   zdialog_add_widget(zdbatchAddTags,"edit","deftags","scrwin5",0,"expand|wrap");         //  v.11.06

   load_deftags();                                                         //  stuff defined tags into dialog
   deftags_stuff(zdbatchAddTags);

   batchAddTags_filelist = 0;
   batchAddTags_filecount = 0;
   *tags_batchAddTags = 0;

   zdialog_resize(zdbatchAddTags,400,400);                                 //  run dialog
   zdialog_help(zdbatchAddTags,"batch_add_tags");                          //  zdialog help topic        v.11.08
   zdialog_run(zdbatchAddTags,batchAddTags_dialog_event);
   
   batchAddTags_fixwidget(zdbatchAddTags,"batchAddTags");                  //  setup for mouse tag selection
   batchAddTags_fixwidget(zdbatchAddTags,"deftags");

   zdialog_wait(zdbatchAddTags);                                           //  wait for dialog completion

   flist = batchAddTags_filelist;

   if (zdbatchAddTags->zstat != 1) goto cleanup;
   if (! batchAddTags_filecount) goto cleanup;
   if (*tags_batchAddTags <= ' ') goto cleanup;                            //  v.10.5

   write_popup_text("open","Adding Tags",500,200,mWin);                    //  status monitor popup window  v.11.01

   for (ii = 0; flist[ii]; ii++)                                           //  loop all selected files
   {
      file = flist[ii];                                                    //  display image
      err = f_open(file,0);
      if (err) continue;

      write_popup_text("write",file);                                      //  report progress
      zmainloop();

      load_fileinfo(file);                                                 //  load current file tags

      for (jj = 1; ; jj++)                                                 //  add new tags unless already
      {
         ptag = (char *) strField(tags_batchAddTags,tagdelims,jj);
         if (! ptag) break;
         if (*ptag == ' ') continue;
         err = add_tag(ptag,tags_filetags,tagFcc);
         if (err == 2) 
            zmessageACK(mWin,ZTX("%s \n too many tags"),file);
      }

      save_fileinfo(file);                                                 //  save tag changes
   }

   write_popup_text("write","COMPLETED");
   write_popup_text("close",0);
   
cleanup:

   zdialog_free(zdbatchAddTags);

   if (batchAddTags_filecount) {
      for (ii = 0; flist[ii]; ii++) 
         zfree(flist[ii]);
      zfree(flist);
   }

   menulock(0);
   return;
}


//  setup tag display widget for tag selection using mouse clicks

void batchAddTags_fixwidget(zdialog *zd, cchar * widgetname)
{
   void batchAddTags_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname);

   GtkWidget         *widget;
   GdkWindow         *gdkwin;

   widget = zdialog_widget(zd,widgetname);                                 //  make widget wrap text
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                    //  disable widget editing

   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),TEXTWIN);       //  cursor for tag selection
   gdk_window_set_cursor(gdkwin,arrowcursor);

   gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK);                    //  connect mouse-click event
   G_SIGNAL(widget,"button-press-event",batchAddTags_mouse,widgetname);
}


//  batchAddTags mouse-click event function
//  get clicked tag and add to or remove from tags_batchAddTags

void batchAddTags_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname)
{
   int            mpx, mpy, cc;

   if (event->type != GDK_BUTTON_PRESS) return;
   mpx = int(event->x);                                                    //  mouse click position
   mpy = int(event->y);

   cc = get_mouse_tag(widget,mpx,mpy,widgetname);                          //  tags_cliktag = clicked tag in list
   if (! cc) return;

   if (strEqu(widgetname,"batchAddTags")) {
      del_tag(tags_cliktag,tags_batchAddTags);                             //  remove tag from tags_batchAddTags
      zdialog_stuff(zdbatchAddTags,"batchAddTags",tags_batchAddTags);      //  update dialog widgets
   }
   
   if (strEqu(widgetname,"deftags")) {
      add_tag(tags_cliktag,tags_batchAddTags,tagMcc);                      //  add defined tag to tags_batchAddTags
      zdialog_stuff(zdbatchAddTags,"batchAddTags",tags_batchAddTags);
   }

   return;
}


//  batchAddTags dialog event function

int batchAddTags_dialog_event(zdialog *zd, cchar *event)
{
   int      ii, err;
   char     tag[tagcc];
   char     **flist = batchAddTags_filelist, countmess[50];

   if (strEqu(event,"createtag")) {
      err = zdialog_fetch(zd,"ctag",tag,tagcc);                            //  add new tag to list
      if (err) return 0;                                                   //  reject too big tag
      add_tag(tag,tags_batchAddTags,tagMcc);                               //  add tag to tags_batchAddTags
      zdialog_stuff(zd,"batchAddTags",tags_batchAddTags);                  //  update dialog widgets
      zdialog_stuff(zd,"ctag","");
   }

   if (strEqu(event,"files"))                                              //  select images to add tags
   {
      if (flist) {                                                         //  free prior list
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
      }

      flist = image_gallery_getfiles(0,mWin);                              //  get file list from user
      batchAddTags_filelist = flist;

      if (flist)                                                           //  count files in list
         for (ii = 0; flist[ii]; ii++);
      else ii = 0;
      batchAddTags_filecount = ii;
      
      snprintf(countmess,50,ZTX("%d files selected"),batchAddTags_filecount);
      zdialog_stuff(zd,"labcount",countmess);
   }
   
   return 0;
}
   

/**************************************************************************/

//  menu function - delete or replace a tag for many files at once

char        **batchDelTag_filelist = 0;

void m_batchDelTag(GtkWidget *, cchar *)                                   //  new v.10.9
{
   int   batchDelTag_dialog_event(zdialog *zd, cchar *event);
   
   char           **flist, *file;
   char           deltag[tagcc], reptag[tagcc];
   int            ii, err;
   zdialog        *zd;

   zfuncs::F1_help_topic = "batch_delete_tag";
   
   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;                                              //  v.10.5

   zd = zdialog_new(ZTX("Batch Delete Tag"),mWin,Bproceed,Bcancel,null);

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab1","hb1",ZTX("tag to remove"),"space=10");
   zdialog_add_widget(zd,"entry","deltag","hb1",0,"scc=20");

   zdialog_add_widget(zd,"hbox","hb2","dialog",0,"space=5");
   zdialog_add_widget(zd,"label","lab2","hb2",ZTX("optional replacement"),"space=10");
   zdialog_add_widget(zd,"entry","reptag","hb2",0,"scc=20");

   zdialog_add_widget(zd,"hbox","hb3","dialog",0,"space=5");
   zdialog_add_widget(zd,"button","files","hb3",Bselectfiles,"space=10");
   zdialog_add_widget(zd,"label","labcount","hb3",ZTX("0 files selected"),"space=10");
   
   zdialog_add_widget(zd,"hbox","hb4","dialog",0,"space=5");
   zdialog_add_widget(zd,"check","allfiles","hb4",ZTX("search all files"),"space=10");

   batchDelTag_filelist = flist = 0;

   zdialog_help(zd,"batch_delete_tag");                                    //  zdialog help topic        v.11.08
   zdialog_run(zd,batchDelTag_dialog_event);                               //  run dialog
   zdialog_wait(zd);                                                       //  wait for completion

   if (zd->zstat != 1) goto cleanup;                                       //  canceled

   flist = batchDelTag_filelist;                                           //  get selected files
   if (! flist) goto cleanup;
   
   zdialog_fetch(zd,"deltag",deltag,tagcc);                                //  get tag to delete
   strTrim2(deltag);                                                       //  remove leading and trailing blanks
   strToLower(deltag);                                                     //  use lower case for matching
   if (! *deltag) goto cleanup;

   zdialog_fetch(zd,"reptag",reptag,tagcc);                                //  optional replacement tag
   strTrim2(reptag);

   write_popup_text("open","Deleting Tags",500,200,mWin);                  //  status monitor popup window  v.11.01

   for (ii = 0; flist[ii]; ii++)                                           //  loop all selected files
   {
      file = flist[ii];
      err = f_open(file,0);                                                //  display image
      if (err) continue;

      write_popup_text("write",file);                                      //  report progress
      zmainloop();

      load_fileinfo(file);                                                 //  load current file tags

      err = del_tag(deltag,tags_filetags);                                 //  remove tag, if present
      if (err) continue;                                                   //  not present

      if (*reptag) {                                                       //  add replacement tag, if defined
         err = add_tag(reptag,tags_filetags,tagFcc);
         if (err == 2) 
            zmessageACK(mWin,ZTX("%s \n too many tags"),file);
      }

      save_fileinfo(file);                                                 //  save tag changes
   }
   
   write_popup_text("write","COMPLETED");
   write_popup_text("close",0);
   
cleanup:

   zdialog_free(zd);

   if (flist) {
      for (ii = 0; flist[ii]; ii++) 
         zfree(flist[ii]);
      zfree(flist);
   }

   menulock(0);
   return;
}


//  batchDelTag dialog event function

int batchDelTag_dialog_event(zdialog *zd, cchar *event)
{
   FILE           *fidr;
   struct stat    statbuf;
   char           deltag[tagcc];
   char           **flist = batchDelTag_filelist;
   int            err, nfiles, ii, allfiles;
   char           filebuff[tagrecl], tagsbuff[tagrecl];
   char           *ppv, *file, *tags, countmess[50];
   cchar          *ppf;
   int            maxfiles = 100000;                                       //  max. files with tag to delete   v.11.04
   
   zdialog_fetch(zd,"deltag",deltag,tagcc);                                //  get tag to delete
   strTrim2(deltag);                                                       //  remove leading and trailing blanks
   strToLower(deltag);                                                     //  use lower case for matching

   if (zd->zstat == 1)                                                     //  [proceed]
   {
      if (! flist) {
         zmessageACK(mWin,ZTX("no files selected"));                       //  v.11.01
         zd->zstat = 0;                                                    //  keep dialog active
         goto finish;
      }
      
      if (! *deltag) {
         zmessageACK(mWin,ZTX("no tag specified"));                        //  v.11.01
         zd->zstat = 0;
         goto finish;
      }
      
      return 0;
   }

   if (strEqu(event,"deltag"))                                             //  tag changed
   {
      if (flist) {                                                         //  clear file list                 v.11.01
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
         flist = 0;
      }

      zdialog_stuff(zd,"allfiles",0);                                      //  reset "all files" checkbox      v.11.01
      goto finish;
   }

   if (strEqu(event,"files"))                                              //  select images to delete tags
   {
      if (! *deltag) {
         zmessageACK(mWin,ZTX("specify tag"));                             //  v.11.01
         goto finish;
      }

      if (flist) {                                                         //  free prior list
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
         flist = 0;
      }

      flist = image_gallery_getfiles(0,mWin);                              //  get file list from user
      zdialog_stuff(zd,"allfiles",0);                                      //  reset "all files" checkbox      v.11.01
      goto finish;
   }
   
   if (strEqu(event,"allfiles"))
   {
      if (flist) {                                                         //  clear file list
         for (ii = 0; flist[ii]; ii++) 
            zfree(flist[ii]);
         zfree(flist);
         flist = 0;
      }

      zdialog_fetch(zd,"allfiles",allfiles);                               //  get checkbox "all files"
      if (! allfiles) goto finish;                                         //  was unselected
      
      if (! *deltag) {
         zmessageACK(mWin,ZTX("specify tag"));                             //  v.11.01
         goto finish;
      }

      fidr = fopen(search_index_file,"r");                                 //  read search index file
      if (! fidr) goto finish;                                             //  no file

      flist = (char **) zmalloc(maxfiles*sizeof(char *),"DeleteTag");      //  list for files found
      nfiles = 0;                                                          //  count files found
      flist[0] = 0;

      while (true)
      {
         ppv = fgets_trim(filebuff,tagrecl,fidr,1);                        //  next record
         if (! ppv) break;                                                 //  EOF

         if (! strnEqu(ppv,"file: ",6)) continue;                          //  file: /dir.../filename.jpg

         file = ppv+6;
         err=stat(file,&statbuf);                                          //  check file exists
         if (err) continue;
         if (! S_ISREG(statbuf.st_mode)) continue;

         ppv = fgets_trim(tagsbuff,tagrecl,fidr);                          //  next record
         if (! ppv) break;
         if (! strnEqu(ppv,"date: ",6)) continue;                          //  date: yyyy:mm:dd 

         ppv = fgets_trim(tagsbuff,tagrecl,fidr);                          //  next record
         if (! ppv) break;
         if (! strnEqu(ppv,"tags: ",6)) continue;                          //  tags: xxxx, xxxxx, ...

         tags = ppv + 6;
         strToLower(tags);                                                 //  use lower case for matching
         
         for (ii = 1; ; ii++)                                              //  step thru file tags
         {
            ppf = strField(tags,tagdelims,ii);
            if (! ppf) break;
            if (*ppf == ' ') continue;
            if (strEqu(ppf,deltag)) break;                                 //  look for my tag
         }
         
         if (! ppf) continue;                                              //  tag not found
         
         flist[nfiles] = strdupz(file,0,"batch_del_tag");
         nfiles++;
         if (nfiles == maxfiles-1) break;
      }

      fclose(fidr);                                                        //  close search index file
      flist[nfiles] = 0;                                                   //  EOL marker
   }

finish:

   batchDelTag_filelist = flist;                                           //  file list to process

   if (flist)                                                              //  count files in list
      for (ii = 0; flist[ii]; ii++);
   else ii = 0;
   
   snprintf(countmess,50,ZTX("%d files selected"),ii);                     //  stuff file count into dialog
   zdialog_stuff(zd,"labcount",countmess);
   
   return 0;
}
   

/**************************************************************************
   functions to view and edit metadata: EXIF, IPTC, etc. 
***************************************************************************/

//  menu function and popup dialog to show EXIF/IPTC data
//  window is updated when navigating to another image

void m_info_view_short(GtkWidget *, cchar *menu)                           //  v.10.12
{
   zfuncs::F1_help_topic = "view_info";
   info_view(1);
   return;
}

void m_info_view_long(GtkWidget *, cchar *menu)                            //  v.10.12
{
   zfuncs::F1_help_topic = "view_info";
   info_view(2);
   return;
}

void info_view(int arg)
{
   int   info_view_dialog_event(zdialog *zd, cchar *event);

   static int     length = 1;
   char           *buff;
   int            contx = 0;
   GtkWidget      *widget;

   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }
   
   if (! curr_file) return;
   
   if (arg > 0) length = arg;                                              //  change short/long report mode

   if (! zdexifview)                                                       //  popup dialog if not already
   {
      zdexifview = zdialog_new(ZTX("View Info"),mWin,Bcancel,null);
      zdialog_add_widget(zdexifview,"scrwin","scroll","dialog",0,"expand");
      zdialog_add_widget(zdexifview,"edit","exifdata","scroll",0,"expand|wrap");
      zdialog_resize(zdexifview,500,400);
      zdialog_help(zdexifview,"view_info");                                //  zdialog help topic        v.11.08
      zdialog_run(zdexifview,info_view_dialog_event);
   }

   widget = zdialog_widget(zdexifview,"exifdata");                         //  v.10.12
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                    //  disable widget editing
   gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget),GTK_WRAP_NONE);       //  disable text wrap

   if (length == 1)                                                        //  short report
   {
      snprintf(command,ccc,"exiftool -common -%s -%s -%s -%s -%s -%s \"%s\" ",
         iptc_tags_key, iptc_rating_key, iptc_editlog_key, exif_comment_key, 
         iptc_caption_key, exif_focal_length_key, curr_file);
   }
   else 
      snprintf(command,ccc,"exiftool -e \"%s\" ",curr_file);               //  long, output everything
      
   widget = zdialog_widget(zdexifview,"exifdata");                         //  widget for output
   wclear(widget);

   while ((buff = command_output(contx,command))) {                        //  run command, output into window
      wprintf(widget,"%s\n",buff);
      zfree(buff);                                                         //  memory leak      v.11.03
   }

   command_status(contx);                                                  //  free resources

   return;
}


//  dialog event and completion callback function

int info_view_dialog_event(zdialog *zd, cchar *event)                      //  kill dialog
{
   if (! zd->zstat) return 0;
   zdialog_free(zdexifview);
   return 0;
}


/**************************************************************************/

//  edit EXIF/IPTC data - add or change specified EXIF/IPTC/etc. key

void m_info_edit(GtkWidget *, cchar *menu)                                 //  new v.10.2
{
   char        keyname[40], keydata[1000];
   cchar       *pp1[1];
   char        **pp2;

   int   info_edit_dialog_event(zdialog *zd, cchar *event);
   
   if (menu) zfuncs::F1_help_topic = "edit_info";

   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }
   
   if (! curr_file) return;

   if (! zdexifedit)                                                       //  popup dialog if not already   v.10.8
   {   
      zdexifedit = zdialog_new(ZTX("Edit Info"),mWin,Bfetch,Bsave,Bcancel,null);
      zdialog_add_widget(zdexifedit,"vbox","hb1","dialog");
      zdialog_add_widget(zdexifedit,"hbox","hbkey","dialog",0,"space=5");
      zdialog_add_widget(zdexifedit,"hbox","hbdata","dialog",0,"space=5");
      zdialog_add_widget(zdexifedit,"label","labkey","hbkey","key name");
      zdialog_add_widget(zdexifedit,"entry","keyname","hbkey",0,"scc=20");
      zdialog_add_widget(zdexifedit,"label","labdata","hbdata","key value");
      zdialog_add_widget(zdexifedit,"entry","keydata","hbdata",0,"expand");
      zdialog_help(zdexifedit,"edit_info");                                //  zdialog help topic        v.11.08
      zdialog_run(zdexifedit,info_edit_dialog_event);
   }

   zdialog_fetch(zdexifedit,"keyname",keyname,40);                         //  get key name from dialog
   strCompress(keyname);                                                   //  v.10.8

   if (*keyname)                                                           //  update live dialog   v.10.8
   {
      pp1[0] = keyname;                                                    //  look for key data 
      pp2 = info_get(curr_file,pp1,1);
      if (pp2[0]) {
         strncpy0(keydata,pp2[0],999);
         zfree(pp2[0]);
      }
      else *keydata = 0;
      zdialog_stuff(zdexifedit,"keydata",keydata);                         //  stuff into dialog
   }

   return;
}


//  dialog event and completion callback function

int  info_edit_dialog_event(zdialog *zd, cchar *event)
{
   char        keyname[40], keydata[1000];                                 //  v.11.02 (was 100)
   cchar       *pp1[1], *pp2[1];
   char        **pp3;
   int         err;
   
   if (! zd->zstat) return 0;
   
   zdialog_fetch(zd,"keyname",keyname,40);
   zdialog_fetch(zd,"keydata",keydata,1000);
   strCompress(keyname);                                                   //  remove blanks    v.10.8

   if (zd->zstat == 1)                                                     //  fetch
   {
      zd->zstat = 0;
      if (! *keyname) return 0;                                            //  bugfix   v.11.02
      pp1[0] = keyname;
      pp3 = info_get(curr_file,pp1,1);
      if (pp3[0]) {
         strncpy0(keydata,pp3[0],99);
         zfree(pp3[0]);
      }
      else *keydata = 0;
      zdialog_stuff(zd,"keydata",keydata);
   }

   else if (zd->zstat == 2)                                                //  save
   {
      zd->zstat = 0;                                                       //  keep dialog active
      if (! *keyname) return 0;                                            //  bugfix   v.11.02
      if (is_syncbusy()) return 0;                                         //  must wait for file sync      v.11.11
      pp1[0] = keyname;
      pp2[0] = keydata;
      err = info_put(curr_file,pp1,pp2,1);
      if (err) zmessageACK(mWin,"error: %s",strerror(err));
      if (zdexifview) info_view(0);                                        //  update exif view if active
   }

   else zdialog_free(zdexifedit);                                          //  other

   return 1;
}


/**************************************************************************/

//  delete EXIF/IPTC data, specific key or all data

void m_info_delete(GtkWidget *, cchar *menu)                               //  new v.10.2
{
   int   info_delete_dialog_event(zdialog *zd, cchar *event);
   
   zdialog     *zd;

   zfuncs::F1_help_topic = "delete_info";                                  //  v.10.8

   if (! Fexiftool) {                                                      //  exiftool is required
      zmessageACK(mWin,Bexiftoolmissing);
      return;
   }
   
   if (! curr_file) return;
   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11

   zd = zdialog_new(ZTX("Delete Info"),mWin,Bapply,Bcancel,null);
   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"radio","kall","hb1",ZTX("All"),"space=5");
   zdialog_add_widget(zd,"radio","key1","hb1",ZTX("One Key:"));
   zdialog_add_widget(zd,"entry","keyname","hb1",0,"scc=20");
   zdialog_stuff(zd,"key1",1);

   zdialog_help(zd,"delete_info");                                         //  zdialog help topic        v.11.08
   zdialog_run(zd,info_delete_dialog_event);
   return;
}


//  dialog event and completion callback function

int  info_delete_dialog_event(zdialog *zd, cchar *event)
{
   int         kall, key1, err;
   char        keyname[40];
   
   if (! zd->zstat) return 0;

   if (zd->zstat != 1) {                                                   //  canceled
      zdialog_free(zd);
      return 1;
   }
   
   zd->zstat = 0;                                                          //  dialog remains active

   zdialog_fetch(zd,"kall",kall);
   zdialog_fetch(zd,"key1",key1);
   zdialog_fetch(zd,"keyname",keyname,40);
   strCompress(keyname);                                                   //  v.10.8
                                                                           //  bugfix - quotes around file  v.10.3
   if (kall)
      snprintf(command,ccc,"exiftool -m -q -overwrite_original -all=  \"%s\"",curr_file);
   else if (key1)
      snprintf(command,ccc,"exiftool -m -q -overwrite_original -%s=  \"%s\"",keyname,curr_file);
   else return 1;
   
   err = system(command);
   if (err) zmessageACK(mWin,"%s",wstrerror(err));
   
   if (zdexifview) info_view(0);                                           //  update exif view if active

   return 1;
}


/**************************************************************************/

//  get EXIF/IPTC metadata for given image file and EXIF/IPTC key(s)
//  returns array of pointers to corresponding key values
//  if a key is missing, corresponding pointer is null
//  returned strings belong to caller, are subject for zfree()
//  up to 9 keynames may be requested per call
//  command: 
//       exiftool -keyname1 -keyname2 ... "file"
//  command output: 
//       keyname1: keyvalue1
//       keyname2: keyvalue2
//       ...
//  The CPU time for this call is about 0.1 seconds (BIG).

char ** info_get(cchar *file, cchar **keys, int nkeys)
{
   char           *pp;
   static char    *keyvals[10];
   int            contx = 0, err, ii;
   uint           cc;
   
   if (nkeys < 1 || nkeys > 9) zappcrash("info_get nkeys: %d",nkeys);
   
   cc = 10 * sizeof(char *);                                               //  v.12.01
   memset(keyvals,0,cc);

   strcpy(command,"exiftool -m -q -S -fast");
   
   for (ii = 0; ii < nkeys; ii++)                                          //  build command line
      strncatv(command,ccc," -",keys[ii],null);

   strncatv(command,ccc," \"",file,"\"",null);

   while (true)
   {
      pp = command_output(contx,command);
      if (! pp) break;

      for (ii = 0; ii < nkeys; ii++)
      {
         cc = strlen(keys[ii]);
         if (strncasecmp(pp,keys[ii],cc) == 0)                             //  ignore case
            if (strlen(pp) > cc+2) 
               keyvals[ii] = strdupz(pp+cc+2,0,"info_get");                //  check not empty
      }

      zfree(pp);
   }
   
   err = command_status(contx);
   if (err) printf(" exiftool: %s \n",wstrerror(err));

   return keyvals;
}


/**************************************************************************/

//  Get metadata for a list of image files and a list of EXIF/IPTC keys.
//  
//  Returns an array of pointers to corresponding key values.
//  If a key is missing, corresponding pointer is null.
//  Returned strings belong to caller, are subject for zfree().
//  
//  shell command: 
//       exiftool -keyname1 -keyname2 ... "file1" "file2" ...
//
//  shell command output: 
//       ======== file1
//       keyname1:   keyvalue1
//       keyname2:   keyvalue2
//       ...
//       ======== file2
//       keyname1:   keyvalue3
//       keyname2:   keyvalue4
//       ...
//
//  Returned list of strings (char **):
//       keyvalue1, keyvalue2, ... keyvalue3, keyvalue4, ... null
//
//  The CPU time for this call is about 0.1 + 0.01 seconds per file
//  (based on 3.3 GHz Intel Core i5 cpu).

char ** info_getN(cchar **files, int NF, cchar **keys, int NK)             //  v.12.01
{
   char     *ppk, *ppv, **keyvals;
   int      contx = 0, cc, ii, ff, kk;
   char     command[10000];
   
   cc = (1 + NF * NK) * sizeof(char *);
   keyvals = (char **) zmalloc(cc,"info_getN");
   memset(keyvals,0,cc);

   strcpy(command,"exiftool -m -s2 -fast2");                               //  build command line

   for (kk = 0; kk < NK; kk++)                 
      strncatv(command,9999," -",keys[kk],null);

   for (ff = 0; ff < NF; ff++)
      strncatv(command,9999," \"",files[ff],"\"",null);
      
   ff = NF;                                                                //  flag: no current file

   while ((ppk = command_output(contx,command)))
   {
      if (NF > 1)                                                          //  multiple files
      {
         if (strnEqu(ppk,"========",8))                                    //  look for start of next file
         {
            for (ff = 0; ff < NF; ff++)
               if (strstr(ppk+8,files[ff])) break;                         //  locate file in my list
            zfree(ppk);
            continue;
         }

         if (ff == NF) continue;                                           //  no matching file
      }

      else ff = 0;                                                         //  if 1 file, no "========" lines

      for (kk = 0; kk < NK; kk++)
      {                                                                    //  look for key name
         cc = strlen(keys[kk]);
         if (strncasecmp(ppk,keys[kk],cc)) continue;
         ppv = ppk + cc;                                                   //  name matches
         while (*ppv && *ppv == ' ') ppv++;                                //  followed by blanks
         if (strncmp(ppv,": ",2)) continue;                                //  followed by ": "
         ppv += 2;
         if (strlen(ppv) < 1) continue;                                    //  followed by key value
         if (strlen(ppv) > 999) ppv[999] = 0;                              //  limit length to 999
         ii = NK * ff + kk;                                                //  index for file and key
         keyvals[ii] = strdupz(ppv,0,"info_getN");
         break;
      }

      zfree(ppk);
   }
   
   return keyvals;
}


/**************************************************************************/

//  create or change EXIF/IPTC metadata for given image file and key(s)
//  up to 9 keys may be processed
//  command: 
//    exiftool -overwrite_original -keyname="keyvalue" ... "file"
//
//  NOTE: exiftool replaces \n (newline) in "keyvalue" with "." (period)

int info_put(cchar *file, cchar **keys, cchar **text, int nkeys)
{
   int      ii, err;
   char     *ptext[10];
   
   if (nkeys < 1 || nkeys > 9) zappcrash("info_put nkeys: %d",nkeys);
   
   for (ii = 0; ii < nkeys; ii++)
   {                                              
      if (! text[ii]) text[ii] = "";                                       //  if null pointer use empty string
      ptext[ii] = strdupz(text[ii],20,"info_put");
      if (strchr(text[ii],'"'))                                            //  replace imbedded " with \"
         repl_1str(text[ii],ptext[ii],"\"","\\\"");                        //    else exiftool command fails
   }

   strcpy(command,"exiftool -m -q -overwrite_original");
   
   for (ii = 0; ii < nkeys; ii++)                                          //  build command line
      strncatv(command,ccc," -",keys[ii],"=\"",ptext[ii],"\"",null);
   strncatv(command,ccc," \"",file,"\"",null);

   for (ii = 0; ii < nkeys; ii++)                                          //  plug small leak           v.12.01
       zfree((char *) ptext[ii]);

   err = system(command);
   if (err) printf(" info_put: %s \n",wstrerror(err));
   return err;
}


/**************************************************************************/

//  copy EXIF/IPTC data from original image file to new (edited) image file
//  if nkeys > 0, up to 9 keys may be replaced with new values
//  exiftool command revised: parameter sequence and -icc_profile

int info_copy(cchar *file1, cchar *file2, cchar **keys, cchar **text, int nkeys)
{
   int      ii, err;

   strcpy(command,"exiftool -m -q -tagsfromfile");                         //  exiftool -m -q -tagsfromfile "file1"
   strncatv(command,ccc," \"",file1,"\"",null);
   
   strncatv(command,ccc," -all -icc_profile",null);                        //  -all -icc_profile

   for (ii = 0; ii < nkeys; ii++)                                          //  -keyname="keyvalue" ...   (options)
      if (text[ii])
         strncatv(command,ccc," -",keys[ii],"=\"",text[ii],"\"",null);

   strncatv(command,ccc," \"",file2,"\""," -overwrite_original",null);     //  "file2" -overwrite_original
   
   err = system(command);
   if (err) printf(" exiftool: %s \n",wstrerror(err));
   return err;
}


/**************************************************************************/

//  convert between EXIF and fotoxx tag date formats
//  EXIF date: yyyy:mm:dd hh:mm:ss[.ss]
//  tag date: yyyymmdd
//  

void exif_tagdate(cchar *exifdate, char *tagdate)
{
   time_t      tnow;
   struct tm   *snow;
   
   if (! exifdate || strlen(exifdate) < 10) {                              //  bad EXIF date, use current date
      tnow = time(0);
      snow = localtime(&tnow);
      snprintf(tagdate,8,"%4d%02d%02d",
               snow->tm_year+1900, snow->tm_mon+1, snow->tm_mday);
      tagdate[8] = 0;
      return;
   }

   strncpy(tagdate,exifdate,4);                                            //  convert 
   strncpy(tagdate+4,exifdate+5,2);
   strncpy(tagdate+6,exifdate+8,2);
   tagdate[8] = 0;
   return;
}

void tag_exifdate(cchar *tagdate, char *exifdate)
{
   int         cc;
   time_t      tnow;
   struct tm   *snow;
   
   if (! tagdate || strlen(tagdate) < 4) {
      tnow = time(0);
      snow = localtime(&tnow);
      snprintf(exifdate,20,"%4d:%02d:%02d %02d:%02d:%02d",
               snow->tm_year+1900, snow->tm_mon+1, snow->tm_mday,
               snow->tm_hour, snow->tm_min, snow->tm_sec);
      exifdate[19] = 0;
      return;
   }

   strncpy(exifdate,tagdate,4);
   strcpy(exifdate+4,":01:01 00:00:00");
   cc = strlen(tagdate);
   if (cc >= 6) strncpy(exifdate+5,tagdate+4,2);
   if (cc >= 8) strncpy(exifdate+8,tagdate+6,2);
   exifdate[19] = 0;
   return;
}


/**************************************************************************/

//  search image tags, dates, stars, comments, captions for matching images

char     searchDateFrom[12] = "";                                          //  image search date range
char     searchDateTo[12] = "";
char     searchStarsFrom[4] = "";                                          //  image search stars range
char     searchStarsTo[4] = "";

zdialog  *zdsearchinfo = 0;                                                //  search tags dialog


void m_search_images(GtkWidget *, cchar *)
{
   void  searchinfo_fixwidget(zdialog *zd, cchar * widgetname);
   int   searchinfo_dialog_event(zdialog*, cchar *event);

   zdialog *zd;

   if (is_syncbusy()) return;                                              //  must wait for file sync      v.11.11
   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;
   menulock(0);

   zfuncs::F1_help_topic = "search_images";                                //  v.10.8

/***
         date range    [_______] [_______] (yyyymmdd)
         stars range   [__] [__]                  all any
         search tags   [________________________] (o) (o)
         search text   [________________________] (o) (o)
         file names    [________________________] (o) (o)

         defined tags
          ----------------------------------------------
         |                                              |
         |                                              |
         |                                              |
         |                                              |
         |                                              |
          ----------------------------------------------
                                     [proceed] [cancel]
***/

   zd = zdialog_new(ZTX("Search Tags, Comments, File Names"),mWin,Bproceed,Bcancel,null);
   zdsearchinfo = zd;

   zdialog_add_widget(zd,"hbox","hb1","dialog",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"homog|space=3");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"homog|space=3|expand");

   zdialog_add_widget(zd,"label","labD","vb1",ZTX("date range"));
   zdialog_add_widget(zd,"label","labS","vb1",ZTX("stars range"));
   zdialog_add_widget(zd,"label","labT","vb1",ZTX("search tags"));
   zdialog_add_widget(zd,"label","labT","vb1",ZTX("search text"));
   zdialog_add_widget(zd,"label","labF","vb1",ZTX("file names"));

   zdialog_add_widget(zd,"hbox","hbD","vb2",0,"space=1");
   zdialog_add_widget(zd,"entry","datefrom","hbD",0,"scc=10");
   zdialog_add_widget(zd,"entry","dateto","hbD",0,"scc=10");
   zdialog_add_widget(zd,"label","labD","hbD",ZTX("(yyyymmdd)"),"space=5");

   zdialog_add_widget(zd,"hbox","hbS","vb2",0,"space=1");
   zdialog_add_widget(zd,"entry","starsfrom","hbS",0,"scc=2");
   zdialog_add_widget(zd,"entry","starsto","hbS",0,"scc=2");
   zdialog_add_widget(zd,"label","space","hbS",0,"expand");
   zdialog_add_widget(zd,"label","all-any","hbS",ZTX("all/any"));

   zdialog_add_widget(zd,"hbox","hbT","vb2",0,"space=1");
   zdialog_add_widget(zd,"frame","frameT","hbT",0,"expand");
   zdialog_add_widget(zd,"edit","searchtags","frameT",0,"expand|wrap");
   zdialog_add_widget(zd,"radio","alltags","hbT",0);
   zdialog_add_widget(zd,"radio","anytags","hbT",0);

   zdialog_add_widget(zd,"hbox","hbC","vb2",0,"space=1|expand");
   zdialog_add_widget(zd,"entry","searchtext","hbC",0,"expand");
   zdialog_add_widget(zd,"radio","alltext","hbC",0);
   zdialog_add_widget(zd,"radio","anytext","hbC",0);

   zdialog_add_widget(zd,"hbox","hbF","vb2",0,"space=1|expand");
   zdialog_add_widget(zd,"entry","searchfiles","hbF",0,"expand");
   zdialog_add_widget(zd,"radio","allfiles","hbF",0);
   zdialog_add_widget(zd,"radio","anyfiles","hbF",0);
   
   zdialog_add_widget(zd,"hbox","hbA1","dialog",0,"space=3");
   zdialog_add_widget(zd,"label","labdeftags","hbA1",ZTX("defined tags"),"space=5");
   zdialog_add_widget(zd,"hbox","hbA2","dialog",0,"expand");
   zdialog_add_widget(zd,"frame","frameA","hbA2",0,"space=5|expand");
   zdialog_add_widget(zd,"scrwin","scrwinA","frameA",0,"expand");
   zdialog_add_widget(zd,"edit","deftags","scrwinA",0,"expand|wrap"); 

   zdialog_resize(zd,400,500);                                             //  start dialog
   zdialog_help(zd,"search_images");                                       //  zdialog help topic        v.11.08
   zdialog_run(zd,searchinfo_dialog_event);

   searchinfo_fixwidget(zd,"deftags");                                     //  setup tag selection via mouse
   searchinfo_fixwidget(zd,"searchtags");
   
   zdialog_stuff(zd,"datefrom",searchDateFrom);                            //  stuff previous date range
   zdialog_stuff(zd,"dateto",searchDateTo);
   zdialog_stuff(zd,"starsfrom",searchStarsFrom);
   zdialog_stuff(zd,"starsto",searchStarsTo);
   zdialog_stuff(zd,"searchtags",tags_searchtags);                         //  stuff previous search tags
   zdialog_stuff(zd,"searchtext",tags_searchtext);                         //  stuff previous search text
   zdialog_stuff(zd,"searchfiles",tags_searchfiles);                       //  stuff previous search files
   
   zdialog_stuff(zd,"alltags",0);                                          //  default any tags, text, files
   zdialog_stuff(zd,"anytags",1);
   zdialog_stuff(zd,"alltext",0);
   zdialog_stuff(zd,"anytext",1);
   zdialog_stuff(zd,"allfiles",0);
   zdialog_stuff(zd,"anyfiles",1);

   load_deftags();                                                         //  stuff defined tags into dialog
   deftags_stuff(zd);
   
   return;
}


//  setup tag display widget for tag selection using mouse clicks

void searchinfo_fixwidget(zdialog *zd, cchar * widgetname)
{
   void searchinfo_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname);

   GtkWidget         *widget;
   GdkWindow         *gdkwin;

   widget = zdialog_widget(zd,widgetname);                                 //  make widget wrap text
   gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),0);                    //  disable widget editing

   gdkwin = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),TEXTWIN);       //  cursor for tag selection
   gdk_window_set_cursor(gdkwin,arrowcursor);

   gtk_widget_add_events(widget,GDK_BUTTON_PRESS_MASK);                    //  connect mouse-click event
   G_SIGNAL(widget,"button-press-event",searchinfo_mouse,widgetname);
}


//  search tags mouse-click event function
//  get clicked tag and add to or remove from tags_searchtags

void searchinfo_mouse(GtkTextView *widget, GdkEventButton *event, cchar *widgetname)
{
   int      mpx, mpy, cc;
   
   if (event->type != GDK_BUTTON_PRESS) return;
   mpx = int(event->x);                                                    //  mouse click position
   mpy = int(event->y);

   cc = get_mouse_tag(widget,mpx,mpy,widgetname);                          //  tags_cliktag = clicked tag in list
   if (! cc) return;

   if (strEqu(widgetname,"deftags")) {
      add_tag(tags_cliktag,tags_searchtags,tagScc);                        //  add defined tag to search tags
      zdialog_stuff(zdsearchinfo,"searchtags",tags_searchtags);
   }

   if (strEqu(widgetname,"searchtags")) {                                  //  v.10.6
      del_tag(tags_cliktag,tags_searchtags);                               //  remove tag from searchtags
      zdialog_stuff(zdsearchinfo,"searchtags",tags_searchtags);            //  update dialog widgets
   }
   
   return;
}


//  dialog event and completion callback function

int searchinfo_dialog_event(zdialog *zd, cchar *event)
{
   int   searchinfo_text(char *text, int Fmall);

   char           indexbuff[tagrecl], resultsfile[200];
   char           temp[tagrecl], date1[12], date2[12];
   cchar          *pp, *pps, *ppf;
   int            err, iix, iis, iif, cc;
   int            Nxrec, nmatch, nfail;
   int            Fdates, Ftext, Ffiles, Ftags, Fstars;
   int            Falltags, Falltext, Fallfiles;
   FILE           *fid;
   struct stat    statbuf;

   struct xrec_t  {                                                        //  image metadata record
      char        *file;                                                   //  image filespec
      char        *tags;                                                   //  image tags
      char        *comms;                                                  //  image comments
      char        *capt;                                                   //  image caption
      char        date[12];                                                //  image date
      char        stars;                                                   //  image star rating, '0' to '5'
   };

   if (! zd->zstat) return 0;                                              //  wait for completion

   if (zd->zstat != 1) {
      zdialog_free(zd);                                                    //  cancel
      return 0;
   }

   zdialog_fetch(zd,"datefrom",searchDateFrom,10);                         //  get search date range
   zdialog_fetch(zd,"dateto",searchDateTo,10);
   zdialog_fetch(zd,"starsfrom",searchStarsFrom,2);                        //  get search stars range
   zdialog_fetch(zd,"starsto",searchStarsTo,2);
   zdialog_fetch(zd,"searchtags",tags_searchtags,tagScc);                  //  get search tags
   zdialog_fetch(zd,"searchtext",tags_searchtext,tagScc);                  //  get search text*
   zdialog_fetch(zd,"searchfiles",tags_searchfiles,tagScc);                //  get search /path*/file*

   zdialog_fetch(zd,"alltags",Falltags);                                   //  get match all/any options
   zdialog_fetch(zd,"alltext",Falltext);
   zdialog_fetch(zd,"allfiles",Fallfiles);

   zdialog_free(zd);                                                       //  cancel dialog

   strcpy(date1,"0000:01:01");                                             //  defaults for missing dates
   strcpy(date2,"9999:12:31");                                             //  v.10.12

   Fdates = 0;
   if (*searchDateFrom) {                                                  //  date from was given
      Fdates++;
      strncpy(date1,searchDateFrom,4);                                     //  convert format
      if (searchDateFrom[4] >= '0')                                        //    yyyymmdd >> yyyy:mm:dd
         strncpy(date1+5,searchDateFrom+4,2);
      if (searchDateFrom[6] >= '0')
         strncpy(date1+8,searchDateFrom+6,2);
   }
   if (*searchDateTo) {                                                    //  date to was given
      Fdates++;
      strncpy(date2,searchDateTo,4);
      if (searchDateTo[4] >= '0')
         strncpy(date2+5,searchDateTo+4,2);
      if (searchDateTo[6] >= '0')
         strncpy(date2+8,searchDateTo+6,2);
   }

   Fstars = 0;
   if (*searchStarsFrom || *searchStarsTo) Fstars = 1;                     //  stars was given
   
   Ffiles = 0;
   if (! blank_null(tags_searchfiles)) Ffiles = 1;                         //  search path / file (fragment) was given

   Ftext = 0;
   if (! blank_null(tags_searchtext)) Ftext = 1;                           //  search text was given

   Ftags = 0;
   if (! blank_null(tags_searchtags)) Ftags = 1;                           //  search tags was given

   if (Ffiles) strToLower(tags_searchfiles);                               //  all comparisons in lower case
   if (Ftags) strToLower(tags_searchtags);
   if (Ftext) strToLower(tags_searchtext);

   cc = MAXIMAGES * sizeof(xrec_t);
   xrec_t  *xrec = (xrec_t *) zmalloc(cc,"search");
   memset(xrec,0,cc);

   Nxrec = 0;
   fid = fopen(search_index_file,"r");
   if (! fid) goto noindexfile;
   *indexbuff = 0;

   while (true)                                                            //  read index file
   {
      if (! strnEqu(indexbuff,"file: ",6)) {                               //  skip to next "file" record
         pp = fgets_trim(indexbuff,tagrecl,fid);
         if (! pp) break;                                                  //  EOF
         continue;
      }

      *indexbuff = 0;                                                      //  prevent possible looping

      iix = Nxrec;
      if (xrec[iix].file) zfree(xrec[iix].file);                           //  xrecs are re-used
      if (xrec[iix].tags) zfree(xrec[iix].tags);
      if (xrec[iix].comms) zfree(xrec[iix].comms);
      if (xrec[iix].capt) zfree(xrec[iix].capt);

      memset(&xrec[iix],0,sizeof(xrec));
      pp = indexbuff + 6;                                                  //  filespec
      xrec[iix].file = strdupz(pp,0,"search");                             //  setup new xrec member
      xrec[iix].stars = '0';

      while (true)
      {
         pp = fgets_trim(indexbuff,tagrecl,fid);                           //  get recs following "file"
         if (! pp) break;

         if (strnEqu(pp,"date: ",6))
         {
            pp = strField(pp,' ',2);
            if (pp) strncpy0(xrec[iix].date,pp,12);
         }
         
         else if (strnEqu(pp,"tags: ",6))
            xrec[iix].tags = strdupz(pp+6,0,"search");
         
         else if (strnEqu(pp,"stars: ",7))
            xrec[iix].stars = *(pp+7);

         else if (strnEqu(pp,"comms: ",7))
            xrec[iix].comms = strdupz(pp+7,0,"search");

         else if (strnEqu(pp,"capt: ",6))
            xrec[iix].capt = strdupz(pp+6,0,"search");
         
         else break;
      }

      err=stat(xrec[iix].file,&statbuf);                                   //  check file exists
      if (err) continue;
      if (! S_ISREG(statbuf.st_mode)) continue;

      if (Ffiles)                                                          //  file name match is wanted
      {
         nmatch = nfail = 0;

         for (iis = 1; ; iis++)
         {
            pps = strField(tags_searchfiles,' ',iis);                      //  step thru search file names
            if (! pps) break;
            if (strcasestr(xrec[iix].file,pps)) nmatch++;
            else nfail++;
         }

         if (nmatch == 0) continue;                                        //  no match to any file
         if (Fallfiles && nfail) continue;                                 //  no match to all files 
      }

      if (Fdates)                                                          //  date match is wanted
      {
         if (strcmp(xrec[iix].date,date1) < 0) continue;
         if (strcmp(xrec[iix].date,date2) > 0) continue;
      }

      if (Ftags)                                                           //  tags match is wanted
      {
         strToLower(temp,xrec[iix].tags);
         nmatch = nfail = 0;

         for (iis = 1; ; iis++)                                            //  step thru search tags
         {
            pps = strField(tags_searchtags,tagdelims,iis);                 //  (delimited, wildcards)
            if (! pps) break;
            if (*pps == ' ') continue;

            for (iif = 1; ; iif++)                                         //  step thru file tags (delimited)
            {
               ppf = strField(temp,tagdelims,iif);
               if (! ppf) { nfail++; break; }                              //  count matches and fails
               if (*ppf == ' ') continue;
               if (MatchWild(pps,ppf) == 0) { nmatch++; break; }           //  wildcard match 
            }
         }

         if (nmatch == 0) continue;                                        //  no match to any tag
         if (Falltags && nfail) continue;                                  //  no match to all tags
      }

      if (Fstars)                                                          //  stars match is wanted
      {
         if (*searchStarsFrom && xrec[iix].stars < *searchStarsFrom) continue;
         if (*searchStarsTo && xrec[iix].stars > *searchStarsTo) continue;
      }

      if (Ftext)                                                           //  text match is wanted
      {
         nmatch = nfail = 0;

         for (iis = 1; ; iis++)                                            //  step through search words
         {
            pps = strField(tags_searchtext,' ',iis);
            if (! pps) break;
            if (*pps == ' ') continue;
            if (strcasestr(xrec[iix].capt,pps)) nmatch++;                  //  search captions for word
            else if (strcasestr(xrec[iix].comms,pps)) nmatch++;            //  search comments for word
            else nfail++;
         }
                     
         if (nmatch == 0) continue;                                        //  no match to any word
         if (Falltext && nfail) continue;                                  //  no match to all words
      }

      if (++Nxrec == MAXIMAGES) goto toomanyfiles;                         //  keep this record
   }

   fclose(fid);

   if (! Nxrec) goto nomatches;
   
   snprintf(resultsfile,199,"%s/search_results",get_zuserdir());
   fid = fopen(resultsfile,"w");                                           //  search results output file
   if (! fid) goto writerror;
   
   for (iix = 0; iix < Nxrec; iix++)                                       //  write matching filespecs
      fprintf(fid,"%s\n",xrec[iix].file);

   fclose(fid);

   image_gallery(resultsfile,"initF",0,m_gallery2,mWin);                   //  generate gallery of matching files
                                                                           //  sort removed                 v.12.01
   image_gallery(0,"paint1");                                              //  show new image gallery window
   goto freememory;
   
nomatches:
   zmessageACK(mWin,ZTX("No matching images found"));
   goto freememory;

noindexfile:
   zmessageACK(mWin,ZTX("No search index file present"));
   goto freememory;

writerror:
   zmessLogACK(mWin,"output file error: %s",strerror(errno));
   goto freememory;

toomanyfiles:
   zmessLogACK(mWin,"too many image files");
   goto freememory;

freememory:
   for (iix = 0; iix < Nxrec; iix++) {                                     //  free allocated memory
      if (xrec[iix].file) zfree(xrec[iix].file);
      if (xrec[iix].tags) zfree(xrec[iix].tags);
      if (xrec[iix].comms) zfree(xrec[iix].comms);
      if (xrec[iix].capt) zfree(xrec[iix].capt);
   }
   zfree(xrec);
   return 0;
}


/**************************************************************************/

//  report metadata for the current set of image files
//  (from a directory, a collection, or a search)

namespace search_metadata 
{
   char     *keys[5];                                                      //  search keys and match data
   char     *match[5];
   char     keyx[8], matchx[8];
}

void m_search_metadata(GtkWidget *, cchar *)                               //  new v.12.01
{
   using namespace search_metadata;

   int   search_metadata_dialog_event(zdialog*, cchar *event);

   cchar *infomess = "These items are always reported: \n"
                     "date, stars, tags, caption, comment";
   zdialog *zd;

   if (mod_keep()) return;                                                 //  unsaved edits
   if (! menulock(1)) return;
   menulock(0);

   zfuncs::F1_help_topic = "search_metadata";

/***
         Search Metadata (EXIF/IPTC etc.)

         These items from the current 
         image set are reported: date, 
         stars, tags, caption, comment

         Additional Items for Report
            Keyword          Match
         [__________] [_________________]
         [__________] [_________________]
         [__________] [_________________]
         [__________] [_________________]
         [__________] [_________________]

                       [proceed] [cancel]
***/

   zd = zdialog_new("Search Metadata (EXIF/IPTC etc.)",mWin,Bproceed,Bcancel,null);
   zdialog_add_widget(zd,"label","labinfo","dialog",infomess,"space=5");
   zdialog_add_widget(zd,"label","labopts","dialog",ZTX("Additional Items for Report"));

   zdialog_add_widget(zd,"hbox","hb1","dialog");
   zdialog_add_widget(zd,"vbox","vb1","hb1",0,"space=5");
   zdialog_add_widget(zd,"vbox","vb2","hb1",0,"space=5|expand");
   
   zdialog_add_widget(zd,"label","lab1","vb1","Keyword");
   zdialog_add_widget(zd,"entry","key0","vb1");
   zdialog_add_widget(zd,"entry","key1","vb1");
   zdialog_add_widget(zd,"entry","key2","vb1");
   zdialog_add_widget(zd,"entry","key3","vb1");
   zdialog_add_widget(zd,"entry","key4","vb1");

   zdialog_add_widget(zd,"label","lab2","vb2","Match");
   zdialog_add_widget(zd,"entry","match0","vb2",0,"expand");
   zdialog_add_widget(zd,"entry","match1","vb2",0,"expand");
   zdialog_add_widget(zd,"entry","match2","vb2",0,"expand");
   zdialog_add_widget(zd,"entry","match3","vb2",0,"expand");
   zdialog_add_widget(zd,"entry","match4","vb2",0,"expand");
   
   strcpy(keyx,"keyx");
   strcpy(matchx,"matchx");
   
   for (int ii = 0; ii < 5; ii++)                                          //  preload prior data if any
   {
      keyx[3] = '0' + ii;
      if (keys[ii]) zdialog_stuff(zd,keyx,keys[ii]);
      matchx[5] = '0' + ii;
      if (match[ii]) zdialog_stuff(zd,matchx,match[ii]);
   }

   zdialog_help(zd,"search_metadata");
   zdialog_resize(zd,400,300);
   zdialog_run(zd,search_metadata_dialog_event);
   return;
}


//  dialog event and completion callback function

struct xrec_t  {                                                           //  image metadata record
   char        *file;                                                      //  image filespec
   char        *tags;                                                      //  image tags
   char        *comms;                                                     //  image comments
   char        *capt;                                                      //  image caption
   char        date[12];                                                   //  image date
   char        stars;                                                      //  image star rating, '0' to '5'
};


int search_metadata_dialog_event(zdialog *zd, cchar *event)
{
   using namespace search_metadata;
   using namespace image_navi;

   int   metadata_xrec_compare(cchar *rec1, cchar *rec2);
   int   metadata_xrec_search(char *file, xrec_t *xrecs, int nrecs);

   char           indexbuff[tagrecl], *file, **kvals;
   char           mdtext1[2*tagrecl], mdtext2[5][200];
   int            err, ii, jj, iif, iix, nth, cc;
   int            Nxrec, Nkeys, Ncache, pcache, Fmeta;
   char           **ppcache = 0, *file2;
   cchar          *pp, *Fcache[50];
   FILE           *fid;
   struct stat    statbuf;

   if (! zd->zstat) return 0;                                              //  wait for completion

   if (zd->zstat != 1) {
      zdialog_free(zd);                                                    //  cancel
      return 0;
   }
   
   if (! keys[0])                                                          //  first call initialization
   {
      for (ii = 0; ii < 5; ii++) {
         keys[ii] = zmalloc(40,"search_metadata");
         match[ii] = zmalloc(100,"search_metadata");
         *keys[ii] = *match[ii] = 0;
      }
   }   

   Nkeys = 0;

   for (ii = jj = 0; ii < 5; ii++)                                         //  get optional metadata keys
   {
      keyx[3] = '0' + ii;
      zdialog_fetch(zd,keyx,keys[ii],40);
      strCompress(keys[ii]);                                               //  remove all blanks from key names
      if (*keys[ii] <= ' ') continue;
      memmove(keys[jj],keys[ii],40);                                       //  repack blank keys
      matchx[5] = '0' + ii;
      zdialog_fetch(zd,matchx,match[ii],100);                              //  get corresp. match value if any
      strTrim2(match[jj],match[ii]);                                       //  trim leading and trailing blanks
      if (ii > jj) *keys[ii] = *match[ii] = 0;
      jj++;
   }

   Nkeys = jj;                                                             //  keys found, no blanks
   
   zdialog_free(zd);                                                       //  cancel dialog

   cc = MAXIMAGES * sizeof(xrec_t);                                        //  allocate memory for index recs.
   xrec_t  *xrec = (xrec_t *) zmalloc(cc,"search_metadata");
   memset(xrec,0,cc);

   Nxrec = 0;
   fid = fopen(search_index_file,"r");
   if (! fid) goto noindexfile;
   *indexbuff = 0;

   while (true)                                                            //  read index file
   {
      if (! strnEqu(indexbuff,"file: ",6)) {                               //  skip to next "file" record
         pp = fgets_trim(indexbuff,tagrecl,fid);
         if (! pp) break;                                                  //  EOF
         continue;
      }

      *indexbuff = 0;                                                      //  prevent looping
      
      file = indexbuff + 6;
      err=stat(file,&statbuf);                                             //  check file exists
      if (err) continue;
      
      iix = Nxrec;
      memset(&xrec[iix],0,sizeof(xrec));

      xrec[iix].file = strdupz(file,0,"search_metadata");                  //  setup new xrec member
      xrec[iix].stars = '0';

      while (true)
      {
         pp = fgets_trim(indexbuff,tagrecl,fid);                           //  get recs following "file"
         if (! pp) break;

         if (strnEqu(pp,"date: ",6)) {
            pp = strField(pp,' ',2);
            if (pp) strncpy0(xrec[iix].date,pp,12);
         }
         
         else if (strnEqu(pp,"tags: ",6))
            xrec[iix].tags = strdupz(pp+6,0,"search_metadata");
         
         else if (strnEqu(pp,"stars: ",7))
            xrec[iix].stars = *(pp+7);

         else if (strnEqu(pp,"comms: ",7))
            xrec[iix].comms = strdupz(pp+7,0,"search_metadata");

         else if (strnEqu(pp,"capt: ",6))
            xrec[iix].capt = strdupz(pp+6,0,"search_metadata");
         
         else break;
      }

      if (++Nxrec == MAXIMAGES) goto toomanyfiles;
   }

   fclose(fid);

   if (! Nxrec) goto noindexfile;
   
   if (Nxrec > 1)                                                          //  sort index records
      HeapSort((char *) xrec, sizeof(xrec_t), Nxrec, metadata_xrec_compare);
   
   if (! mdlist) {
      cc = nfiles * sizeof(char *);                                        //  allocate metadata list
      mdlist = (char **) zmalloc(cc,"search_metadata"); 
      memset(mdlist,0,cc);
   }

   Ncache = 0;                                                             //  cache empty
   pcache = 0;

   if (gallerytype == 3) gallerytype = 2;                                  //  make gallery deletes work

   for (iif = 0; iif < nfiles; iif++)                                      //  loop image gallery files
   {
      if (mdlist[iif]) zfree(mdlist[iif]);                                 //  clear prior metadata
      mdlist[iif] = 0;

      file = image_gallery(0,"find",iif);                                  //  file gone?
      if (! file) continue;

      if (image_file_type(file) != 2) {                                    //  remove directory from gallery list
         nth = image_gallery_position(file,iif);
         image_gallery(0,"delete",nth);
         zfree(file);
         iif--;
         continue;
      }
      
      iix = metadata_xrec_search(file,xrec,Nxrec);                         //  get corresp. index record

      if (iix >= 0)                                                        //  get metadata from index rec.
      {
         cc = 2 * tagrecl;
         snprintf(mdtext1, cc," date: %s  stars: %c\n caption: %s\n comments: %s\n tags: %s", 
                     xrec[iix].date, xrec[iix].stars, xrec[iix].capt, xrec[iix].comms, xrec[iix].tags);
      }
      else *mdtext1 = 0;                                                   //  none avail.

      if (Nkeys)                                                           //  optional metadata requested
      {
         if (! Ncache)                                                     //  cache is empty
         {
            for (jj = iif; Ncache < 50 && jj < nfiles; jj++)
            {
               file2 = image_gallery(0,"find",jj);
               if (! file2) continue;
               if (image_file_type(file2) == 2) {                          //  put next 50 files into cache
                  Fcache[Ncache] = file2;
                  Ncache++;
               }
               else zfree(file2);
            }

            if (Ncache) {
               if (ppcache) zfree(ppcache);                                //  get metadata for cached files
               ppcache = info_getN(Fcache,Ncache,(cchar**) keys,Nkeys);
            }

            pcache = 0;                                                    //  first cache position
         }

         file2 = (char *) Fcache[pcache];                                  //  next cached file
         kvals = ppcache + Nkeys * pcache;                                 //  key values for cached file
         
         if (Ncache && strEqu(file,file2)) {                               //  cached metadata is for this file
            Fmeta = 1;
            Ncache--;                                                      //  index cache to next
            pcache++;
         }
         else Fmeta = 0;                                                   //  no metadata for this file
         
         for (jj = 0; jj < Nkeys; jj++) {                                  //  check key values for match
            if (*match[jj] > ' ') {                                        //  match value(s) are present
               if (! Fmeta) break;                                         //  metadata not available
               if (! kvals[jj]) break;                                     //  key not present in metadata
               for (nth = 1; ; nth++) {
                  pp = strField(match[jj],' ',nth);                        //  get each match value
                  if (! pp) break;                                         //  no more, no match found
                  if (strcasestr(kvals[jj],pp)) break;                     //  found match
               }
               if (! pp) break;                                            //  no match found
            }
         }

         if (jj < Nkeys) {                                                 //  at least one match fail
            nth = image_gallery_position(file,iif);                        //  remove file from gallery list
            image_gallery(0,"delete",nth);
            iif--;
            zfree(file2);                                                  //  free memory
            for (jj = 0; jj < Nkeys; jj++)
               if (kvals[jj]) zfree(kvals[jj]);
            continue;                                                      //  skip this file
         }
         else {                                                            //  all metadata matched
            for (jj = 0; jj < 5; jj++) 
               mdtext2[jj][0] = 0;
            for (jj = 0; jj < Nkeys; jj++)                                 //  add metadata to report
               snprintf(mdtext2[jj], 200, "\n key: %s  value: %s", keys[jj], kvals[jj]);
            strncatv(mdtext1, cc, mdtext2[0], mdtext2[1], mdtext2[2], mdtext2[3], mdtext2[4], null);
            zfree(file2);                                                  //  free memory
            for (jj = 0; jj < Nkeys; jj++)
               if (kvals[jj]) zfree(kvals[jj]);
         }                                                                 //  keep this file
      }

      mdlist[iif] = strdupz(mdtext1,0,"metadata report");                  //  attach metadata for gallery paint
      zfree(file);
   }

   if (galleryname) zfree(galleryname);
   galleryname = strdupz("metadata report");
   gallerytype = 3;                                                        //  outlaw gallery adds/deletes
   image_gallery(0,"paint1");                                              //  show new image gallery window
   goto freememory;
   
noindexfile:
   zmessageACK(mWin,ZTX("No search index file present"));
   goto freememory;

toomanyfiles:
   zmessLogACK(mWin,"too many image files");
   goto freememory;

freememory:
   if (ppcache) zfree(ppcache);
   for (iix = 0; iix < Nxrec; iix++) {                                     //  free allocated memory
      if (xrec[iix].file) zfree(xrec[iix].file);
      if (xrec[iix].tags) zfree(xrec[iix].tags);
      if (xrec[iix].comms) zfree(xrec[iix].comms);
      if (xrec[iix].capt) zfree(xrec[iix].capt);
   }
   zfree(xrec);

   return 0;
}


//  sort compare function - compare xrec files and return
//   returns <0  0  >0   for   file1  <  =  >  file2

int metadata_xrec_compare(cchar *rec1, cchar *rec2)
{
   char * file1 = ((xrec_t *) rec1)->file;
   char * file2 = ((xrec_t *) rec2)->file;
   return strcmp(file1,file2);
}


//  perform a binary search for a file within sorted xrecs in memory
//  return matching record number (0 based) or -1 if not found
//  execution time < 1 microsec.

int metadata_xrec_search(char *file, xrec_t *xrecs, int nrecs)
{
   int      ii, jj, kk, rkk;

   ii = nrecs / 2;                                                         //  next element to search
   jj = (ii + 1) / 2;                                                      //  next increment
   nrecs--;                                                                //  last element
   rkk = 0;

   while (true)
   {
      kk = strcmp(xrecs[ii].file,file);                                    //  compare member rec to seek rec

      if (kk > 0)
      {
         ii -= jj;                                                         //  too high, go down in set
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                         //  too low, go up in set
         if (ii > nrecs) return -1;
      }

      else if (kk == 0) return ii;                                         //  matched

      jj = jj / 2;                                                         //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                           //  step by 1 element
         if (! rkk) rkk = kk;                                              //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                        //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}



