-------------------------------------------------------------------------------
--         FILE:  luaotfload-configuration.lua
--  DESCRIPTION:  part of luaotfload / luaotfload-tool / config file reader
--       AUTHOR:  Philipp Gesang, <phg@phi-gamma.net>
--       AUTHOR:  Dohyun Kim <nomosnomos@gmail.com>
-------------------------------------------------------------------------------

assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") { 
    name          = "luaotfload-configuration",
    version       = "3.29",       --TAGVERSION
    date          = "2024-12-03", --TAGDATE
    description   = "luaotfload submodule / config file reader",
    license       = "GPL v2.0"
}

------------------------------

local status_file             = "luaotfload-status"
local luaotfloadstatus        = require (status_file)

local string                  = string
local stringfind              = string.find
local stringformat            = string.format
local stringstrip             = string.strip
local stringsub               = string.sub

local tableappend             = table.append
local tableconcat             = table.concat
local tablecopy               = table.copy
local table                   = table
local tabletohash             = table.tohash

local math                    = math
local mathfloor               = math.floor

local io                      = io
local ioloaddata              = io.loaddata
local iopopen                 = io.popen
local iowrite                 = io.write

local os                      = os
local osdate                  = os.date
local osgetenv                = os.getenv

local lpeg                    = require "lpeg"
local lpegmatch               = lpeg.match
local commasplitter           = lpeg.splitat ","
local equalssplitter          = lpeg.splitat "="

local kpseexpand_path         = kpse.expand_path

local lfs                     = lfs
local lfsisfile               = lfs.isfile
local lfsisdir                = lfs.isdir

local file                    = file
local filejoin                = file.join
local filereplacesuffix       = file.replacesuffix

local logreport               = print -- overloaded later
local getwritablepath         = luaotfload.fontloader.caches.getwritablepath


local config_parser -- set later during init
local stripslashes  -- set later during init

-------------------------------------------------------------------------------
---                                SETTINGS
-------------------------------------------------------------------------------

local path_t = 0
local kpse_t = 1

local val_home            = kpseexpand_path "~"
local val_xdg_config_home = kpseexpand_path "$XDG_CONFIG_HOME"

if val_xdg_config_home == "" then val_xdg_config_home = "~/.config" end

local config_paths = {
  --- needs adapting for those other OS
  { path_t, "./luaotfload.conf" },
  { path_t, "./luaotfloadrc" },
  { path_t, filejoin (val_xdg_config_home, "luaotfload/luaotfload.conf") },
  { path_t, filejoin (val_xdg_config_home, "luaotfload/luaotfloadrc") },
  { path_t, filejoin (val_home, ".luaotfloadrc") },
  { kpse_t, "luaotfloadrc" },
  { kpse_t, "luaotfload.conf" },
}

local valid_formats = tabletohash {
  "otf", "ttc", "ttf", "afm", "pfb"
}

local default_location_precedence = {
  "system", "texmf", "local"
}
local valid_locations = tabletohash {
  "system", "texmf", "local"
}

local default_anon_sequence = {
  "tex", "path", "name"
}

local valid_resolvers = tabletohash {
  "tex", "path", "name", "file", "my"
}

local base_features = tabletohash {
  -- Adopt the generic default from HarfBuzz.
  -- The first line is generally enabled there,
  -- the second only for horizontal text.
  -- The third line is luaotfload specific
  "abvm", "blwm", "ccmp", "locl", "mark", "mkmk", "rlig",
  "calt", "clig", "curs", "dist", "kern", "liga", "rclt",
  "itlc",
}
local feature_presets = {
  arab = table.merged( tabletohash {
    "isol", "fina", "fin2", "fin3", "medi", "med2", "init",
    "cswh", "mset",
  }, base_features),
  deva = table.merged( tabletohash {
    "init", "nukt", "akhn", "rphf", "blwf", "half", "pstf",
    "vatu", "pres", "blws", "abvs", "psts", "haln",
  }, base_features),
  khmr = table.merged( tabletohash {
    "pref", "blwf", "abvf", "pstf", "pres", "blws", "abvs",
    "psts",
  }, base_features),
  hang = table.merged( tabletohash {
    "ccmp", "ljmo", "vjmo", "tjmo",
  }, base_features),
}

--[[doc--

  We allow loading of arbitrary fontloaders. Nevertheless we maintain a
  list of the “official” ones shipped with Luaotfload so we can emit a
  different log message.

--doc]]--

