/*
 * XML DRI client-side driver configuration
 * Copyright (C) 2003 Felix Kuehling
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * FELIX KUEHLING, OR ANY OTHER CONTRIBUTORS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */
/**
 * \file xmlconfig.c
 * \brief Driver-independent client-side part of the XML configuration
 * \author Felix Kuehling
 */

#include "xmlconfig.h"
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#if WITH_XMLCONFIG
#include <expat.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#endif
#ifdef NO_REGEX
typedef int regex_t;
#define REG_EXTENDED 0
#define REG_NOSUB 0
#define REG_NOMATCH 1
static inline int regcomp(regex_t *r, const char *s, int f) { return 0; }
static inline int regexec(regex_t *r, const char *s, int n, void *p, int f) { return REG_NOMATCH; }
static inline void regfree(regex_t* r) {}
#else
#include <regex.h>
#endif
#include <fcntl.h>
#include <math.h>
#include "strndup.h"
#include "u_process.h"
#include "os_file.h"

/* For systems like Hurd */
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif

static bool
be_verbose(void)
{
   const char *s = getenv("MESA_DEBUG");
   if (!s)
      return true;

   return strstr(s, "silent") == NULL;
}

/** \brief Locale-independent integer parser.
 *
 * Works similar to strtol. Leading space is NOT skipped. The input
 * number may have an optional sign. Radix is specified by base. If
 * base is 0 then decimal is assumed unless the input number is
 * prefixed by 0x or 0X for hexadecimal or 0 for octal. After
 * returning tail points to the first character that is not part of
 * the integer number. If no number was found then tail points to the
 * start of the input string. */
static int
strToI(const char *string, const char **tail, int base)
{
   int radix = base == 0 ? 10 : base;
   int result = 0;
   int sign = 1;
   bool numberFound = false;
   const char *start = string;

   assert(radix >= 2 && radix <= 36);

   if (*string == '-') {
      sign = -1;
      string++;
   } else if (*string == '+')
      string++;
   if (base == 0 && *string == '0') {
      numberFound = true;
      if (*(string+1) == 'x' || *(string+1) == 'X') {
         radix = 16;
         string += 2;
      } else {
         radix = 8;
         string++;
      }
   }
   do {
      int digit = -1;
      if (radix <= 10) {
         if (*string >= '0' && *string < '0' + radix)
            digit = *string - '0';
      } else {
         if (*string >= '0' && *string <= '9')
            digit = *string - '0';
         else if (*string >= 'a' && *string < 'a' + radix - 10)
            digit = *string - 'a' + 10;
         else if (*string >= 'A' && *string < 'A' + radix - 10)
            digit = *string - 'A' + 10;
      }
      if (digit != -1) {
         numberFound = true;
         result = radix*result + digit;
         string++;
      } else
         break;
   } while (true);
   *tail = numberFound ? string : start;
   return sign * result;
}

/** \brief Locale-independent floating-point parser.
 *
 * Works similar to strtod. Leading space is NOT skipped. The input
 * number may have an optional sign. '.' is interpreted as decimal
 * point and may occur at most once. Optionally the number may end in
 * [eE]<exponent>, where <exponent> is an integer as recognized by
 * strToI. In that case the result is number * 10^exponent. After
 * returning tail points to the first character that is not part of
 * the floating point number. If no number was found then tail points
 * to the start of the input string.
 *
 * Uses two passes for maximum accuracy. */
static float
strToF(const char *string, const char **tail)
{
   int nDigits = 0, pointPos, exponent;
   float sign = 1.0f, result = 0.0f, scale;
   const char *start = string, *numStart;

   /* sign */
   if (*string == '-') {
      sign = -1.0f;
      string++;
   } else if (*string == '+')
      string++;

   /* first pass: determine position of decimal point, number of
    * digits, exponent and the end of the number. */
   numStart = string;
   while (*string >= '0' && *string <= '9') {
      string++;
      nDigits++;
   }
   pointPos = nDigits;
   if (*string == '.') {
      string++;
      while (*string >= '0' && *string <= '9') {
         string++;
         nDigits++;
      }
   }
   if (nDigits == 0) {
      /* no digits, no number */
      *tail = start;
      return 0.0f;
   }
   *tail = string;
   if (*string == 'e' || *string == 'E') {
      const char *expTail;
      exponent = strToI(string+1, &expTail, 10);
      if (expTail == string+1)
         exponent = 0;
      else
         *tail = expTail;
   } else
      exponent = 0;
   string = numStart;

   /* scale of the first digit */
   scale = sign * (float)pow(10.0, (double)(pointPos-1 + exponent));

   /* second pass: parse digits */
   do {
      if (*string != '.') {
         assert(*string >= '0' && *string <= '9');
         result += scale * (float)(*string - '0');
         scale *= 0.1f;
         nDigits--;
      }
      string++;
   } while (nDigits > 0);

   return result;
}

