/*
 * This file is part of the Poliqarp suite.
 * 
 * Copyright (C) 2009, 2010 by Instytut Podstaw Informatyki Polskiej
 * Akademii Nauk (IPI PAN; Institute of Computer Science, Polish
 * Academy of Sciences; cf. www.ipipan.waw.pl).  All rights reserved.
 * 
 * This file may be distributed and/or modified under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation and appearing in the file gpl.txt included in the packaging
 * of this file.  (See http://www.gnu.org/licenses/translations.html for
 * unofficial translations.)
 * 
 * A commercial license is available from IPI PAN (contact
 * Michal.Ciesiolka@ipipan.waw.pl or ipi@ipipan.waw.pl for more
 * information).  Licensees holding a valid commercial license from IPI
 * PAN may use this file in accordance with that license.
 * 
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE.
 */

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

#include <foostring/foostring.h>

#include <sakura/common/args.h>
#include <sakura/common/getline.h>
#include <sakura/config.h>

#include <bp/common.h>
#include <bp/ng/configuration.h>
#include <bp/ng/xmlns-item.h>
#include <bp/ng/meta-key.h>
#include <bp/ng/xml-nkjp.h>
#include <bp/ng/xpath.h>

static const size_t meta_key_names_table_size = 32;

static char *mangle_locale_name(const char *s)
{
#ifdef _WIN32
   /* For user convenience, we translate the usual locale name forms [1] to
    * the Windows-specific ones [2] [3].
    *
    * [1] http://www.gnu.org/software/hello/manual/gettext/Locale-Names.html
    * [2] http://msdn.microsoft.com/en-us/library/39cwe7zf.aspx
    * [3] http://msdn.microsoft.com/en-us/library/cdax410z.aspx
    */

   static const char* language_table[][2] = {
      {"zh", "chinese"},
      {"cs", "czech"},
      {"da", "danish"},
      {"nl", "dutch"},
      {"en", "english"},
      {"fi", "finnish"},
      {"fr", "french"},
      {"de", "german"},
      {"el", "greek"},
      {"hu", "hungarian"},
      {"is", "icelandic"},
      {"it", "italian"},
      {"ja", "japanese"},
      {"ko", "korean"},
      {"no", "norwegian"},
      {"nb", "norwegian-bokmal"},
      {"nn", "norwegian-nynorsk"},
      {"pl", "polish"},
      {"pt", "portuguese"},
      {"ru", "russian"},
      {"sk", "slovak"},
      {"es", "spanish"},
      {"sv", "swedish"},
      {"tr", "turkish"},
      {NULL, NULL}
   };

   static const char* region_table[][2] = {
      {"AU", "australia"},
      {"AT", "austria"},
      {"BE", "belgium"},
      {"BR", "brazil"},
      {"CA", "canada"},
      {"CN", "china"},
      {"CZ", "czech"},
      {"DK", "denmark"},
      {"FI", "finland"},
      {"FR", "france"},
      {"DE", "germany"},
      {"GR", "greece"},
      {"HK", "hong-kong"},
      {"HU", "hungary"},
      {"IS", "iceland"},
      {"IE", "ireland"},
      {"IT", "italy"},
      {"JP", "japan"},
      {"KR", "korea",},
      {"KP", "korea",},
      {"MX", "mexico"},
      {"NL", "netherlands"},
      {"NZ", "new zealand"},
      {"NO", "norway"},
      {"PL", "poland"},
      {"PT", "portugal"},
      {"RU", "russia"},
      {"SG", "singapore"},
      {"SK", "slovak"},
      {"ES", "spain"},
      {"SE", "sweden"},
      {"CH", "switzerland"},
      {"TW", "taiwan"},
      {"TR", "turkey"},
      {"GB", "united kingdom"},
      {"US", "united states"},
      {NULL, NULL}
   };

   const char *start = s;
   const char *end = s;

   string_t new_locale = string_create();
   while (ascii_isalnum(*end))
      end++;
   if (end - start == 2) {
      const char *(*language)[2] = language_table;
      while ((*language)[0] != NULL) {
         if (strncasecmp((*language)[0], start, 2) == 0) {
            string_append_str(new_locale, (*language)[1]);
            break;
         }
         language++;
      }
   }
   else
      string_append_buf(new_locale, start, end - start);
   start = end;
   while (*start != '\0' && !ascii_isalnum(*start)) {
      string_append_char(new_locale, *start);
      start++;
   }
   if (*start != '\0') {
      end = start;
      while (ascii_isalnum(*end))
         end++;
      if (end - start == 2) {
         const char *(*region)[2] = region_table;
         while ((*region)[0] != NULL) {
            if (strncasecmp((*region)[0], start, 2) == 0) {
               string_append_str(new_locale, (*region)[1]);
               break;
            }
            region++;
         }
      }
   }
   return string_free_and_get_buffer(new_locale);
#else
   return bp_strdup(s);
#endif
}