local function default_fontloader ()
  return luaotfloadstatus and luaotfloadstatus.notes.loader or "reference"
end

local registered_loaders = {
  default    = default_fontloader (),
  reference  = "reference",
  unpackaged = "unpackaged",
  context    = "context",
}

local function pick_fontloader (s)
  local ldr = registered_loaders[s]
  if ldr ~= nil and type (ldr) == "string" then
    logreport ("log", 2, "conf", "Using predefined fontloader %q.", ldr)
    return ldr
  end
  local idx = stringfind (s, ":")
  if idx and idx > 2 then
    if stringsub (s, 1, idx - 1) == "context" then
      local pth = stringsub (s, idx + 1)
      pth = stringstrip (pth)
      logreport ("log", 2, "conf", "Context base path specified at %q.", pth)
      if lfsisdir (pth) then
        logreport ("log", 5, "conf", "Context base path exists at %q.", pth)
        return pth
      end
      pth = kpseexpand_path (pth)
      if lfsisdir (pth) then
        logreport ("log", 5, "conf", "Context base path exists at %q.", pth)
        return pth
      end
      logreport ("both", 0, "conf", "Context base path not found at %q.", pth)
    end
  end
  return nil
end

--[[doc--

  The ``post_linebreak_filter`` has been made the default callback for
  hooking the colorizer into. This helps with the linebreaking whose
  inserted hyphens would remain unaffected by the coloring otherwise.

  http://tex.stackexchange.com/q/238539/14066

--doc]]--

local permissible_color_callbacks = {
  default               = "post_linebreak_filter",
  pre_linebreak_filter  = "pre_linebreak_filter",
  post_linebreak_filter = "post_linebreak_filter",
  pre_output_filter     = "pre_output_filter",
}

local known_dvi_drivers = {
  xdvipsk = 'xdvipsk',
  dvisvgm = 'dvisvgm',
}

-------------------------------------------------------------------------------
---                                DEFAULTS
-------------------------------------------------------------------------------

local default_config = {
  db = {
    location_precedence = default_location_precedence,
    formats             = "otf,ttf,ttc",
    scan_local          = false,
    skip_read           = false,
    strip               = true,
    update_live         = true,
    compress            = true,
    max_fonts           = 2^51,
    designsize_dimen    = "bp",
  },
  run = {
    anon_sequence      = default_anon_sequence,
    resolver           = "cached",
    definer            = "patch",
    log_level          = 0,
    color_callback     = "post_linebreak_filter",
    fontloader         = default_fontloader (),
    default_dvi_driver = "dvisvgm",
    live               = true,
  },
  misc = {
    bisect         = false,
    version        = luaotfload.version,
    statistics     = false,
    termwidth      = nil,
    keepnames      = true,
  },
  paths = {
    names_dir           = "names",
    cache_dir           = "fonts",
    index_file          = "luaotfload-names.lua",
    lookups_file        = "luaotfload-lookup-cache.lua",
    lookup_path_lua     = nil,
    lookup_path_luc     = nil,
    index_path_lua      = nil,
    index_path_luc      = nil,
  },
  default_features = {
    global = { mode = "node" },
    dflt = base_features,

    arab = feature_presets.arab,
    syrc = feature_presets.arab,
    mong = feature_presets.arab,
    nko  = feature_presets.arab,

    deva = feature_presets.deva,
    beng = feature_presets.deva,
    guru = feature_presets.deva,
    gujr = feature_presets.deva,
    orya = feature_presets.deva,
    taml = feature_presets.deva,
    telu = feature_presets.deva,
    knda = feature_presets.deva,
    mlym = feature_presets.deva,
    sinh = feature_presets.deva,

    khmr = feature_presets.khmr,
    tibt = feature_presets.khmr,
    thai = feature_presets.thai,
    lao  = feature_presets.thai,

    hang = feature_presets.hang,
  },
  status = luaotfloadstatus,
}

-------------------------------------------------------------------------------
---                          RECONFIGURATION TASKS
-------------------------------------------------------------------------------

--[[doc--

    Procedures to be executed in order to put the new configuration into effect.

--doc]]--

local reconf_tasks = nil

local min_terminal_width = 40