/** \brief Parse a value of a given type. */
static unsigned char
parseValue(driOptionValue *v, driOptionType type, const char *string)
{
   const char *tail = NULL;
   /* skip leading white-space */
   string += strspn(string, " \f\n\r\t\v");
   switch (type) {
   case DRI_BOOL:
      if (!strcmp(string, "false")) {
         v->_bool = false;
         tail = string + 5;
      } else if (!strcmp(string, "true")) {
         v->_bool = true;
         tail = string + 4;
      }
      else
         return false;
      break;
   case DRI_ENUM: /* enum is just a special integer */
   case DRI_INT:
      v->_int = strToI(string, &tail, 0);
      break;
   case DRI_FLOAT:
      v->_float = strToF(string, &tail);
      break;
   case DRI_STRING:
      free(v->_string);
      v->_string = strndup(string, STRING_CONF_MAXLEN);
      return true;
   case DRI_SECTION:
      unreachable("shouldn't be parsing values in section declarations");
   }

   if (tail == string)
      return false; /* empty string (or containing only white-space) */
   /* skip trailing white space */
   if (*tail)
      tail += strspn(tail, " \f\n\r\t\v");
   if (*tail)
      return false; /* something left over that is not part of value */

   return true;
}

/** \brief Find an option in an option cache with the name as key */
static uint32_t
findOption(const driOptionCache *cache, const char *name)
{
   uint32_t len = strlen(name);
   uint32_t size = 1 << cache->tableSize, mask = size - 1;
   uint32_t hash = 0;
   uint32_t i, shift;

   /* compute a hash from the variable length name */
   for (i = 0, shift = 0; i < len; ++i, shift = (shift+8) & 31)
      hash += (uint32_t)name[i] << shift;
   hash *= hash;
   hash = (hash >> (16-cache->tableSize/2)) & mask;

   /* this is just the starting point of the linear search for the option */
   for (i = 0; i < size; ++i, hash = (hash+1) & mask) {
      /* if we hit an empty entry then the option is not defined (yet) */
      if (cache->info[hash].name == 0)
         break;
      else if (!strcmp(name, cache->info[hash].name))
         break;
   }
   /* this assertion fails if the hash table is full */
   assert (i < size);

   return hash;
}

/** \brief Like strdup with error checking. */
#define XSTRDUP(dest,source) do {                                       \
      if (!(dest = strdup(source))) {                                   \
         fprintf(stderr, "%s: %d: out of memory.\n", __FILE__, __LINE__); \
         abort();                                                       \
      }                                                                 \
   } while (0)

/** \brief Check if a value is in info->range. */
UNUSED static bool
checkValue(const driOptionValue *v, const driOptionInfo *info)
{
   switch (info->type) {
   case DRI_ENUM: /* enum is just a special integer */
   case DRI_INT:
      return (info->range.start._int == info->range.end._int ||
              (v->_int >= info->range.start._int &&
               v->_int <= info->range.end._int));

   case DRI_FLOAT:
      return (info->range.start._float == info->range.end._float ||
              (v->_float >= info->range.start._float &&
               v->_float <= info->range.end._float));

   default:
      return true;
   }
}

void
driParseOptionInfo(driOptionCache *info,
                   const driOptionDescription *configOptions,
                   unsigned numOptions)
{
   /* Make the hash table big enough to fit more than the maximum number of
    * config options we've ever seen in a driver.
    */
   info->tableSize = 6;
   info->info = calloc((size_t)1 << info->tableSize, sizeof(driOptionInfo));
   info->values = calloc((size_t)1 << info->tableSize, sizeof(driOptionValue));
   if (info->info == NULL || info->values == NULL) {
      fprintf(stderr, "%s: %d: out of memory.\n", __FILE__, __LINE__);
      abort();
   }

   UNUSED bool in_section = false;
   for (int o = 0; o < numOptions; o++) {
      const driOptionDescription *opt = &configOptions[o];

      if (opt->info.type == DRI_SECTION) {
         in_section = true;
         continue;
      }

      /* for driconf xml generation, options must always be preceded by a
       * DRI_CONF_SECTION
       */
      assert(in_section);

      const char *name = opt->info.name;
      int i = findOption(info, name);
      driOptionInfo *optinfo = &info->info[i];
      driOptionValue *optval = &info->values[i];

      assert(!optinfo->name); /* No duplicate options in your list. */

      optinfo->type = opt->info.type;
      optinfo->range = opt->info.range;
      XSTRDUP(optinfo->name, name);

      switch (opt->info.type) {
      case DRI_BOOL:
         optval->_bool = opt->value._bool;
         break;

      case DRI_INT:
      case DRI_ENUM:
         optval->_int = opt->value._int;
         break;

      case DRI_FLOAT:
         optval->_float = opt->value._float;
         break;

      case DRI_STRING:
         XSTRDUP(optval->_string, opt->value._string);
         break;

      case DRI_SECTION:
         unreachable("handled above");
      }

      /* Built-in default values should always be valid. */
      assert(checkValue(optval, optinfo));

      char *envVal = getenv(name);
      if (envVal != NULL) {
         driOptionValue v;

         /* make sure the value is initialized to something sensible */
         v._string = NULL;

         if (parseValue(&v, opt->info.type, envVal) &&
             checkValue(&v, optinfo)) {
            /* don't use XML_WARNING, we want the user to see this! */
            if (be_verbose()) {
               fprintf(stderr,
                       "ATTENTION: default value of option %s overridden by environment.\n",
                       name);
            }
            *optval = v;
         } else {
            fprintf(stderr, "illegal environment value for %s: \"%s\".  Ignoring.\n",
                    name, envVal);
         }
      }
   }
}

