/* pk-settings.pk - Global settings.  */

/* Copyright (C) 2021, 2022, 2023 Jose E. Marchesi */

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/* This file implements the infrastructure for maintining a global
   database of settings in the poke application.  The .set dot-command
   operates in a generic way on the structures defined here.  */


/* The Poke_Setting type describes a poke global setting.

   NAME is a string identifying the setting.  Please choose reasonably
   short names.  When adding compound words, like "pretty-print",
   please use dash characters and not underline characters.

   KIND identifies the nature of the setting.

   SUMMARY is a one line string with a terse description of the
   setting.  It is used by the help subsystem.

   DESCRIPTION is a long description of the global setting,
   multi-line, and preferably showing some usage example.  It is used
   by the help subsystem.

   GETTER is a function that returns the current value of the global
   setting.

   SETTER is a function that, given a value, tries to set it as the
   new value for the global setting.  If the operation success, the
   function returns a value different than zero.  Otherwise it must
   return zero.  The setters can assume that the given value is of the
   right type according to the kind.

   Note how Poke_Setting doesn't provide a recipient to store the
   value of the global setting itself: it is up to the getter and
   setter to do provide it if needed (via a lexical closure, see
   examples below.)  */


var POKE_SETTING_INT = 0;
var POKE_SETTING_BOOL = 1;
var POKE_SETTING_STR = 2;

