/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2021 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

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

#include "prefs.h"
#include "pref_tables.h"
#include "status_types.h"
#include "defaults.h"
#include "log.h"
#include "file.h"
#include "datafile.h"
#include "parse.h"

#define PREFS_COMMENT_CHARACTER '#'
#define PREFS_MAGIC_NUMBER 0x4005

void
setStatusFields (const unsigned char *fields) {
  if (!statusFieldsSet) {
    if (fields) {
      unsigned int index = 0;

      while (index < (ARRAY_COUNT(prefs.statusFields) - 1)) {
        unsigned char field = fields[index];
        if (field == sfEnd) break;
        prefs.statusFields[index++] = field;
      }

      statusFieldsSet = 1;
    }
  }
}

static void
setStatusStyle (unsigned char style) {
  static const unsigned char styleNone[] = {
    sfEnd
  };

  static const unsigned char styleAlva[] = {
    sfAlphabeticCursorCoordinates, sfAlphabeticWindowCoordinates, sfStateLetter, sfEnd
  };

  static const unsigned char styleTieman[] = {
    sfCursorAndWindowColumn2, sfCursorAndWindowRow2, sfStateDots, sfEnd
  };

  static const unsigned char stylePB80[] = {
    sfWindowRow, sfEnd
  };

  static const unsigned char styleConfigurable[] = {
    sfGeneric, sfEnd
  };

  static const unsigned char styleMDV[] = {
    sfWindowCoordinates2, sfEnd
  };

  static const unsigned char styleVoyager[] = {
    sfWindowRow, sfCursorRow, sfCursorColumn, sfEnd
  };

  static const unsigned char styleTime[] = {
    sfTime, sfEnd
  };

  static const unsigned char *const styleTable[] = {
    styleNone, styleAlva, styleTieman, stylePB80,
    styleConfigurable, styleMDV, styleVoyager, styleTime
  };
  static const unsigned char styleCount = ARRAY_COUNT(styleTable);

  if (style < styleCount) {
    const unsigned char *fields = styleTable[style];
    if (*fields != sfEnd) setStatusFields(fields);
  }
}

static int
comparePreferenceNames (const char *name1, const char *name2) {
  return strcmp(name1, name2);
}

static int
sortPreferenceDefinitions (const void *element1, const void *element2) {
  const PreferenceDefinition *const *pref1 = element1;
  const PreferenceDefinition *const *pref2 = element2;
  return comparePreferenceNames((*pref1)->name, (*pref2)->name);
}

static int
searchPreferenceDefinition (const void *target, const void *element) {
  const char *name = target;
  const PreferenceDefinition *const *pref = element;
  return comparePreferenceNames(name, (*pref)->name);
}

static const PreferenceDefinition *
findPreferenceDefinition (const char *name) {
  static const PreferenceDefinition **sortedDefinitions = NULL;

  if (!sortedDefinitions) {
    if (!(sortedDefinitions = malloc(ARRAY_SIZE(sortedDefinitions, preferenceDefinitionCount)))) {
      logMallocError();
      return NULL;
    }

    for (unsigned int index=0; index<preferenceDefinitionCount; index+=1) {
      sortedDefinitions[index] = &preferenceDefinitionTable[index];
    }

    qsort(
      sortedDefinitions, preferenceDefinitionCount,
      sizeof(*sortedDefinitions), sortPreferenceDefinitions
    );
  }

  {
    const PreferenceDefinition *const *pref = bsearch(
      name, sortedDefinitions, preferenceDefinitionCount,
      sizeof(*sortedDefinitions), searchPreferenceDefinition
    );

    if (pref) return *pref;
  }

  return NULL;
}

static int
sortPreferenceAliases (const void *element1, const void *element2) {
  const PreferenceAlias *const *alias1 = element1;
  const PreferenceAlias *const *alias2 = element2;
  return comparePreferenceNames((*alias1)->oldName, (*alias2)->oldName);
}