char *
driGetOptionsXml(const driOptionDescription *configOptions, unsigned numOptions)
{
   char *str = ralloc_strdup(NULL,
      "<?xml version=\"1.0\" standalone=\"yes\"?>\n" \
      "<!DOCTYPE driinfo [\n" \
      "   <!ELEMENT driinfo      (section*)>\n" \
      "   <!ELEMENT section      (description+, option+)>\n" \
      "   <!ELEMENT description  (enum*)>\n" \
      "   <!ATTLIST description  lang CDATA #FIXED \"en\"\n" \
      "                          text CDATA #REQUIRED>\n" \
      "   <!ELEMENT option       (description+)>\n" \
      "   <!ATTLIST option       name CDATA #REQUIRED\n" \
      "                          type (bool|enum|int|float) #REQUIRED\n" \
      "                          default CDATA #REQUIRED\n" \
      "                          valid CDATA #IMPLIED>\n" \
      "   <!ELEMENT enum         EMPTY>\n" \
      "   <!ATTLIST enum         value CDATA #REQUIRED\n" \
      "                          text CDATA #REQUIRED>\n" \
      "]>" \
      "<driinfo>\n");

   bool in_section = false;
   for (int o = 0; o < numOptions; o++) {
      const driOptionDescription *opt = &configOptions[o];

      const char *name = opt->info.name;
      const char *types[] = {
         [DRI_BOOL] = "bool",
         [DRI_INT] = "int",
         [DRI_FLOAT] = "float",
         [DRI_ENUM] = "enum",
         [DRI_STRING] = "string",
      };

      if (opt->info.type == DRI_SECTION) {
         if (in_section)
            ralloc_asprintf_append(&str, "  </section>\n");

         ralloc_asprintf_append(&str,
                                "  <section>\n"
                                "    <description lang=\"en\" text=\"%s\"/>\n",
                                opt->desc);

         in_section = true;
         continue;
      }

      ralloc_asprintf_append(&str,
                             "      <option name=\"%s\" type=\"%s\" default=\"",
                             name,
                             types[opt->info.type]);

      switch (opt->info.type) {
      case DRI_BOOL:
         ralloc_asprintf_append(&str, opt->value._bool ? "true" : "false");
         break;

      case DRI_INT:
      case DRI_ENUM:
         ralloc_asprintf_append(&str, "%d", opt->value._int);
         break;

      case DRI_FLOAT:
         ralloc_asprintf_append(&str, "%f", opt->value._float);
         break;

      case DRI_STRING:
         ralloc_asprintf_append(&str, "%s", opt->value._string);
         break;

      case DRI_SECTION:
         unreachable("handled above");
         break;
      }
      ralloc_asprintf_append(&str, "\"");


      switch (opt->info.type) {
      case DRI_INT:
      case DRI_ENUM:
         if (opt->info.range.start._int < opt->info.range.end._int) {
            ralloc_asprintf_append(&str, " valid=\"%d:%d\"",
                                   opt->info.range.start._int,
                                   opt->info.range.end._int);
         }
         break;

      case DRI_FLOAT:
         if (opt->info.range.start._float < opt->info.range.end._float) {
            ralloc_asprintf_append(&str, " valid=\"%f:%f\"",
                                   opt->info.range.start._float,
                                   opt->info.range.end._float);
         }
         break;

      default:
         break;
      }

      ralloc_asprintf_append(&str, ">\n"); /* end of <option> */


      ralloc_asprintf_append(&str, "        <description lang=\"en\" text=\"%s\"%s>\n",
                             opt->desc, opt->info.type != DRI_ENUM ? "/" : "");

      if (opt->info.type == DRI_ENUM) {
         for (int i = 0; i < ARRAY_SIZE(opt->enums) && opt->enums[i].desc; i++) {
            ralloc_asprintf_append(&str, "          <enum value=\"%d\" text=\"%s\"/>\n",
                                   opt->enums[i].value, opt->enums[i].desc);
         }
         ralloc_asprintf_append(&str, "        </description>\n");
      }

      ralloc_asprintf_append(&str, "      </option>\n");
   }

   assert(in_section);
   ralloc_asprintf_append(&str, "  </section>\n");

   ralloc_asprintf_append(&str, "</driinfo>\n");

   char *output = strdup(str);
   ralloc_free(str);

   return output;
}