static bool parse_true_false(const char *s)
{
   if (strcmp(s, "true") == 0 || strcmp(s, "yes") == 0)
      return true;
   else if (strcmp(s, "false") == 0 || strcmp(s, "no") == 0)
      return false;
   else {
      bp_warning("unknown boolean value: %s; assuming false", s);
      return false;
   }
}

static void add_file_name(struct list *list, const char *name)
{
   char *dup_name;
   dup_name = string_aformat("%s.gz", name);
   list_append(list, dup_name);
   dup_name = bp_strdup(name);
   list_append(list, dup_name);
}

static void add_file_names(struct configuration *configuration, const char *name, const char *value)
{
   int rc;
   size_t i;
   struct text_args args;
   struct list *list;
   if (strcmp(name, "morphosyntax") == 0)
      list = &configuration->morph_names;
   else if (strcmp(name, "header") == 0)
      list = &configuration->header_names;
   else {
      bp_warning("unknown path property %s", name);
      return;
   }
   list_clear(list, free);
   args_init(&args);
   rc = args_parse(&args, value);
   if (rc < 0)
      bp_system_error(NULL);
   for (i = 0; i < args.num_items; i++)
      add_file_name(list, args.item[i].value);
   args_free(&args);
}

static void meta_key_add_values(struct meta_key *this, const char *values)
{
   int rc;
   struct text_args args;
   args_init(&args);
   rc = args_parse(&args, values);
   if (rc < 0)
      bp_system_error("memory allocation error");
   size_t i;
   for (i = 0; i < args.num_items; i++)
      meta_key_add_value(this, args.item[i].value);
   args_free(&args);
}

static void meta_key_property_set(struct meta_key *this,
   struct hash_table *meta_key_names,
   const char *name, const char *value)
{
   if (strcmp(name, "name") == 0) {
      const intptr_t duplicate = (intptr_t) hash_table_get(meta_key_names, value);
      if (duplicate)
         bp_warning("duplicate meta key \"%s\"", meta_key_get_name(this), value);
      else {
         char *dup_value = bp_strdup(value);
         list_append(&this->names, dup_value);
         hash_table_set(meta_key_names, value, (void*) 1);
      }
   }
   else if (strcmp(name, "path") == 0)
      list_append(&this->paths, bp_strdup(value));
   else if (strcmp(name, "type") == 0) {
      if (strcmp(value, "string") == 0)
         this->type = META_TYPE_STRING;
      else if (strcmp(value, "enum") == 0)
         this->type = META_TYPE_ENUM;
      else if (strcmp(value, "date") == 0)
         this->type = META_TYPE_DATE;
      else {
         bp_warning("unknown meta type %s, falling back to string", value);
         this->type = META_TYPE_STRING;
      }
   }
   else if (strcmp(name, "multiple") == 0)
      this->multiple = parse_true_false(value);
   else if (strcmp(name, "required") == 0)
      this->required = parse_true_false(value);
   else if (strcmp(name, "values") == 0)
      meta_key_add_values(this, value);
   else
      bp_warning("unknown property: %s", name);
}

typedef int configuration_handler(void *conf_, int section, char *text,
   struct poliqarp_error *error);