static int
searchPreferenceAlias (const void *target, const void *element) {
  const char *name = target;
  const PreferenceAlias *const *alias = element;
  return comparePreferenceNames(name, (*alias)->oldName);
}

static const PreferenceAlias *
findPreferenceAlias (const char *name) {
  static const PreferenceAlias **sortedAliases = NULL;

  if (!sortedAliases) {
    if (!(sortedAliases = malloc(ARRAY_SIZE(sortedAliases, preferenceAliasCount)))) {
      logMallocError();
      return NULL;
    }

    for (unsigned int index=0; index<preferenceAliasCount; index+=1) {
      sortedAliases[index] = &preferenceAliasTable[index];
    }

    qsort(
      sortedAliases, preferenceAliasCount,
      sizeof(*sortedAliases), sortPreferenceAliases
    );
  }

  {
    const PreferenceAlias *const *alias = bsearch(
      name, sortedAliases, preferenceAliasCount,
      sizeof(*sortedAliases), searchPreferenceAlias
    );

    if (alias) return *alias;
  }

  return NULL;
}

const PreferenceDefinition *
findPreference (const char *name) {
  while (name) {
    {
      const PreferenceDefinition *pref = findPreferenceDefinition(name);
      if (pref) return pref;
    }

    {
      const PreferenceAlias *alias = findPreferenceAlias(name);
      if (!alias) break;
      name = alias->newName;
    }
  }

  if (name) logMessage(LOG_WARNING, "unknown preference: %s", name);
  return NULL;
}

static void
resetPreference (const PreferenceDefinition *pref) {
  if (pref->settingCount) {
    memset(pref->setting, pref->defaultValue, pref->settingCount);
  } else {
    *pref->setting = pref->defaultValue;
  }

  if (pref->encountered) *pref->encountered = 0;
}

void
resetPreferences (void) {
  memset(&prefs, 0, sizeof(prefs));

  prefs.magic[0] = PREFS_MAGIC_NUMBER & 0XFF;
  prefs.magic[1] = PREFS_MAGIC_NUMBER >> 8;
  prefs.version = 6;

  {
    const PreferenceDefinition *pref = preferenceDefinitionTable;
    const PreferenceDefinition *end = pref + preferenceDefinitionCount;

    while (pref < end) resetPreference(pref++);
  }
}

static const char *
getSettingName (const PreferenceDefinition *pref, unsigned char index) {
  const PreferenceStringTable *names = pref->settingNames;
  if (!names) return NULL;
  if (index >= names->count) return NULL;
  return names->table[index];
}

static int
changePreferenceSetting (
  const char *name, const char *operand,
  unsigned char *setting, const PreferenceStringTable *names
) {
  if (names) {
    for (unsigned int index=0; index<names->count; index+=1) {
      const char *name = names->table[index];
      if (!name) continue;

      if (strcmp(operand, name) == 0) {
        *setting = index;
        return 1;
      }
    }
  }

  {
    int value;

    if (isInteger(&value, operand)) {
      unsigned char maximum = names? (names->count - 1): 0XFF;

      if ((value >= 0) && (value <= maximum)) {
        *setting = value;
        return 1;
      }
    }
  }

  logMessage(LOG_WARNING, "invalid preference setting: %s %s", name, operand);
  return 0;
}

int
setPreference (char *string) {
  const char *name;

  {
    static const char delimiters[] = {
      ' ', '\t', PARAMETER_ASSIGNMENT_CHARACTER, 0
    };

    name = strtok(string, delimiters);
  }

  if (name) {
    const PreferenceDefinition *pref = findPreference(name);

    if (pref) {
      if (pref->encountered) *pref->encountered = 1;

      static const char delimiters[] = " \t";
      const char *operand;

      if (pref->settingCount) {
        unsigned char count = pref->settingCount;
        unsigned char *setting = pref->setting;

        while (count) {
          if ((operand = strtok(NULL, delimiters))) {
            if (changePreferenceSetting(name, operand, setting, pref->settingNames)) {
              setting += 1;
              count -= 1;
              continue;
            }
          }

          *setting = 0;
          break;
        }
      } else if (!(operand = strtok(NULL, delimiters))) {
        logMessage(LOG_WARNING, "missing preference setting: %s", name);
      } else if (!changePreferenceSetting(name, operand, pref->setting, pref->settingNames)) {
      }
    }
  } else {
    logMessage(LOG_WARNING, "missing preference name");
  }

  return 1;
}