/**
 * Print message to \c stderr if the \c LIBGL_DEBUG environment variable
 * is set.
 *
 * Is called from the drivers.
 *
 * \param f \c printf like format string.
 */
static void
__driUtilMessage(const char *f, ...)
{
   va_list args;
   const char *libgl_debug;

   libgl_debug=getenv("LIBGL_DEBUG");
   if (libgl_debug && !strstr(libgl_debug, "quiet")) {
      fprintf(stderr, "libGL: ");
      va_start(args, f);
      vfprintf(stderr, f, args);
      va_end(args);
      fprintf(stderr, "\n");
   }
}

/* We don't have real line/column # info in static-config case: */
#if !WITH_XML_CONFIG
#  define XML_GetCurrentLineNumber(p) -1
#  define XML_GetCurrentColumnNumber(p) -1
#endif

/** \brief Output a warning message. */
#define XML_WARNING1(msg) do {                                          \
      __driUtilMessage("Warning in %s line %d, column %d: "msg, data->name, \
                        (int) XML_GetCurrentLineNumber(data->parser),   \
                        (int) XML_GetCurrentColumnNumber(data->parser)); \
   } while (0)
#define XML_WARNING(msg, ...) do {                                      \
      __driUtilMessage("Warning in %s line %d, column %d: "msg, data->name, \
                        (int) XML_GetCurrentLineNumber(data->parser),   \
                        (int) XML_GetCurrentColumnNumber(data->parser), \
                        ##__VA_ARGS__);                                 \
   } while (0)
/** \brief Output an error message. */
#define XML_ERROR1(msg) do {                                            \
      __driUtilMessage("Error in %s line %d, column %d: "msg, data->name, \
                        (int) XML_GetCurrentLineNumber(data->parser),   \
                        (int) XML_GetCurrentColumnNumber(data->parser)); \
   } while (0)
#define XML_ERROR(msg, ...) do {                                        \
      __driUtilMessage("Error in %s line %d, column %d: "msg, data->name, \
                        (int) XML_GetCurrentLineNumber(data->parser),   \
                        (int) XML_GetCurrentColumnNumber(data->parser), \
                        ##__VA_ARGS__);                                 \
   } while (0)

/** \brief Parser context for configuration files. */
struct OptConfData {
   const char *name;
#if WITH_XMLCONFIG
   XML_Parser parser;
#endif
   driOptionCache *cache;
   int screenNum;
   const char *driverName, *execName;
   const char *kernelDriverName;
   const char *deviceName;
   const char *engineName;
   const char *applicationName;
   uint32_t engineVersion;
   uint32_t applicationVersion;
   uint32_t ignoringDevice;
   uint32_t ignoringApp;
   uint32_t inDriConf;
   uint32_t inDevice;
   uint32_t inApp;
   uint32_t inOption;
};

/** \brief Parse a list of ranges of type info->type. */
static unsigned char
parseRange(driOptionInfo *info, const char *string)
{
   char *cp;

   XSTRDUP(cp, string);

   char *sep;
   sep = strchr(cp, ':');
   if (!sep) {
      free(cp);
      return false;
   }

   *sep = '\0';
   if (!parseValue(&info->range.start, info->type, cp) ||
       !parseValue(&info->range.end, info->type, sep+1)) {
      free(cp);
      return false;
   }
   if (info->type == DRI_INT &&
       info->range.start._int >= info->range.end._int) {
      free(cp);
      return false;
   }
   if (info->type == DRI_FLOAT &&
       info->range.start._float >= info->range.end._float) {
      free(cp);
      return false;
   }

   free(cp);
   return true;
}

/** \brief Parse attributes of a device element. */
static void
parseDeviceAttr(struct OptConfData *data, const char **attr)
{
   uint32_t i;
   const char *driver = NULL, *screen = NULL, *kernel = NULL, *device = NULL;
   for (i = 0; attr[i]; i += 2) {
      if (!strcmp(attr[i], "driver")) driver = attr[i+1];
      else if (!strcmp(attr[i], "screen")) screen = attr[i+1];
      else if (!strcmp(attr[i], "kernel_driver")) kernel = attr[i+1];
      else if (!strcmp(attr[i], "device")) device = attr[i+1];
      else XML_WARNING("unknown device attribute: %s.", attr[i]);
   }
   if (driver && strcmp(driver, data->driverName))
      data->ignoringDevice = data->inDevice;
   else if (kernel && (!data->kernelDriverName ||
                       strcmp(kernel, data->kernelDriverName)))
      data->ignoringDevice = data->inDevice;
   else if (device && (!data->deviceName ||
                       strcmp(device, data->deviceName)))
      data->ignoringDevice = data->inDevice;
   else if (screen) {
      driOptionValue screenNum;
      if (!parseValue(&screenNum, DRI_INT, screen))
         XML_WARNING("illegal screen number: %s.", screen);
      else if (screenNum._int != data->screenNum)
         data->ignoringDevice = data->inDevice;
   }
}