type Poke_Setting =
  struct
    {
      fun is_line_p
        = (string s) int<32>: { return strchr (s, '\n') == s'length; }

      string name;
      int kind : kind in [POKE_SETTING_INT,
                          POKE_SETTING_BOOL,
                          POKE_SETTING_STR];
      string summary : is_line_p (summary);
      string usage : is_line_p (usage);
      string description;
      ()any getter;
      (any)int setter;
    };

/* The global settings registry.  */

type Poke_Setting_Registry =
  struct
    {
      Poke_Setting[] entries;

      /* Search for a given setting by name.

         If the given name doesn't identify a setting in pk_settings,
         raise E_inval.  */

      method search = (string name) Poke_Setting:
        {
          for (e in entries)
            if (e.name == name)
              return e;
          raise E_inval;
        }

      /* Add a new global setting to the registry.

         If a setting with the given name already exists in the
         registry, raise E_inval.  */

      method add_setting = (Poke_Setting entry) void:
        {
          try search (entry.name);
          catch if E_inval {
            entries += [entry];
            return;
          }

          raise E_inval;
        }

      /* Get the value of a global setting given its name.  */

      method get = (string name) any:
      {
        var s = search (name);
        var val = s.getter;

        if (s.kind == POKE_SETTING_BOOL)
          val = val as int<32> ? "yes" : "no";

        return val;
      }

      /* Set the value of a global setting given its name.

         If the provided name doesn't name a setting, or the given
         value is not of the right kind for the setting, raise
         E_inval.

         If the provided value doesn't satisfy the constraints
         enforced by the setting, an explicative error message is
         returned.

         If the value is successfully set as the new value of the
         global setting, return an empty string.  */

      method set = (string name, any val) string:
      {
        var s = search (name);

        if (s.kind == POKE_SETTING_BOOL)
          {
            if (!(val isa string))
              raise E_inval;

            if (!(val as string in ["yes", "no"]))
              return "Usage: .set " + s.name + " {yes,no}";

            val = (val as string == "yes");
          }

        try
          return s.setter (val) ? "" : "Usage: " + s.usage;
        catch if E_conv { raise E_inval; }
      }
    };

/* Create the registry.  */

var pk_settings = Poke_Setting_Registry {};

pk_settings.add_setting
  :entry Poke_Setting {
           name = "pretty-print",
           kind = POKE_SETTING_BOOL,
           summary = "whether poke should use struct pretty-printers",
           usage = ".set pretty-print {yes,no}",
           description = "\
This setting determines whether poke should use pretty-printers while
printing out the written representation of struct values.

Pretty-printers are defined as methods named `_print'.",
           getter = lambda any:
             {
               return vm_opprint;
             },
           setter = lambda (any val) int:
             {
               vm_set_opprint (val as int<32>);
               return 1;
             }
         };

pk_settings.add_setting
  :entry Poke_Setting {
      name = "auto-map",
      kind = POKE_SETTING_BOOL,
      summary = "whether poke auto-loads maps when opening IO spaces",
      usage = ".set auto-map {yes,no}",
      description = "\
This setting determines whether to automatically load map files when
opening IO spaces.

The decision of what maps to load is driven by the contents of the
array `auto_map'.  Each entry in the array is an array whose first
element is a regular expression, and the second element is the name of
a map.

Example:

  auto_map = [[\"\\.o\", \"elf\"], [\"\\.o\", \"dwarf\"]];

The entry above makes poke to automatically load the maps `elf.map'
and `dwarf.map' when opening any file whose name ends in `.o',
provided `auto-map' is set to `yes'.",
      getter = lambda any: { return pk_auto_map_p; },
      setter = lambda (any val) int:
        {
          pk_auto_map_p = val as int<32>;
          return 1;
        }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "prompt-commented",
    kind = POKE_SETTING_BOOL,
    summary = "whether the interactive prompt should be shebang-commented",
    usage = ".set prompt-commented {yes,no}",
    description = "\
This setting determines whether the prompt is a shebang-comment, allowing
for easy copy-paste of full terminal lines because the prompt will be
a syntactic no-op.

Example of a prompt with prompt-commented enabled:

  #!!# 1+2;
  3

Example of a prompt with prompt-commented, map information, and `prompt-maps' set to
`yes':

  #!!# .file foo.o
  #![dwarf,elf]!# 1+2;
  3
  #![dwarf,elf]!#

",
    getter = lambda any: { return pk_prompt_commented_p; },
    setter = lambda (any val) int:
      {
        pk_prompt_commented_p = val as int<32>;
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "prompt-maps",
    kind = POKE_SETTING_BOOL,
    summary = "whether map information should be included in the prompt",
    usage = ".set prompt-maps {yes,no}",
    description = "\
This setting determines whether a list of open maps is displayed
before the poke prompt.

Example of a prompt with map information and `prompt-maps' set to
`yes':

  (poke) .file foo.o
  [dwarf,elf](poke)

Which indicates that the opening of the IO space `foo.o' resulted in
the `elf.map' and `dwarf.map' maps to be automatically loaded.",
    getter = lambda any: { return pk_prompt_maps_p; },
    setter = lambda (any val) int:
      {
        pk_prompt_maps_p = val as int<32>;
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "omaps",
    kind = POKE_SETTING_BOOL,
    summary = "whether include offset information when printing values",
    usage = ".set omaps {yes,no}",
    description = "\
This sort of misnamed setting determines whether to show the mapped
offsets and element/field offsets of composite values like arrays and
structs.

Example:

  (poke) .set omaps yes
  (poke) [1,2,3]
  [0x1 @ 0x0UL#b,0x2 @ 0x20UL#b,0x3 @ 0x40UL#b] @ 0x0UL#b

This setting defaults to `no'.",
    getter = lambda any: { return vm_omaps; },
    setter = lambda (any val) int:
      {
        vm_set_omaps (val as int<32>);
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
           name = "obase",
           kind = POKE_SETTING_INT,
           summary = "numerical base to use when printing integral values",
           usage = ".set obase {2,8,10,16}",
           description = "\
This setting determines the output numerical base to use when printing
integral values like integers, offset magnitudes, and the like.  The
supported bases are 2 for binary, 8 for octal, 10 for decimal and 16
for hexadecimal.",
           getter = lambda any:
             {
               return vm_obase;
             },
           setter = lambda (any v) int:
             {
               var obase = v as int<32>;

               try vm_set_obase (obase);
               catch if E_inval {
                 return 0;
               }
               return 1;
             }
         };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "oacutoff",
    kind = POKE_SETTING_INT,
    summary = "cutoff value used when printing array elements",
    usage = ".set oacutoff INTEGER",
    description = "\
This setting determines the maximum number of elements to output when
printing out array values.  Once the maximum number of elements is
reached, an ellipsis `...' is printed to represent the rest of the
elements.

Example:

  (poke) [1,2,3,4,5]
  [1,2,3,4,5]
  (poke) .set oacutoff 3
  [1,2,3,...]

The default value is 0, which means no limit.",
    getter = lambda any: { return vm_oacutoff; },
    setter = lambda (any val) int:
      {
        vm_set_oacutoff (val as int<32>);
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "odepth",
    kind = POKE_SETTING_INT,
    summary = "maximum depth to include when printing nested structures",
    usage = ".set odepth INTEGER",
    description = "\
This setting determines the maximum number of nested structs to output
when printing struct and union values.  Once the maximum number of
nested values is reached, a collapsed form `Type {...}' is printed.

Example:

  (poke) type Foo = struct { struct { int i; } bar; }
  (poke) Foo {}
  Foo {
    bar=struct {...}
  }

The default value for `odepth' is 0, which means infinite depth.",
    getter = lambda any: { return vm_odepth; },
    setter = lambda (any val) int:
      {
        vm_set_odepth (val as int<32>);
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "oindent",
    kind = POKE_SETTING_INT,
    summary = "width of each indentation leveel when printing structures",
    usage = ".set oindent INTEGER",
    description = "\
This setting determines the width (in blank characters) of each
indentation level when printing out struct values in `tree' mode.

Example:

  (poke) .set oindent 4
  (poke) Foo {}
  Foo {
      i=0x0,
      l=0x0L
  }

This setting defaults to 2.
See also \".help omode\".",
    getter = lambda any: { return vm_oindent; },
    setter = lambda (any val) int:
      {
        vm_set_oindent (val as int<32>);
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "omode",
    kind = POKE_SETTING_STR,
    summary = "output mode for printing values",
    usage = ".set omode {flat,tree}",
    description = "\
This setting determines the way binary struct data is displayed.

In `flat' mode data is not formatted in any special way.  Each value,
be it simple or composite, is printed on one line.

In `tree' mode composite values like structs and arrays are displayed
in an hierarchical way, using newlines and indentation.

Examples:

  (poke) .set omode flat
  (poke) Foo {}
  Foo {i=0x0,l=0x0L}

  (poke) .set omode tree
  (poke) Foo {}
  Foo {
    i=0x0,
    l=0x0L
  }

The default value of `omode' is `flat'.",
    getter = lambda any:
      {
        var omode = vm_omode;

        if (omode == VM_OMODE_PLAIN)
          return "flat";
        else if (omode == VM_OMODE_TREE)
          return "tree";

        assert (0, "this point shouldn't be reached");
      },
    setter = lambda (any val) int:
      {
        var s = val as string;

        if (s == "flat")
          vm_set_omode (VM_OMODE_PLAIN);
        else if (s == "tree")
          vm_set_omode (VM_OMODE_TREE);
        else
          return 0;

        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
           name = "endian",
           kind = POKE_SETTING_STR,
           summary = "endianness used while accessing the IO spaces",
           usage = ".set endian {little,big,host,network}",
           description = "\
This setting determines the byte endianness used when accessing IO
spaces.  Valid values are `big', `little', `host' and `network'.

The meaning of `big' and `little' is the obvious one: the endianness
is set to big-endian and little-endian respectively.

The `host' endianness is the endianness used by the computer and
operating system that is running poke in order to handle it's own
memory and files.

The `network' endianness is the endianness used by the host machine in
order to communicate with the network.",
           getter = lambda any:
             {
               if (get_endian == ENDIAN_BIG)
                  return "big";
               else
                  return "little";
             },
           setter = lambda (any v) int:
             {
               var s = v as string;
               var endian = ENDIAN_LITTLE;

               if (s == "big")
                 endian = ENDIAN_BIG;
               else if (s == "little")
                 endian = ENDIAN_LITTLE;
               else if (s == "host")
                 endian = pk_host_endian;
               else if (s == "network")
                 endian = pk_network_endian;
               else
                 return 0;

               set_endian (endian);
               return 1;
             }
         };

pk_settings.add_setting
  :entry Poke_Setting {
    name = "doc-viewer",
    kind = POKE_SETTING_STR,
    summary = "program to show document from the manual",
    usage = ".set doc-viewer {info,less}",
    description = "\
This setting determines what mechanism to use to open the online user
manual.

If `info' is selected, the GNU info standalone program is launched to
show the info version of the user manual.

If `less' is selected, the less utility is launched ot show a plain
text version of the user manual.

See also \".help .doc\".",
    getter = lambda any: { return pk_doc_viewer; },
    setter = lambda (any val) int:
      {
        var s = val as string;

        if (s != "info" && s != "less")
          return 0;

        pk_doc_viewer = s;
        return 1;
      }
    };

pk_settings.add_setting
  :entry Poke_Setting {
      name = "pager",
      kind = POKE_SETTING_BOOL,
      summary = "whether to use the terminal pager",
      usage = ".set pager {yes,no}",
      description = "\
This setting determines whether the output of poke commands will
be paged or not.  Each page will have the height of the terminal.

This setting is `no' by default.",
    getter = lambda any: { return pk_pager_p; },
    setter = lambda (any val) int:
      {
        pk_pager_p = val as int<32>;
        return 1;
      }
   };

pk_settings.add_setting
  :entry Poke_Setting {
      name = "tracer",
      kind = POKE_SETTING_BOOL,
      summary = "whether to activate the Poke tracer",
      usage = ".set tracer {yes,no}",
      description = "\
This setting determines whether the Poke compiler and runtime
will generate tracing events or not.  This is mainly useful
for debugging puposes.

Beware that enabling tracer will undoubtly have an impact
in performance, and that every type compiled when tracing is
enabled will contain instrumentalization.  This means you
will most likely want to re-compile the types with tracing
disabled once the debugging is done.

This setting is `no' by default.",
      getter = lambda any: { return pk_tracer_p; },
      setter = lambda (any val) int:
        {
          pk_tracer_p = val as int<32>;
          return 1;
        }
    };

pk_settings.add_setting
  :entry Poke_Setting {
      name = "autoremap",
      kind = POKE_SETTING_BOOL,
      summary = "whether to activate auto-remap mode",
      usage = ".set autoremap {yes,no}",
      description = "\
This setting determines whether poke will remap mapped values
automatically when they are referred to.  This assures that the values
are always up to date with respect the contents of the IO space.

Turning auto-remap off may be useful in situations where it is very
slow to keep all the data structures up to date.  In these case, you
can `.set autoremap off' and then use the `remap' Poke operator to
force the re-mapping of any given value, explicitly.

This setting is `yes' by default.",
      getter = lambda any: { return vm_autoremap; },
      setter = lambda (any val) int:
        {
          vm_set_autoremap (val as int<32>);
          return 1;
        }
    };

/* Create help topics for the global settings defined above.  */

for (setting in pk_settings.entries)
  pk_help_add_topic
    :entry Poke_HelpEntry {
             category = "settings",
             topic = setting.name,
             summary = setting.summary,
             synopsis = setting.usage,
             description = setting.description
           };

/* Create the help topic for error-on-warning, which is handled in C
   only and therefore is not in the settings registry.  */

pk_help_add_topic
  :entry Poke_HelpEntry {
           category = "settings",
           topic = "error-on-warning",
           summary = "emit errors instead of warnings",
           synopsis = ".set error-on-warning {yes,no}",
           description = "\
This setting determines whether the poke incremental compiler should
turn warnings into errors.

This setting is `no' by default.",
    };

/* Dump current settings.  */

fun pk_settings_dump = void:
  {
    var table = Pk_Table { num_columns = pk_hserver_p ? 3 : 2 };

    /* Table header.  */
    table.row ("table-header");
    table.column ("Setting");
    table.column ("Value");
    if (pk_hserver_p) table.column ("");

    /* Add a table row for each setting.  */
    for (setting in pk_settings.entries)
      {
        table.row;
        table.column (setting.name, "",
                      hserver_make_hyperlink ('e', ".help " + setting.name));
        table.column (setting.kind == POKE_SETTING_INT
                      ? format ("%i32d", setting.getter as int)
                      : setting.kind == POKE_SETTING_BOOL
                      ? setting.getter as int ? "yes" : "no"
                      : setting.kind == POKE_SETTING_STR
                      ? setting.getter as string
                      : "<unknown>",
                      setting.kind == POKE_SETTING_INT
                      ? "setting-integer"
                      : setting.kind == POKE_SETTING_BOOL
                      ? "setting-boolean"
                      : setting.kind == POKE_SETTING_STR
                      ? "setting-string"
                      : "");

        /* Add a toolbar if the hyperserver is running.  */
        if (pk_hserver_p)
        {
          if (setting.kind == POKE_SETTING_BOOL)
          {
            var setting_name = setting.name;
            var setting_getter = (setting.getter);
            var setting_setter = (setting.setter);
            fun toggle_setting = void:
              {
                var newval = !(setting_getter as int);

                print ("Toggling setting " + setting_name
                       + " to " + (newval ? "`yes'" : "`no'") + "\n");
                setting_setter (!(setting_getter as int));
              }

            table.column ("[toggle]", "",
                          hserver_make_hyperlink ('c', "", (toggle_setting)));
          }
          else
            table.column ("");
        }
      }

    table.print_table;
  }