void configuration_create(struct configuration *this, const char *base_name)
{
   enum section {
      SECTION_META,
      SECTION_LOCALE,
      SECTION_FILENAMES,
      SECTION_XMLNS,
      SECTION_NONE /* needs to be the last */
   } section = SECTION_NONE;
  
   static const char * const section_names[SECTION_NONE + 1] = {
      [SECTION_META] = "meta",
      [SECTION_LOCALE] = "locale",
      [SECTION_FILENAMES] = "filenames",
      [SECTION_XMLNS] = "xmlns",
      [SECTION_NONE] = NULL,
   };

   this->morph_names = list_empty;
   list_append(&this->morph_names, bp_strdup("ann_morphosyntax.xml"));
   list_append(&this->morph_names, bp_strdup("morph.xml"));
   this->header_names = list_empty;
   list_append(&this->header_names, bp_strdup("header.xml"));
   this->meta_keys = list_empty;
   this->xmlns = list_empty;
   list_append(&this->xmlns, xmlns_item_new("tei", xml_ns_tei));
   list_append(&this->xmlns, xmlns_item_new("nkjp", xml_ns_nkjp));
   list_append(&this->xmlns, xmlns_item_new("poliqarp", xml_ns_poliqarp));
   this->locale = NULL;

   int rc;
   struct hash_table meta_key_names;
   rc = create_hash_table(&meta_key_names, meta_key_names_table_size,
      0, NULL);
   if (rc != 0)
      bp_system_error("memory allocation error");

   char *path = string_aformat("%s.bp.conf", base_name);
   FILE *fp = fopen(path, "rt");
   if (fp == NULL)
      bp_system_error(path);

   struct meta_key *current_meta_key = NULL;
   char *line = NULL, *line_ptr;
   size_t line_length = 0;
   unsigned int line_no = 0;
   while (getline(&line, &line_length, fp) > 0) {
      line_no++;
      line_ptr = line;
      while (*line_ptr != '\0' && ascii_isspace(*line_ptr))
         line_ptr++;
      if (*line_ptr == '\0')
         continue;
      if (line[0] == '#')
         continue;
      if (line[0] == '[') {
         const char *name = line + 1;
         char *end_name = strrchr(line, ']');
         if (end_name == NULL)
            goto malformed_line;
         *end_name = '\0';
         for (section = 0;
            section != SECTION_NONE &&
            strcasecmp(section_names[section], name) != 0;
            section++);
         switch (section) {
            case SECTION_META:
               if (meta_key_validate(current_meta_key) != 0) {
                  bp_fatal("invalid metadata specification");
               }
               current_meta_key = bp_malloc(sizeof *current_meta_key);
               meta_key_create(current_meta_key);
               current_meta_key->xmlns = &this->xmlns;
               list_append(&this->meta_keys, current_meta_key);
               break;
            case SECTION_NONE:
               bp_warning("unknown section %s", name);
               break;
            default:
               break;
         }
      } else {
         struct poliqarp_variable var;
         char *line_ptr = line;
         int rc = poliqarp_parse_variable(&var, line_ptr);
         if (rc != 0) {
            if (errno == EINVAL)
               goto malformed_line;
            else
               bp_system_error(path);
         }
         switch (section) {
            case SECTION_META:
               meta_key_property_set(current_meta_key, &meta_key_names, var.name, var.value);
               break;
            case SECTION_FILENAMES:
               add_file_names(this, var.name, var.value);
               break;
            case SECTION_XMLNS:
               list_append(&this->xmlns, xmlns_item_new(var.name, var.value));
               break;
            case SECTION_LOCALE:
               if (strcmp(var.name, "locale") == 0)
                  this->locale = mangle_locale_name(var.value);
               else
                  bp_warning("unknown locale property %s", var.name);
               break;
            case SECTION_NONE:
               bp_warning("%s: line %u ignored", path, line_no);
         }
         poliqarp_free_variable(&var);
      }
   }
   if (current_meta_key != NULL)
      meta_key_validate(current_meta_key);
   if (this->locale == NULL)
      bp_fatal("no locale was specified");
   free(line);
   if (!feof(fp))
      bp_system_error("read error");
   fclose(fp);
   free(path);
   destroy_hash_table(&meta_key_names, NULL);
   return;
malformed_line:
   bp_fatal("%s: malformed line %d", path, line_no);
}

void configuration_destroy(struct configuration *this)
{
   list_destroy(&this->morph_names, free);
   list_destroy(&this->header_names, free);
   list_destroy(&this->meta_keys, meta_key_free);
   list_destroy(&this->xmlns, xmlns_item_free);
   free(this->locale);
   this->locale = NULL;
}

void configuration_commit(const struct configuration *conf,
   struct corpus *corpus)
{
   size_t i;
   for (i = 0; i < conf->meta_keys.length; i++)
      corpus_add_meta_key(corpus, conf->meta_keys.items[i]);
   corpus_commit_meta_keys(corpus);
}