--- The “termwidth” value is only considered when printing
--- short status messages, e.g. when building the database
--- online.
local function check_termwidth ()
  if config.luaotfload.misc.termwidth == nil then
      local tw = 79
      if not (   os.type == "windows" --- Assume broken terminal.
              or osgetenv "TERM" == "dumb")
      then
          local p = iopopen "tput cols"
          if p then
              result = tonumber (p:read "*all")
              p:close ()
              if result then
                  tw = result
              else
                  logreport ("log", 2, "db", "tput returned non-number.")
              end
          else
              logreport ("log", 2, "db", "Shell escape disabled or tput executable missing.")
              logreport ("log", 2, "db", "Assuming 79 cols terminal width.")
          end
      end
      config.luaotfload.misc.termwidth = tw
  end
  return true
end

local function set_font_filter ()
  local names = fonts.names
  if names and names.set_font_filter then
    local formats = config.luaotfload.db.formats
    if not formats or formats == "" then
      formats = default_config.db.formats
    end
    names.set_font_filter (formats)
  end
  return true
end

local function set_location_precedence_list ()
  local names = fonts.names
  if names and names.set_location_precedence then
    local locations = config.luaotfload.db.location_precedence
    if not locations or locations == "" then
      locations = default_config.db.location_precedence
    end
    fonts.names.set_location_precedence (locations)
  end
  return true
end

local function set_size_dimension ()
  local names = fonts.names
  if names and names.set_size_dimension then
    local dim = config.luaotfload.db.designsize_dimen
    if not dim or dim == "" then
      dim = default_config.db.designsize_dimen
    end
    names.set_size_dimension (dim)
  end
  return true
end

local function set_name_resolver ()
  local names = fonts.names
  if names and names.resolve_cached then
    --- replace the resolver from luatex-fonts
    if config.luaotfload.db.resolver == "cached" then
        logreport ("both", 2, "cache", "Caching of name: lookups active.")
        names.resolvespec  = fonts.names.lookup_font_name_cached
    else
        names.resolvespec  = fonts.names.lookup_font_name
    end
  end
  return true
end

local function set_loglevel ()
  if luaotfload then
    luaotfload.log.set_loglevel (config.luaotfload.run.log_level)
    return true
  end
  return false
end

local function build_cache_paths ()
  local paths  = config.luaotfload.paths
  local prefix = getwritablepath (paths.names_dir, "")

  if not prefix then
    luaotfload.error ("Impossible to find a suitable writeable cache...")
    return false
  end

  prefix = lpegmatch (stripslashes, prefix)
  logreport ("log", 0, "conf", "Root cache directory is %q.", prefix)

  local index_file      = filejoin (prefix, paths.index_file)
  local lookups_file    = filejoin (prefix, paths.lookups_file)

  paths.prefix          = prefix
  paths.index_path_lua  = filereplacesuffix (index_file,   "lua")
  paths.index_path_luc  = filereplacesuffix (index_file,   "luc")
  paths.lookup_path_lua = filereplacesuffix (lookups_file, "lua")
  paths.lookup_path_luc = filereplacesuffix (lookups_file, "luc")
  return true
end


local function set_default_features ()
  local default_features = config.luaotfload.default_features
  luaotfload.features    = luaotfload.features or {
                             global   = { },
                             defaults = { },
                           }
  local current_features = luaotfload.features
  for var, val in next, default_features do
    if var == "global" then
      current_features.global = val
    else
      current_features.defaults[var] = val
    end
  end
  return true
end

reconf_tasks = {
  { "Set the log level"         , set_loglevel         },
  { "Build cache paths"         , build_cache_paths    },
  { "Check terminal dimensions" , check_termwidth      },
  { "Set the font filter"       , set_font_filter      },
  { "Set design size dimension" , set_size_dimension   },
  { "Install font name resolver", set_name_resolver    },
  { "Set default features"      , set_default_features },
  { "Set location precedence"   , set_location_precedence_list },
}

-------------------------------------------------------------------------------
---                          OPTION SPECIFICATION
-------------------------------------------------------------------------------

local string_t    = "string"
local table_t     = "table"
local number_t    = "number"
local boolean_t   = "boolean"
local function_t  = "function"

local function tointeger (n)
  n = tonumber (n)
  if n then
    return mathfloor (n + 0.5)
  end
end