/** \brief Parse attributes of an application element. */
static void
parseAppAttr(struct OptConfData *data, const char **attr)
{
   uint32_t i;
   const char *exec = NULL;
   const char *sha1 = NULL;
   const char *application_name_match = NULL;
   const char *application_versions = NULL;
   driOptionInfo version_range = {
      .type = DRI_INT,
   };

   for (i = 0; attr[i]; i += 2) {
      if (!strcmp(attr[i], "name")) /* not needed here */;
      else if (!strcmp(attr[i], "executable")) exec = attr[i+1];
      else if (!strcmp(attr[i], "sha1")) sha1 = attr[i+1];
      else if (!strcmp(attr[i], "application_name_match"))
         application_name_match = attr[i+1];
      else if (!strcmp(attr[i], "application_versions"))
         application_versions = attr[i+1];
      else XML_WARNING("unknown application attribute: %s.", attr[i]);
   }
   if (exec && strcmp(exec, data->execName)) {
      data->ignoringApp = data->inApp;
   } else if (sha1) {
      /* SHA1_DIGEST_STRING_LENGTH includes terminating null byte */
      if (strlen(sha1) != (SHA1_DIGEST_STRING_LENGTH - 1)) {
         XML_WARNING("Incorrect sha1 application attribute");
         data->ignoringApp = data->inApp;
      } else {
         size_t len;
         char* content;
         char path[PATH_MAX];
         if (util_get_process_exec_path(path, ARRAY_SIZE(path)) > 0 &&
             (content = os_read_file(path, &len))) {
            uint8_t sha1x[SHA1_DIGEST_LENGTH];
            char sha1s[SHA1_DIGEST_STRING_LENGTH];
            _mesa_sha1_compute(content, len, sha1x);
            _mesa_sha1_format((char*) sha1s, sha1x);
            free(content);

            if (strcmp(sha1, sha1s)) {
               data->ignoringApp = data->inApp;
            }
         } else {
            data->ignoringApp = data->inApp;
         }
      }
   } else if (application_name_match) {
      regex_t re;

      if (regcomp(&re, application_name_match, REG_EXTENDED|REG_NOSUB) == 0) {
         if (regexec(&re, data->applicationName, 0, NULL, 0) == REG_NOMATCH)
            data->ignoringApp = data->inApp;
         regfree(&re);
      } else
         XML_WARNING("Invalid application_name_match=\"%s\".", application_name_match);
   }
   if (application_versions) {
      driOptionValue v = { ._int = data->applicationVersion };
      if (parseRange(&version_range, application_versions)) {
         if (!checkValue(&v, &version_range))
            data->ignoringApp = data->inApp;
      } else {
         XML_WARNING("Failed to parse application_versions range=\"%s\".",
                     application_versions);
      }
   }
}

/** \brief Parse attributes of an application element. */
static void
parseEngineAttr(struct OptConfData *data, const char **attr)
{
   uint32_t i;
   const char *engine_name_match = NULL, *engine_versions = NULL;
   driOptionInfo version_range = {
      .type = DRI_INT,
   };
   for (i = 0; attr[i]; i += 2) {
      if (!strcmp(attr[i], "name")) /* not needed here */;
      else if (!strcmp(attr[i], "engine_name_match")) engine_name_match = attr[i+1];
      else if (!strcmp(attr[i], "engine_versions")) engine_versions = attr[i+1];
      else XML_WARNING("unknown application attribute: %s.", attr[i]);
   }
   if (engine_name_match) {
      regex_t re;

      if (regcomp(&re, engine_name_match, REG_EXTENDED|REG_NOSUB) == 0) {
         if (regexec(&re, data->engineName, 0, NULL, 0) == REG_NOMATCH)
            data->ignoringApp = data->inApp;
         regfree(&re);
      } else
         XML_WARNING("Invalid engine_name_match=\"%s\".", engine_name_match);
   }
   if (engine_versions) {
      driOptionValue v = { ._int = data->engineVersion };
      if (parseRange(&version_range, engine_versions)) {
         if (!checkValue(&v, &version_range))
            data->ignoringApp = data->inApp;
      } else {
         XML_WARNING("Failed to parse engine_versions range=\"%s\".",
                     engine_versions);
      }
   }
}

