/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998 Alexander Larsson
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_DIRENT_H
#include <dirent.h>
#endif
#include <glib.h>
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/xmlmemory.h>
#include "dia_xml_libxml.h"
#include <string.h>

#include "intl.h"

#include "sheet.h"
#include "message.h"
#include "object.h"
#include "dia_dirs.h"
#include "charconv.h"

static GSList *sheets = NULL;

Sheet *
new_sheet(char *name, utfchar *description, char *filename, SheetScope scope,
          Sheet *shadowing)
{
  Sheet *sheet;

  sheet = g_new(Sheet, 1);

#ifdef UNICODE_WORK_IN_PROGRESS
  sheet->name = g_strdup(name);
  sheet->description = g_strdup(description);
#else
  sheet->name = charconv_utf8_to_local8(name);
  sheet->description = charconv_utf8_to_local8(description);
#endif

  sheet->filename = filename;
  sheet->scope = scope;
  sheet->shadowing = shadowing;
  sheet->objects = NULL;
  return sheet;
}

void
sheet_prepend_sheet_obj(Sheet *sheet, SheetObject *obj)
{
  ObjectType *type;

  type = object_get_type(obj->object_type);
  if (type == NULL) {
    message_warning("Object '%s' needed in sheet '%s' was not found.\n"
		    "It will not be available for use.",
		    obj->object_type, sheet->name);
  } else {
    sheet->objects = g_slist_prepend( sheet->objects, (gpointer) obj);
  }
}

void
sheet_append_sheet_obj(Sheet *sheet, SheetObject *obj)
{
  ObjectType *type;

  type = object_get_type(obj->object_type);
  if (type == NULL) {
    message_warning("Object '%s' needed in sheet '%s' was not found.\n"
		    "It will not be availible for use.",
		    obj->object_type, sheet->name);
  } else {
    sheet->objects = g_slist_append( sheet->objects, (gpointer) obj);
  }
}

void
register_sheet(Sheet *sheet)
{
  sheets = g_slist_append(sheets, (gpointer) sheet);
}

GSList *
get_sheets_list(void)
{
  return sheets;
}

/* Sheet file management */

static void load_sheets_from_dir(const gchar *directory, SheetScope scope);
static void load_register_sheet(const gchar *directory,const gchar *filename,
                                SheetScope scope);

static gint
dia_sheet_sort_callback(gconstpointer a, gconstpointer b)
{
  return strcmp(((Sheet *)(a))->name, ((Sheet *)(b))->name);
}

void
dia_sort_sheets(void)
{
  sheets = g_slist_sort(sheets, dia_sheet_sort_callback);
}

void load_all_sheets(void) {
  char *sheet_path;
  char *home_dir;

  home_dir = dia_config_filename("sheets");
  if (home_dir) {
    load_sheets_from_dir(home_dir, SHEET_SCOPE_USER);
    g_free(home_dir);
  }

  sheet_path = getenv("DIA_SHEET_PATH");
  if (sheet_path) {
    char **dirs = g_strsplit(sheet_path,G_SEARCHPATH_SEPARATOR_S,0);
    int i;

    for (i=0; dirs[i] != NULL; i++) 
      load_sheets_from_dir(dirs[i], SHEET_SCOPE_SYSTEM);
    g_strfreev(dirs);
  } else {
    char *thedir = dia_get_data_directory("sheets");
    load_sheets_from_dir(thedir, SHEET_SCOPE_SYSTEM);
    g_free(thedir);
  }

  /* Sorting their sheets alphabetically makes user merging easier */

  dia_sort_sheets();
}

static void 
load_sheets_from_dir(const gchar *directory, SheetScope scope)
{
  DIR *dp;
  struct dirent *dirp;
  struct stat statbuf;
  gchar *p;

  dp = opendir(directory);
  if (!dp) return;

  while ( (dirp = readdir(dp)) ) {
    gchar *filename = g_strconcat(directory,G_DIR_SEPARATOR_S,
				  dirp->d_name,NULL);

    if (!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, "..")) {
      g_free(filename);
      continue;
    }
    /* filter out non-files */
    if (stat(filename, &statbuf) < 0) {
      g_free(filename);
      continue;
    }
    if (!S_ISREG(statbuf.st_mode)) {
      g_free(filename);
      continue;
    }

    /* take only .sheet files */
    p = filename + strlen(filename) - 6 /* strlen(".sheet") */;
    if (0!=strncmp(p,".sheet",6)) {
      g_free(filename);
      continue;
    }

    load_register_sheet(directory, filename, scope);
    g_free(filename);
				
  }

  closedir(dp);
}