local function toarray (s)
  local fields = { lpegmatch (commasplitter, s) }
  local ret    = { }
  for i = 1, #fields do
    local field = stringstrip (fields[i])
    if field and field ~= "" then
      ret[#ret + 1] = field
    end
  end
  return ret
end

local function tohash (s)
  local result = { }
  local fields = toarray (s)
  for _, field in next, fields do
    local var, val
    if stringfind (field, "=") then
      local tmp
      var, tmp = lpegmatch (equalssplitter, field)
      if tmp == "true" or tmp == "yes" then val = true else val = tmp end
    else
      var, val = field, true
    end
    result[var] = val
  end
  return result
end

local option_spec = {
  db = {
    formats      = {
      in_t  = string_t,
      out_t = string_t,
      transform = function (f)
        local fields = toarray (f)

        --- check validity
        if not fields then
          logreport ("both", 0, "conf",
                     "Expected list of identifiers, got %q.", f)
          return nil
        end

        --- strip dupes
        local known  = { }
        local result = { }
        for i = 1, #fields do
          local field = fields[i]
          if known[field] ~= true then
            --- yet unknown, tag as seen
            known[field] = true
            --- include in output if valid
            if valid_formats[field] == true then
              result[#result + 1] = field
            else
              logreport ("both", 4, "conf",
                         "Invalid font format identifier %q, ignoring.",
                         field)
            end
          end
        end
        if #result == 0 then
          --- force defaults
          return nil
        end
        return tableconcat (result, ",")
      end
    },
    location_precedence = {
      in_t      = string_t,
      out_t     = table_t,
      transform = function (s)
        local bits = { lpegmatch (commasplitter, s) }
        if next (bits) then
          local seq = { }
          local done = { }
          for i = 1, #bits do
            local bit = bits [i]
            if valid_locations [bit] then
              if not done [bit] then
                done [bit] = true
                seq [#seq + 1] = bit
              else
                logreport ("both", 0, "conf",
                           "ignoring duplicate location %s at position %d \z
                            in precedence list",
                           bit, i)
              end
            else
              logreport ("both", 0, "conf",
                         "location precedence list contains invalid item %s \z
                          at position %d.",
                         bit, i)
            end
          end
          if next (seq) then
            logreport ("both", 2, "conf",
                       "overriding anon lookup sequence %s.",
                       tableconcat (seq, ","))
            return seq
          end
        end
        logreport ("both", 0, "conf", "no lookup locations enabled, falling back to default precedence list")
        return default_location_precedence
      end
    },
    scan_local   = { in_t = boolean_t, },
    skip_read    = { in_t = boolean_t, },
    strip        = { in_t = boolean_t, },
    update_live  = { in_t = boolean_t, },
    compress     = { in_t = boolean_t, },
    max_fonts    = {
      in_t      = number_t,
      out_t     = number_t, --- TODO int_t from 5.3.x on
      transform = tointeger,
    },
    designsize_dimen = {
      in_t  = string_t,
      out_t = string_t,
    },
  },
  run = {
    anon_sequence = {
      in_t      = string_t,
      out_t     = table_t,
      transform = function (s)
        if s ~= "default" then
          local bits = { lpegmatch (commasplitter, s) }
          if next (bits) then
            local seq = { }
            local done = { }
            for i = 1, #bits do
              local bit = bits [i]
              if valid_resolvers [bit] then
                if not done [bit] then
                  done [bit] = true
                  seq [#seq + 1] = bit
                else
                  logreport ("both", 0, "conf",
                             "ignoring duplicate resolver %s at position %d \z
                              in lookup sequence",
                             bit, i)
                end
              else
                logreport ("both", 0, "conf",
                           "lookup sequence contains invalid item %s \z
                            at position %d.",
                           bit, i)
              end
            end
            if next (seq) then
              logreport ("both", 2, "conf",
                         "overriding anon lookup sequence %s.",
                         tableconcat (seq, ","))
              return seq
            end
          end
        end
        return default_anon_sequence
      end
    },
    live = { in_t = boolean_t, }, --- false for the tool, true for TeX run
    resolver = {
      in_t      = string_t,
      out_t     = string_t,
      transform = function (r) return r == "normal" and r or "cached" end,
    },
    definer = {
      in_t      = string_t,
      out_t     = string_t,
      transform = function (d)
        if   d == "generic"      or d == "patch"
          or d == "info_generic" or d == "info_patch"
        then
          return d
        end
        return "patch"
      end,
    },
    fontloader = {
      in_t      = string_t,
      out_t     = string_t,
      transform = function (id)
        local ldr = pick_fontloader (id)
        if ldr ~= nil then
          return ldr
        end
        logreport ("log", 0, "conf",
                    "Requested fontloader %q not defined, "
                    .. "use at your own risk.",
                    id)
        return id
      end,
    },
    log_level = {
      in_t      = number_t,
      out_t     = number_t, --- TODO int_t from 5.3.x on
      transform = tointeger,
    },
    color_callback = {
      in_t      = string_t,
      out_t     = string_t,
      transform = function (cb_spec)
        --- These are the two that make sense.
        local cb = permissible_color_callbacks[cb_spec]
        if cb then
          logreport ("log", 3, "conf",
                     "Using callback %q for font colorization.",
                     cb)
          return cb
        end
        logreport ("log", 0, "conf",
                    "Requested callback identifier %q invalid, "
                    .. "falling back to default.",
                    cb_spec)
        return permissible_color_callbacks.default
      end,
    },
    default_dvi_driver = {
      in_t      = string_t,
      out_t     = string_t,
      transform = function (driver)
        local mapped = known_dvi_drivers[driver]
        if mapped then
          logreport ("log", 5, "conf",
                     "Using default DVI driver %q if used in DVI mode.",
                     mapped)
          return mapped
        end
        logreport ("log", 0, "conf",
                    "Requested default DVI driver %q invalid, "
                    .. "falling back to dvisvgm.",
                    driver)
        return known_dvi_drivers.dvisvgm
      end,
    },
  },
  misc = {
    bisect          = { in_t = boolean_t, }, --- doesn’t make sense in a config file
    version         = { in_t = string_t,  },
    statistics      = { in_t = boolean_t, },
    termwidth       = {
      in_t      = number_t,
      out_t     = number_t,
      transform = function (w)
        w = tointeger (w)
        if w < min_terminal_width then
          return min_terminal_width
        end
        return w
      end,
    },
    keepnames       = { in_t = boolean_t, },
  },
  paths = {
    names_dir           = { in_t = string_t, },
    cache_dir           = { in_t = string_t, },
    index_file          = { in_t = string_t, },
    lookups_file        = { in_t = string_t, },
    lookup_path_lua     = { in_t = string_t, },
    lookup_path_luc     = { in_t = string_t, },
    index_path_lua      = { in_t = string_t, },
    index_path_luc      = { in_t = string_t, },
  },
  default_features = {
    __default = { in_t  = string_t, out_t = table_t, transform = tohash, },
  },
}

-------------------------------------------------------------------------------
---                               FORMATTERS
-------------------------------------------------------------------------------

local function commented (str)
  return ";" .. str
end

local underscore_replacer = lpeg.replacer ("_", "-", true)

local function dashed (var)
  --- INI spec dictates that dashes are valid in variable names, not
  --- underscores.
  return underscore_replacer (var) or var
end

local indent = "  "
local function format_string (var, val)
  return stringformat (indent .. "%s = %s", var, val)
end

local function format_integer (var, val)
  return stringformat (indent .. "%s = %d", var, val)
end

local function format_boolean (var, val)
  return stringformat (indent .. "%s = %s", var, val == true and "true" or "false")
end

local function format_keyval (var, val)
  local list = { }
  local keys = table.sortedkeys (val)
  for i = 1, #keys do
    local key    = keys[i]
    local subval = val[key]
    if subval == true then
      list[#list + 1] = stringformat ("%s", key)
    else
      list[#list + 1] = stringformat ("%s=%s", key, val[key])
    end
  end
  if next (list) then
    return stringformat (indent .. "%s = %s",
                         var,
                         tableconcat (list, ","))
  end
end

local function format_list (var, val)
  local elts = { }
  for i = 1, #val do elts [i] = val [i] end
  if next (elts) then
    return stringformat (indent .. "%s = %s",
                         var, tableconcat (elts, ","))
  end
end

local function format_section (title)
  return stringformat ("[%s]", dashed (title))
end

local conf_header = [==[
;;-----------------------------------------------------------------------------
;; Luaotfload Configuration
;;-----------------------------------------------------------------------------
;;
;; This file was generated by luaotfload-tool
;; on %s. Configuration variables
;; are documented in the manual to luaotfload.conf(5).
;;
;;-----------------------------------------------------------------------------

]==]

local conf_footer = [==[

;; vim:filetype=dosini:expandtab:shiftwidth=2
]==]

--- Each dumpable variable (the ones mentioned in the man page) receives a
--- formatter that will be used in dumping the variable. Each value receives a
--- “commented” flag that indicates whether or not the line should be printed
--- as a comment.

local formatters = {
  db = {
    compress         = { false, format_boolean },
    designsize_dimen = { false, format_string  },
    formats          = { false, format_string  },
    max_fonts        = { false, format_integer },
    scan_local       = { false, format_boolean },
    skip_read        = { false, format_boolean },
    strip            = { false, format_boolean },
    update_live      = { false, format_boolean },
  },
  default_features = {
    __default = { true, format_keyval },
  },
  misc = {
    bisect     = { false, format_boolean },
    statistics = { false, format_boolean },
    termwidth  = { true , format_integer },
    version    = { true , format_string  },
  },
  paths = {
    cache_dir    = { false, format_string },
    names_dir    = { false, format_string },
    index_file   = { false, format_string },
    lookups_file = { false, format_string },
  },
  run = {
    anon_sequence      = { false, format_list    },
    color_callback     = { false, format_string  },
    definer            = { false, format_string  },
    fontloader         = { true,  format_string  },
    log_level          = { false, format_integer },
    resolver           = { false, format_string  },
    default_dvi_driver = { false, format_string  },
  },
}

-------------------------------------------------------------------------------
---                           MAIN FUNCTIONALITY
-------------------------------------------------------------------------------

--[[doc--

  tilde_expand -- Rudimentary tilde expansion; covers just the “substitute ‘~’
  by the current users’s $HOME” part.

--doc]]--

local function tilde_expand (p)
  if #p > 2 then
    if stringsub (p, 1, 2) == "~/" then
      local homedir = osgetenv "HOME"
      if homedir and lfsisdir (homedir) then
        p = filejoin (homedir, stringsub (p, 3))
      end
    end
  end
  return p
end

local function resolve_config_path ()
  for i = 1, #config_paths do
    local t, p = unpack (config_paths[i])
    local fullname
    if t == kpse_t then
      fullname = kpse.find_file (p)
      logreport ("both", 6, "conf", "kpse lookup: %s -> %s.", p, fullname)
    elseif t == path_t then
      local expanded = tilde_expand (p)
      if lfsisfile (expanded) then
        fullname = expanded
      end
      logreport ("both", 6, "conf", "path lookup: %s -> %s.", p, fullname)
    end
    if fullname then
      logreport ("both", 3, "conf", "Reading configuration file at %q.", fullname)
      return fullname
    end
  end
  logreport ("both", 2, "conf", "No configuration file found.")
  return false
end

local function add_config_paths (t)
  if not next (t) then
    return
  end
  local result = { }
  for i = 1, #t do
    local path = t[i]
    result[#result + 1] = { path_t, path }
  end
  config_paths = tableappend (result, config_paths)
end

local process_options = function (opts)
  local new = { }
  for i = 1, #opts do
    local section = opts[i]
    local title = section.section.title
    local vars  = section.variables

    if not title then --- trigger warning: arrow code ahead
      logreport ("both", 2, "conf", "Section %d lacks a title; skipping.", i)
    elseif not vars then
      logreport ("both", 2, "conf", "Section %d (%s) lacks a variable section; skipping.", i, title)
    else
      local spec = option_spec[title]
      if not spec then
        logreport ("both", 2, "conf", "Section %d (%s) unknown; skipping.", i, title)
      else
        local newsection = new[title]
        if not newsection then
          newsection = { }
          new[title] = newsection
        end

        for var, val in next, vars do
          local vspec = spec[var] or spec.__default
          local t_val = type (val)
          if not vspec then
            logreport ("both", 2, "conf",
                       "Section %d (%s): invalid configuration variable %q (%q); ignoring.",
                       i, title,
                       var, tostring (val))
          elseif t_val ~= vspec.in_t then
            logreport ("both", 2, "conf",
                       "Section %d (%s): type mismatch of input value %q (%q, %s != %s); ignoring.",
                       i, title,
                       var, tostring (val), t_val, vspec.in_t)
          else --- type matches
            local transform = vspec.transform
            if transform then
              local dval
              local t_transform = type (transform)
              if t_transform == function_t then
                dval = transform (val)
              elseif t_transform == table_t then
                dval = transform[val]
              end
              if dval then
                local out_t = vspec.out_t
                if out_t then
                  local t_dval = type (dval)
                  if t_dval == out_t then
                    newsection[var] = dval
                  else
                    logreport ("both", 2, "conf",
                               "Section %d (%s): type mismatch of derived value of %q (%q, %s != %s); ignoring.",
                               i, title,
                               var, tostring (dval), t_dval, out_t)
                  end
                else
                  newsection[var] = dval
                end
              else
                logreport ("both", 2, "conf",
                           "Section %d (%s): value of %q could not be derived via %s from input %q; ignoring.",
                           i, title, var, t_transform, tostring (val))
              end
            else --- insert as is
              newsection[var] = val
            end
          end
        end
      end
    end
  end
  return new
end

local function apply (old, new)
  if not new then
    if not old then
      return false
    end
    return tablecopy (old)
  elseif not old then
    return tablecopy (new)
  end
  local result = tablecopy (old)
  for name, section in next, new do
    local t_section = type (section)
    if t_section ~= table_t then
      logreport ("both", 1, "conf",
                 "Error applying configuration: entry %s is %s, expected table.",
                 section, t_section)
      --- ignore
    else
      local currentsection = result[name]
      for var, val in next, section do
        currentsection[var] = val
      end
    end
  end
  return result
end

local function reconfigure()
  for i = 1, #reconf_tasks do
    local name, task = unpack (reconf_tasks[i])
    logreport ("both", 3, "conf", "Launch post-configuration task %q.", name)
    if not task () then
      logreport ("both", 0, "conf", "Post-configuration task %q failed.", name)
      return false
    end
  end
  return true
end

local function read (extra)
  if extra then
    add_config_paths (extra)
  end

  local readme = resolve_config_path ()
  if readme == false then
    logreport ("both", 2, "conf", "No configuration file.")
    return false
  end

  local raw = ioloaddata (readme)
  if not raw then
    logreport ("both", 2, "conf", "Error reading the configuration file %q.", readme)
    return false
  end

  local parsed = lpegmatch (config_parser, raw)
  if not parsed then
    logreport ("both", 2, "conf", "Error parsing configuration file %q.", readme)
    return false
  end

  local ret, msg = process_options (parsed)
  if not ret then
    logreport ("both", 2, "conf", "File %q is not a valid configuration file.", readme)
    logreport ("both", 2, "conf", "Error: %s", msg)
    return false
  end
  return ret
end

local function apply_defaults ()
  local defaults      = default_config
  local vars          = read ()
  --- Side-effects galore ...
  config.luaotfload   = apply (defaults, vars)
  return reconfigure ()
end

local function dump ()
  local sections = table.sortedkeys (config.luaotfload)
  local confdata = { }
  for i = 1, #sections do
    local section    = sections[i]
    local vars       = config.luaotfload[section]
    local varnames   = table.sortedkeys (vars)
    local sformats   = formatters[section]
    if sformats then
      confdata[#confdata + 1] = format_section (section)
      for j = 1, #varnames do
        local var = varnames[j]
        local val = vars[var]
        local comment, sformat = unpack (sformats[var] or { })
        if not sformat then
          comment, sformat = unpack (sformats.__default or { })
        end

        if sformat then
          local dashedvar = dashed (var)
          if comment then
            confdata[#confdata + 1] = commented (sformat (dashedvar, val))
          else
            confdata[#confdata + 1] = sformat (dashedvar, val)
          end
        end

      end
      confdata[#confdata + 1] = ""
    end
  end
  if next(confdata) then
    iowrite (stringformat (conf_header,
                           osdate ("%Y-%m-%d %H:%M:%S", os.time ())))
    iowrite (tableconcat (confdata, "\n"))
    iowrite (conf_footer)
  end
end

-------------------------------------------------------------------------------
---                                 EXPORTS
-------------------------------------------------------------------------------

return function ()
  config.luaotfload = { }
  logreport         = luaotfload.log.report
  local parsers     = luaotfload.parsers
  config_parser     = parsers.config
  stripslashes      = parsers.stripslashes

  luaotfload.default_config = default_config
  config.actions = {
    read             = read,
    apply            = apply,
    apply_defaults   = apply_defaults,
    reconfigure      = reconfigure,
    dump             = dump,
  }
  if not apply_defaults () then
    logreport ("log", 0, "load",
               "Configuration unsuccessful: error loading default settings.")
  end
  return true
end