/** \brief Parse attributes of an option element. */
static void
parseOptConfAttr(struct OptConfData *data, const char **attr)
{
   uint32_t i;
   const char *name = NULL, *value = NULL;
   for (i = 0; attr[i]; i += 2) {
      if (!strcmp(attr[i], "name")) name = attr[i+1];
      else if (!strcmp(attr[i], "value")) value = attr[i+1];
      else XML_WARNING("unknown option attribute: %s.", attr[i]);
   }
   if (!name) XML_WARNING1("name attribute missing in option.");
   if (!value) XML_WARNING1("value attribute missing in option.");
   if (name && value) {
      driOptionCache *cache = data->cache;
      uint32_t opt = findOption(cache, name);
      if (cache->info[opt].name == NULL)
         /* don't use XML_WARNING, drirc defines options for all drivers,
          * but not all drivers support them */
         return;
      else if (getenv(cache->info[opt].name)) {
         /* don't use XML_WARNING, we want the user to see this! */
         if (be_verbose()) {
            fprintf(stderr,
                    "ATTENTION: option value of option %s ignored.\n",
                    cache->info[opt].name);
         }
      } else if (!parseValue(&cache->values[opt], cache->info[opt].type, value))
         XML_WARNING("illegal option value: %s.", value);
   }
}

#if WITH_XMLCONFIG

/** \brief Elements in configuration files. */
enum OptConfElem {
   OC_APPLICATION = 0, OC_DEVICE, OC_DRICONF, OC_ENGINE, OC_OPTION, OC_COUNT
};
static const char *OptConfElems[] = {
   [OC_APPLICATION]  = "application",
   [OC_DEVICE] = "device",
   [OC_DRICONF] = "driconf",
   [OC_ENGINE]  = "engine",
   [OC_OPTION] = "option",
};

static int compare(const void *a, const void *b) {
   return strcmp(*(char *const*)a, *(char *const*)b);
}
/** \brief Binary search in a string array. */
static uint32_t
bsearchStr(const char *name, const char *elems[], uint32_t count)
{
   const char **found;
   found = bsearch(&name, elems, count, sizeof(char *), compare);
   if (found)
      return found - elems;
   else
      return count;
}

/** \brief Handler for start element events. */
static void
optConfStartElem(void *userData, const char *name,
                 const char **attr)
{
   struct OptConfData *data = (struct OptConfData *)userData;
   enum OptConfElem elem = bsearchStr(name, OptConfElems, OC_COUNT);
   switch (elem) {
   case OC_DRICONF:
      if (data->inDriConf)
         XML_WARNING1("nested <driconf> elements.");
      if (attr[0])
         XML_WARNING1("attributes specified on <driconf> element.");
      data->inDriConf++;
      break;
   case OC_DEVICE:
      if (!data->inDriConf)
         XML_WARNING1("<device> should be inside <driconf>.");
      if (data->inDevice)
         XML_WARNING1("nested <device> elements.");
      data->inDevice++;
      if (!data->ignoringDevice && !data->ignoringApp)
         parseDeviceAttr(data, attr);
      break;
   case OC_APPLICATION:
      if (!data->inDevice)
         XML_WARNING1("<application> should be inside <device>.");
      if (data->inApp)
         XML_WARNING1("nested <application> or <engine> elements.");
      data->inApp++;
      if (!data->ignoringDevice && !data->ignoringApp)
         parseAppAttr(data, attr);
      break;
   case OC_ENGINE:
      if (!data->inDevice)
         XML_WARNING1("<engine> should be inside <device>.");
      if (data->inApp)
         XML_WARNING1("nested <application> or <engine> elements.");
      data->inApp++;
      if (!data->ignoringDevice && !data->ignoringApp)
         parseEngineAttr(data, attr);
      break;
   case OC_OPTION:
      if (!data->inApp)
         XML_WARNING1("<option> should be inside <application>.");
      if (data->inOption)
         XML_WARNING1("nested <option> elements.");
      data->inOption++;
      if (!data->ignoringDevice && !data->ignoringApp)
         parseOptConfAttr(data, attr);
      break;
   default:
      XML_WARNING("unknown element: %s.", name);
   }
}

/** \brief Handler for end element events. */
static void
optConfEndElem(void *userData, const char *name)
{
   struct OptConfData *data = (struct OptConfData *)userData;
   enum OptConfElem elem = bsearchStr(name, OptConfElems, OC_COUNT);
   switch (elem) {
   case OC_DRICONF:
      data->inDriConf--;
      break;
   case OC_DEVICE:
      if (data->inDevice-- == data->ignoringDevice)
         data->ignoringDevice = 0;
      break;
   case OC_APPLICATION:
   case OC_ENGINE:
      if (data->inApp-- == data->ignoringApp)
         data->ignoringApp = 0;
      break;
   case OC_OPTION:
      data->inOption--;
      break;
   default:
      /* unknown element, warning was produced on start tag */;
   }
}