static void 
load_register_sheet(const gchar *dirname, const gchar *filename,
                    SheetScope scope)
{
  xmlDocPtr doc;
  xmlNsPtr ns;
  xmlNodePtr node, contents,subnode,root;
  char *tmp;
  gchar *name = NULL, *description = NULL;
  int name_score = -1;
  int descr_score = -1;
  Sheet *sheet = NULL;
  GSList *sheetp;
  gboolean set_line_break = FALSE;
  gboolean name_is_gmalloced = FALSE;
  Sheet *shadowing = NULL;
  Sheet *shadowing_sheet = NULL;

  /* the XML fun begins here. */

  doc = xmlDoParseFile(filename);
  if (!doc) return;
  root = doc->xmlRootNode;
  while (root && (root->type != XML_ELEMENT_NODE)) root=root->next;
  if (!root) return;
  if (xmlIsBlankNode(root)) return;

  if (!(ns = xmlSearchNsByHref(doc,root,
	   "http://www.lysator.liu.se/~alla/dia/dia-sheet-ns"))) {
    g_warning("could not find sheet namespace");
    xmlFreeDoc(doc); 
    return;
  }
    if ((root->ns != ns) || (strcmp(root->name,"sheet"))) {
    g_warning("root element was %s -- expecting sheet", 
              doc->xmlRootNode->name);
    xmlFreeDoc(doc);
    return;
  }

  contents = NULL;
  for (node = root->xmlChildrenNode; node != NULL; node = node->next) {
    if (xmlIsBlankNode(node)) continue;
    if (node->type != XML_ELEMENT_NODE)
      continue;

    if (node->ns == ns && !strcmp(node->name, "name")) {
      gint score;
      
      /* compare the xml:lang property on this element to see if we get a
       * better language match.  LibXML seems to throw away attribute
       * namespaces, so we use "lang" instead of "xml:lang" */
      tmp = xmlGetProp(node, "xml:lang");
      if (!tmp) tmp = xmlGetProp(node, "lang");
      score = intl_score_locale(tmp);
      if (tmp) xmlFree(tmp);

      if (name_score < 0 || score < name_score) {
        name_score = score;
        if (name) xmlFree(name);
        name = xmlNodeGetContent(node);
      }      
    } else if (node->ns == ns && !strcmp(node->name, "description")) {
      gint score;

      /* compare the xml:lang property on this element to see if we get a
       * better language match.  LibXML seems to throw away attribute
       * namespaces, so we use "lang" instead of "xml:lang" */
      tmp = xmlGetProp(node, "xml:lang");
      if (!tmp) tmp = xmlGetProp(node, "lang");
      score = intl_score_locale(tmp);
      if (tmp) xmlFree(tmp);

      if (descr_score < 0 || score < descr_score) {
        descr_score = score;
        if (description) xmlFree(description);
        description = xmlNodeGetContent(node);
      }
      
    } else if (node->ns == ns && !strcmp(node->name, "contents")) {
      contents = node;
    }
  }

  if (!name || !contents) {
    g_warning("No <name> and/or <contents> in sheet %s--skipping", filename);
    xmlFreeDoc(doc);
    if (name) xmlFree(name);
    if (description) xmlFree(description);
    return;
  }

  /* Notify the user when we load a sheet that appears to be an updated
     version of a sheet loaded previously (i.e. from ~/.dia/sheets). */

  sheetp = get_sheets_list();
  while (sheetp)
  {
    if (sheetp->data && !strcmp(((Sheet *)(sheetp->data))->name, name)) 
    {
      struct stat first_file, this_file;
      int stat_ret;
      
      stat_ret = stat(((Sheet *)(sheetp->data))->filename, &first_file);
      g_assert(!stat_ret);

      stat_ret = stat(filename, &this_file);
      g_assert(!stat_ret);

      if (this_file.st_mtime > first_file.st_mtime)
      {
        gchar *tmp = g_strdup_printf("%s [Copy of system]", name);
        message_notice("The system sheet '%s' appears to be more recent"
                       " than your custom\n"
                       "version and has been loaded as '%s' for this session."
                       "\n\n"
                       "Move new objects (if any) from '%s' into your custom"
                       " sheet\n"
                       "or remove '%s', using the 'Sheets and Objects' dialog.",
                       name, tmp, tmp, tmp);
        xmlFree(name);
        name = tmp;
        name_is_gmalloced = TRUE;
        shadowing = sheetp->data;  /* This copy-of-system sheet shadows
                                      a user sheet */
      }
      else
      {
        /* The already-created user sheet shadows this sheet (which will be
           invisible), but we don't know this sheet's address yet */
        shadowing_sheet = sheetp->data;
      }
    }
    sheetp = g_slist_next(sheetp);
  }

  sheet = new_sheet(name, description, g_strdup(filename), scope, shadowing);

  if (shadowing_sheet)
    shadowing_sheet->shadowing = sheet;                   /* Hilarious :-) */

  if (name_is_gmalloced == TRUE)
    g_free(name);
  else
    xmlFree(name);
  xmlFree(description);

  for (node = contents->xmlChildrenNode ; node != NULL; node = node->next) {
    SheetObject *sheet_obj;
    ObjectType *otype;
    gchar *iconname = NULL;

    int subdesc_score = -1;
    gchar *objdesc = NULL;

    gint intdata = 0;
    gchar *chardata = NULL;

    gboolean has_intdata = FALSE;
    gboolean has_icon_on_sheet = FALSE;

    if (xmlIsBlankNode(node)) continue;

    if (node->type != XML_ELEMENT_NODE) 
      continue;
    if (node->ns != ns) continue;
    if (!strcmp(node->name,"object")) {
      /* nothing */
    } else if (!strcmp(node->name,"shape")) {
      g_message("%s: you should use object tags rather than shape tags now",
		filename);
    } else if (!strcmp(node->name,"br")) {
      /* Line break tag. */
      set_line_break = TRUE;
      continue;
    } else
      continue; /* unknown tag */
    
    tmp = xmlGetProp(node,"intdata");
    if (tmp) { 
      char *p;
      intdata = (gint)strtol(tmp,&p,0);
      if (*p != 0) intdata = 0;
      xmlFree(tmp);
      has_intdata = TRUE;
    }
    chardata = xmlGetProp(node,"chardata");
    /* TODO.... */
    if (chardata) xmlFree(chardata);
    
    for (subnode = node->xmlChildrenNode; 
         subnode != NULL ; 
         subnode = subnode->next) {
      if (xmlIsBlankNode(subnode)) continue;

      if (subnode->ns == ns && !strcmp(subnode->name, "description")) {
	gint score;

	/* compare the xml:lang property on this element to see if we get a
	 * better language match.  LibXML seems to throw away attribute
	 * namespaces, so we use "lang" instead of "xml:lang" */
	  
	tmp = xmlGetProp(subnode, "xml:lang");
	if (!tmp) tmp = xmlGetProp(subnode, "lang");
	score = intl_score_locale(tmp);
	if (tmp) xmlFree(tmp);

	if (subdesc_score < 0 || score < subdesc_score) {
	  subdesc_score = score;
	  if (objdesc) xmlFree(objdesc);
	  objdesc = xmlNodeGetContent(subnode);
	}
	  
      } else if (subnode->ns == ns && !strcmp(subnode->name,"icon")) {
	tmp = xmlNodeGetContent(subnode);
	iconname = g_strconcat(dirname,G_DIR_SEPARATOR_S,tmp,NULL);
	has_icon_on_sheet = TRUE;
	if (tmp) xmlFree(tmp);
      }
    }

    tmp = xmlGetProp(node,"name");

    sheet_obj = g_new(SheetObject,1);
    sheet_obj->object_type = g_strdup(tmp);
#ifdef UNICODE_WORK_IN_PROGRESS   
    sheet_obj->description = g_strdup(objdesc);
#else
    sheet_obj->description = charconv_utf8_to_local8(objdesc);
#endif
    xmlFree(objdesc); objdesc = NULL;

    sheet_obj->pixmap = NULL;
    sheet_obj->user_data = (void *)intdata; /* XXX modify user_data type ? */
    sheet_obj->user_data_type = has_intdata ? USER_DATA_IS_INTDATA /* sure,   */
                                            : USER_DATA_IS_OTHER;  /* why not */
    sheet_obj->pixmap_file = iconname; 
    sheet_obj->has_icon_on_sheet = has_icon_on_sheet;
    sheet_obj->line_break = set_line_break;
    set_line_break = FALSE;

    if ((otype = object_get_type(tmp)) == NULL) {
      g_warning("object_get_type(%s) returned NULL", tmp);
      if (sheet_obj->description) g_free(sheet_obj->description);
      g_free(sheet_obj->pixmap_file);
      g_free(sheet_obj->object_type);
      g_free(sheet_obj);
      if (tmp) xmlFree(tmp);
      continue; 
    }	  
    
    /* set defaults */
    if (sheet_obj->pixmap_file == NULL) {
      g_assert(otype->pixmap || otype->pixmap_file);
      sheet_obj->pixmap = otype->pixmap;
      sheet_obj->pixmap_file = otype->pixmap_file;
      sheet_obj->has_icon_on_sheet = has_icon_on_sheet;
    }
    if (sheet_obj->user_data == NULL
        && sheet_obj->user_data_type != USER_DATA_IS_INTDATA)
      sheet_obj->user_data = otype->default_user_data;
    else
      sheet_obj->user_data_type = USER_DATA_IS_INTDATA;

    if (tmp) xmlFree(tmp);
      
    /* we don't need to fix up the icon and descriptions for simple objects,
       since they don't have their own description, and their icon is 
       already automatically handled. */
    sheet_append_sheet_obj(sheet,sheet_obj);
  }

  if (!shadowing_sheet)
    register_sheet(sheet); 

  xmlFreeDoc(doc);
}