char *
makePreferencesFilePath (const char *name) {
  if (!name) name = PREFERENCES_FILE;
  return makeUpdatablePath(name);
}

static int
processPreferenceLine (const LineHandlerParameters *parameters) {
  char *line = parameters->line.text;
  while (isspace(*line)) line += 1;
  if (!*line) return 1;
  if (*line == PREFS_COMMENT_CHARACTER) return 1;
  return setPreference(line);
}

int
loadPreferencesFile (const char *path) {
  int ok = 0;

  logMessage(LOG_DEBUG, "loading preferences file: %s", path);
  FILE *file = openDataFile(path, "rb", 1);

  if (file) {
    PreferenceSettings newPreferences;
    size_t length = fread(&newPreferences, 1, sizeof(newPreferences), file);

    if (ferror(file)) {
      logMessage(LOG_ERR, "%s: %s: %s",
                 gettext("cannot read preferences file"), path, strerror(errno));
    } else if ((length < 40) ||
               (newPreferences.magic[0] != (PREFS_MAGIC_NUMBER & 0XFF)) ||
               (newPreferences.magic[1] != (PREFS_MAGIC_NUMBER >> 8))) {
      fclose(file);

      if ((file = openDataFile(path, "r", 1))) {
        resetPreferences();
        if (processLines(file, processPreferenceLine, NULL)) ok = 1;
      }
    } else {
      prefs = newPreferences;
      ok = 1;

      {
        const PreferenceDefinition *pref = preferenceDefinitionTable;
        const PreferenceDefinition *end = pref + preferenceDefinitionCount;

        const unsigned char *from = prefs.magic;
        const unsigned char *to = from + length;

        while (pref < end) {
          unsigned char count = pref->settingCount;
          if (!count) count = 1;

          if ((pref->setting < from) || ((pref->setting + count) > to)) {
            resetPreference(pref);
          }

          pref += 1;
        }
      }

      if (length < (prefs.statusFields + sizeof(prefs.statusFields) - prefs.magic)) {
        setStatusStyle(prefs.expandCurrentWord);
      } else {
        statusFieldsSet = 1;
      }

      if (prefs.version == 0) {
        prefs.version += 1;
        prefs.pcmVolume = DEFAULT_PCM_VOLUME;
        prefs.midiVolume = DEFAULT_MIDI_VOLUME;
        prefs.fmVolume = DEFAULT_FM_VOLUME;
      }

      if (prefs.version == 1) {
        prefs.version += 1;
        prefs.sayLineMode = DEFAULT_SAY_LINE_MODE;
        prefs.autospeak = DEFAULT_AUTOSPEAK;
      }

      if (prefs.version == 2) {
        prefs.version += 1;
        prefs.autorepeatEnabled = DEFAULT_AUTOREPEAT_ENABLED;
        prefs.longPressTime = DEFAULT_LONG_PRESS_TIME;
        prefs.autorepeatInterval = DEFAULT_AUTOREPEAT_INTERVAL;
        prefs.screenCursorVisibleTime *= 4;
        prefs.screenCursorInvisibleTime *= 4;
        prefs.attributesVisibleTime *= 4;
        prefs.attributesInvisibleTime *= 4;
        prefs.capitalsVisibleTime *= 4;
        prefs.capitalsInvisibleTime *= 4;
      }

      if (prefs.version == 3) {
        prefs.version += 1;
        prefs.autorepeatPanning = DEFAULT_AUTOREPEAT_PANNING;
      }

      if (prefs.version == 4) {
        prefs.version += 1;
        prefs.touchSensitivity = DEFAULT_TOUCH_SENSITIVITY;
      }

      if (prefs.version == 5) {
        prefs.version += 1;
        prefs.expandCurrentWord = DEFAULT_EXPAND_CURRENT_WORD;
      }
    }

    if (file) {
      fclose(file);
      file = NULL;
    }
  }

  return ok;
}