static void
_parseOneConfigFile(XML_Parser p)
{
#define BUF_SIZE 0x1000
   struct OptConfData *data = (struct OptConfData *)XML_GetUserData(p);
   int status;
   int fd;

   if ((fd = open(data->name, O_RDONLY)) == -1) {
      __driUtilMessage("Can't open configuration file %s: %s.",
                       data->name, strerror(errno));
      return;
   }

   while (1) {
      int bytesRead;
      void *buffer = XML_GetBuffer(p, BUF_SIZE);
      if (!buffer) {
         __driUtilMessage("Can't allocate parser buffer.");
         break;
      }
      bytesRead = read(fd, buffer, BUF_SIZE);
      if (bytesRead == -1) {
         __driUtilMessage("Error reading from configuration file %s: %s.",
                          data->name, strerror(errno));
         break;
      }
      status = XML_ParseBuffer(p, bytesRead, bytesRead == 0);
      if (!status) {
         XML_ERROR("%s.", XML_ErrorString(XML_GetErrorCode(p)));
         break;
      }
      if (bytesRead == 0)
         break;
   }

   close(fd);
#undef BUF_SIZE
}

/** \brief Parse the named configuration file */
static void
parseOneConfigFile(struct OptConfData *data, const char *filename)
{
   XML_Parser p;

   p = XML_ParserCreate(NULL); /* use encoding specified by file */
   XML_SetElementHandler(p, optConfStartElem, optConfEndElem);
   XML_SetUserData(p, data);
   data->parser = p;
   data->name = filename;
   data->ignoringDevice = 0;
   data->ignoringApp = 0;
   data->inDriConf = 0;
   data->inDevice = 0;
   data->inApp = 0;
   data->inOption = 0;

   _parseOneConfigFile(p);
   XML_ParserFree(p);
}

static int
scandir_filter(const struct dirent *ent)
{
#ifndef DT_REG /* systems without d_type in dirent results */
   struct stat st;

   if ((lstat(ent->d_name, &st) != 0) ||
       (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)))
      return 0;
#else
   if (ent->d_type != DT_REG && ent->d_type != DT_LNK)
      return 0;
#endif

   int len = strlen(ent->d_name);
   if (len <= 5 || strcmp(ent->d_name + len - 5, ".conf"))
      return 0;

   return 1;
}

/** \brief Parse configuration files in a directory */
static void
parseConfigDir(struct OptConfData *data, const char *dirname)
{
   int i, count;
   struct dirent **entries = NULL;

   count = scandir(dirname, &entries, scandir_filter, alphasort);
   if (count < 0)
      return;

   for (i = 0; i < count; i++) {
      char filename[PATH_MAX];

      snprintf(filename, PATH_MAX, "%s/%s", dirname, entries[i]->d_name);
      free(entries[i]);

      parseOneConfigFile(data, filename);
   }

   free(entries);
}
#else
#  include "driconf_static.h"

static void
parseStaticOptions(struct OptConfData *data, const struct driconf_option *options,
                   unsigned num_options)
{
   if (data->ignoringDevice || data->ignoringApp)
      return;
   for (unsigned i = 0; i < num_options; i++) {
      const char *optattr[] = {
         "name", options[i].name,
         "value", options[i].value,
         NULL
      };
      parseOptConfAttr(data, optattr);
   }
}

static void
parseStaticConfig(struct OptConfData *data)
{
   data->ignoringDevice = 0;
   data->ignoringApp = 0;
   data->inDriConf = 0;
   data->inDevice = 0;
   data->inApp = 0;
   data->inOption = 0;

   for (unsigned i = 0; i < ARRAY_SIZE(driconf); i++) {
      const struct driconf_device *d = driconf[i];
      const char *devattr[] = {
         "driver", d->driver,
         "device", d->device,
         NULL
      };

      data->ignoringDevice = 0;
      data->inDevice++;
      parseDeviceAttr(data, devattr);
      data->inDevice--;

      data->inApp++;

      for (unsigned j = 0; j < d->num_engines; j++) {
         const struct driconf_engine *e = &d->engines[j];
         const char *engattr[] = {
            "engine_name_match", e->engine_name_match,
            "engine_versions", e->engine_versions,
            NULL
         };

         data->ignoringApp = 0;
         parseEngineAttr(data, engattr);
         parseStaticOptions(data, e->options, e->num_options);
      }

      for (unsigned j = 0; j < d->num_applications; j++) {
         const struct driconf_application *a = &d->applications[j];
         const char *appattr[] = {
            "name", a->name,
            "executable", a->executable,
            "executable_regexp", a->executable_regexp,
            "sha1", a->sha1,
            "application_name_match", a->application_name_match,
            "application_versions", a->application_versions,
            NULL
         };

         data->ignoringApp = 0;
         parseAppAttr(data, appattr);
         parseStaticOptions(data, a->options, a->num_options);
      }

      data->inApp--;
   }
}
#endif /* WITH_XMLCONFIG */

/** \brief Initialize an option cache based on info */
static void
initOptionCache(driOptionCache *cache, const driOptionCache *info)
{
   unsigned i, size = 1 << info->tableSize;
   cache->info = info->info;
   cache->tableSize = info->tableSize;
   cache->values = malloc(((size_t)1 << info->tableSize) * sizeof(driOptionValue));
   if (cache->values == NULL) {
      fprintf(stderr, "%s: %d: out of memory.\n", __FILE__, __LINE__);
      abort();
   }
   memcpy(cache->values, info->values,
           ((size_t)1 << info->tableSize) * sizeof(driOptionValue));
   for (i = 0; i < size; ++i) {
      if (cache->info[i].type == DRI_STRING)
         XSTRDUP(cache->values[i]._string, info->values[i]._string);
   }
}

#ifndef SYSCONFDIR
#define SYSCONFDIR "/etc"
#endif

#ifndef DATADIR
#define DATADIR "/usr/share"
#endif

static const char *datadir = DATADIR "/drirc.d";
static const char *execname;

void
driInjectDataDir(const char *dir)
{
   datadir = dir;
}

void
driInjectExecName(const char *exec)
{
   execname = exec;
}

void
driParseConfigFiles(driOptionCache *cache, const driOptionCache *info,
                    int screenNum, const char *driverName,
                    const char *kernelDriverName,
                    const char *deviceName,
                    const char *applicationName, uint32_t applicationVersion,
                    const char *engineName, uint32_t engineVersion)
{
   initOptionCache(cache, info);
   struct OptConfData userData;

   userData.cache = cache;
   userData.screenNum = screenNum;
   userData.driverName = driverName;
   userData.kernelDriverName = kernelDriverName;
   userData.deviceName = deviceName;
   userData.applicationName = applicationName ? applicationName : "";
   userData.applicationVersion = applicationVersion;
   userData.engineName = engineName ? engineName : "";
   userData.engineVersion = engineVersion;
   userData.execName = execname ? execname : util_get_process_name();

#if WITH_XMLCONFIG
   char *home;

   parseConfigDir(&userData, datadir);
   parseOneConfigFile(&userData, SYSCONFDIR "/drirc");

   if ((home = getenv("HOME"))) {
      char filename[PATH_MAX];

      snprintf(filename, PATH_MAX, "%s/.drirc", home);
      parseOneConfigFile(&userData, filename);
   }
#else
   parseStaticConfig(&userData);
#endif /* WITH_XMLCONFIG */
}

void
driDestroyOptionInfo(driOptionCache *info)
{
   driDestroyOptionCache(info);
   if (info->info) {
      uint32_t i, size = 1 << info->tableSize;
      for (i = 0; i < size; ++i) {
         if (info->info[i].name) {
            free(info->info[i].name);
         }
      }
      free(info->info);
   }
}

void
driDestroyOptionCache(driOptionCache *cache)
{
   if (cache->info) {
      unsigned i, size = 1 << cache->tableSize;
      for (i = 0; i < size; ++i) {
         if (cache->info[i].type == DRI_STRING)
            free(cache->values[i]._string);
      }
   }
   free(cache->values);
}

unsigned char
driCheckOption(const driOptionCache *cache, const char *name,
               driOptionType type)
{
   uint32_t i = findOption(cache, name);
   return cache->info[i].name != NULL && cache->info[i].type == type;
}

unsigned char
driQueryOptionb(const driOptionCache *cache, const char *name)
{
   uint32_t i = findOption(cache, name);
   /* make sure the option is defined and has the correct type */
   assert(cache->info[i].name != NULL);
   assert(cache->info[i].type == DRI_BOOL);
   return cache->values[i]._bool;
}

int
driQueryOptioni(const driOptionCache *cache, const char *name)
{
   uint32_t i = findOption(cache, name);
   /* make sure the option is defined and has the correct type */
   assert(cache->info[i].name != NULL);
   assert(cache->info[i].type == DRI_INT || cache->info[i].type == DRI_ENUM);
   return cache->values[i]._int;
}

float
driQueryOptionf(const driOptionCache *cache, const char *name)
{
   uint32_t i = findOption(cache, name);
   /* make sure the option is defined and has the correct type */
   assert(cache->info[i].name != NULL);
   assert(cache->info[i].type == DRI_FLOAT);
   return cache->values[i]._float;
}

char *
driQueryOptionstr(const driOptionCache *cache, const char *name)
{
   uint32_t i = findOption(cache, name);
   /* make sure the option is defined and has the correct type */
   assert(cache->info[i].name != NULL);
   assert(cache->info[i].type == DRI_STRING);
   return cache->values[i]._string;
}