static int
putPreferenceComment (FILE *file, const PreferenceDefinition *pref) {
  if (fprintf(file, "\n%c %s", PREFS_COMMENT_CHARACTER, pref->name) < 0) return 0;

  if (pref->settingCount) {
    if (fprintf(file, "[%u]", pref->settingCount) < 0) return 0;
  }

  if (fputs(": ", file) == EOF) return 0;
  const char *name = getSettingName(pref, pref->defaultValue);

  if (name) {
    if (fputs(name, file) == EOF) return 0;
  } else {
    if (fprintf(file, "%u", pref->defaultValue) < 0) return 0;
  }

  if (pref->settingNames) {
    if (fputs(" {", file) == EOF) return 0;

    unsigned char count = pref->settingNames->count;
    int first = 1;

    for (unsigned char index=0; index<count; index+=1) {
      const char *name = getSettingName(pref, index);
      if (name) {
        if (first) {
          first = 0;
        } else if (fputc(' ', file) == EOF) {
          return 0;
        }

        if (fputs(name, file) == EOF) return 0;
      } else {
        logMessage(LOG_WARNING,
          "unnamed preference setting: %s: %u",
          pref->name, index
        );
      }
    }

    if (fputc('}', file) == EOF) return 0;
  }

  if (fputc('\n', file) == EOF) return 0;
  return 1;
}

static int
putSetting (FILE *file, const PreferenceDefinition *pref, unsigned char setting) {
  if (fputc(' ', file) == EOF) return 0;
  const char *name = getSettingName(pref, setting);

  if (name) {
    if (fputs(name, file) == EOF) return 0;
  } else {
    if (fprintf(file, "%u", setting) < 0) return 0;
  }

  return 1;
}

static int
putPreference (FILE *file, const PreferenceDefinition *pref) {
  if (pref->dontSave) return 1;

  if (!putPreferenceComment(file, pref)) return 0;
  if (fputs(pref->name, file) == EOF) return 0;

  if (pref->settingCount) {
    unsigned char count = pref->settingCount;
    unsigned char *setting = pref->setting;

    while (count-- && *setting) {
      if (!putSetting(file, pref, *setting++)) return 0;
    }
  } else if (!putSetting(file, pref, *pref->setting)) {
    return 0;
  }

  if (fputs("\n", file) == EOF) return 0;
  return 1;
}

static int
putPreferences (FILE *file) {
  const PreferenceDefinition *pref = preferenceDefinitionTable;
  const PreferenceDefinition *const end = pref + preferenceDefinitionCount;

  while (pref < end) {
    if (!putPreference(file, pref)) return 0;
    pref += 1;
  }

  return 1;
}

static int
putHeader (FILE *file) {
  fprintf(file,
    "%c %s Preferences File\n",
    PREFS_COMMENT_CHARACTER, PACKAGE_NAME
  );

  return !ferror(file);
}

int
savePreferencesFile (const char *path) {
  int ok = 0;
  FILE *file = openDataFile(path, "w", 0);

  if (file) {
    if (putHeader(file)) {
      if (putPreferences(file)) {
        ok = 1;
      }
    }

    if (!ok) {
      if (!ferror(file)) errno = EIO;
      logMessage(LOG_ERR,
        "%s: %s: %s",
        gettext("cannot write to preferences file"),
        path, strerror(errno)
      );
    }

    fclose(file);
  }

  return ok;
}
