/* readval.c
 * Routines to read a prefix or number from the current input file
 * Copyright (C) 1991-2025 Olly Betts
 *
 * 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 2 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
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <limits.h>
#include <stddef.h> /* for offsetof */

#include "cavern.h"
#include "commands.h" /* For match_tok(), etc */
#include "date.h"
#include "debug.h"
#include "filename.h"
#include "message.h"
#include "readval.h"
#include "datain.h"
#include "netbits.h"
#include "osalloc.h"
#include "str.h"

int root_depr_count = 0;

static prefix *
new_anon_station(void)
{
    prefix *name = osnew(prefix);
    name->pos = NULL;
    name->ident.p = NULL;
    name->stn = NULL;
    name->up = pcs->Prefix;
    name->down = NULL;
    name->filename = file.filename;
    name->line = file.line;
    name->column = 0;
    name->min_export = name->max_export = 0;
    name->sflags = BIT(SFLAGS_ANON);
    /* Keep linked list of anon stations for node stats. */
    name->right = anon_list;
    anon_list = name;
    return name;
}

static char *id = NULL;
static size_t id_len = 0;

/* if prefix is omitted: if PFX_OPT set return NULL, otherwise use longjmp */
extern prefix *
read_prefix(unsigned pfx_flags)
{
   bool f_optional = !!(pfx_flags & PFX_OPT);
   bool fSurvey = !!(pfx_flags & PFX_SURVEY);
   bool fSuspectTypo = !!(pfx_flags & PFX_SUSPECT_TYPO);
   prefix *back_ptr, *ptr;
   size_t i;
   bool fNew;
   bool fImplicitPrefix = true;
   int depth = -1;
   filepos here;
   filepos fp_firstsep;

   skipblanks();
   get_pos(&here);
#ifndef NO_DEPRECATED
   if (isRoot(ch)) {
      if (!(pfx_flags & PFX_ALLOW_ROOT)) {
	 compile_diagnostic(DIAG_ERR|DIAG_COL, /*ROOT is deprecated*/25);
	 longjmp(jbSkipLine, 1);
      }
      if (root_depr_count < 5) {
	 compile_diagnostic(DIAG_WARN|DIAG_COL, /*ROOT is deprecated*/25);
	 if (++root_depr_count == 5)
	    compile_diagnostic(DIAG_INFO, /*Further uses of this deprecated feature will not be reported*/95);
      }
      nextch();
      ptr = root;
      if (!isNames(ch)) {
	 if (!isSep(ch)) return ptr;
	 /* Allow optional SEPARATOR after ROOT */
	 get_pos(&fp_firstsep);
	 nextch();
      }
      fImplicitPrefix = false;
#else
   if (0) {
#endif
   } else {
      if ((pfx_flags & PFX_ANON) &&
	  (isSep(ch) || (pcs->dash_for_anon_wall_station && ch == '-'))) {
	 int first_ch = ch;
	 nextch();
	 if (isBlank(ch) || isComm(ch) || isEol(ch)) {
	    if (!isSep(first_ch))
	       goto anon_wall_station;
	    /* A single separator alone ('.' by default) is an anonymous
	     * station which is on a point inside the passage and implies
	     * the leg to it is a splay.
	     */
	    if (TSTBIT(pcs->flags, FLAGS_ANON_ONE_END)) {
	       set_pos(&here);
	       compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Can't have a leg between two anonymous stations*/47);
	       longjmp(jbSkipLine, 1);
	    }
	    pcs->flags |= BIT(FLAGS_ANON_ONE_END) | BIT(FLAGS_IMPLICIT_SPLAY);
	    return new_anon_station();
	 }
	 if (isSep(first_ch) && ch == first_ch) {
	    nextch();
	    if (isBlank(ch) || isComm(ch) || isEol(ch)) {
	       /* A double separator ('..' by default) is an anonymous station
		* which is on the wall and implies the leg to it is a splay.
		*/
	       prefix * pfx;
anon_wall_station:
	       if (TSTBIT(pcs->flags, FLAGS_ANON_ONE_END)) {
		  set_pos(&here);
		  compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Can't have a leg between two anonymous stations*/47);
		  longjmp(jbSkipLine, 1);
	       }
	       pcs->flags |= BIT(FLAGS_ANON_ONE_END) | BIT(FLAGS_IMPLICIT_SPLAY);
	       pfx = new_anon_station();
	       pfx->sflags |= BIT(SFLAGS_WALL);
	       return pfx;
	    }
	    if (ch == first_ch) {
	       nextch();
	       if (isBlank(ch) || isComm(ch) || isEol(ch)) {
		  /* A triple separator ('...' by default) is an anonymous
		   * station, but otherwise not handled specially (e.g. for
		   * a single leg down an unexplored side passage to a station
		   * which isn't refindable).
		   */
		  if (TSTBIT(pcs->flags, FLAGS_ANON_ONE_END)) {
		     set_pos(&here);
		     compile_diagnostic(DIAG_ERR|DIAG_WORD, /*Can't have a leg between two anonymous stations*/47);
		     longjmp(jbSkipLine, 1);
		  }
		  pcs->flags |= BIT(FLAGS_ANON_ONE_END);
		  return new_anon_station();
	       }
	    }
	 }
	 set_pos(&here);
      }
      ptr = pcs->Prefix;
   }

   i = 0;
   do {
      fNew = false;
      if (id == NULL) {
	 /* Allocate buffer the first time */
	 id_len = 256;
	 id = osmalloc(id_len);
      }
      /* i==0 iff this is the first pass */
      if (i) {
	 i = 0;
	 nextch();
      }
      while (isNames(ch)) {
	 if (i < pcs->Truncate) {
	    /* truncate name */
	    id[i++] = (pcs->Case == LOWER ? tolower(ch) :
		       (pcs->Case == OFF ? ch : toupper(ch)));
	    if (i >= id_len) {
	       id_len *= 2;
	       id = osrealloc(id, id_len);
	    }
	 }
	 nextch();
      }
      if (isSep(ch)) {
	 fImplicitPrefix = false;
	 get_pos(&fp_firstsep);
      }
      if (i == 0) {
	 if (!f_optional) {
	    if (isEol(ch)) {
	       if (fSurvey) {
		  compile_diagnostic(DIAG_ERR|DIAG_COL, /*Expecting survey name*/89);
	       } else {
		  compile_diagnostic(DIAG_ERR|DIAG_COL, /*Expecting station name*/28);
	       }
	    } else {
	       /* TRANSLATORS: Here "station" is a survey station, not a train station. */
	       compile_diagnostic(DIAG_ERR|DIAG_COL, /*Character “%c” not allowed in station name (use *SET NAMES to set allowed characters)*/110, ch);
	    }
	    longjmp(jbSkipLine, 1);
	 }
	 return (prefix *)NULL;
      }

      id[i++] = '\0';

      back_ptr = ptr;
      ptr = ptr->down;
      if (ptr == NULL) {
	 /* Special case first time around at each level */
	 ptr = osnew(prefix);
	 ptr->sflags = BIT(SFLAGS_SURVEY);
	 if (i <= sizeof(ptr->ident.i)) {
	     memcpy(ptr->ident.i, id, i);
	     ptr->sflags |= BIT(SFLAGS_IDENT_INLINE);
	 } else {
	     char *new_id = osmalloc(i);
	     memcpy(new_id, id, i);
	     ptr->ident.p = new_id;
	 }
	 ptr->right = ptr->down = NULL;
	 ptr->pos = NULL;
	 ptr->stn = NULL;
	 ptr->up = back_ptr;
	 ptr->filename = file.filename;
	 ptr->line = file.line;
	 ptr->column = here.offset - file.lpos;
	 ptr->min_export = ptr->max_export = 0;
	 if (fSuspectTypo && !fImplicitPrefix)
	    ptr->sflags |= BIT(SFLAGS_SUSPECTTYPO);
	 back_ptr->down = ptr;
	 fNew = true;
      } else {
	 /* Use caching to speed up adding an increasing sequence to a
	  * large survey */
	 static prefix *cached_survey = NULL, *cached_station = NULL;
	 prefix *ptrPrev = NULL;
	 int cmp = 1; /* result of strcmp ( -ve for <, 0 for =, +ve for > ) */
	 if (cached_survey == back_ptr) {
	    cmp = strcmp(prefix_ident(cached_station), id);
	    if (cmp <= 0) ptr = cached_station;
	 }
	 while (ptr && (cmp = strcmp(prefix_ident(ptr), id)) < 0) {
	    ptrPrev = ptr;
	    ptr = ptr->right;
	 }
	 if (cmp) {
	    /* ie we got to one that was higher, or the end */
	    prefix *newptr = osnew(prefix);
	    newptr->sflags = BIT(SFLAGS_SURVEY);
	    if (strlen(id) < sizeof(newptr->ident.i)) {
		memcpy(newptr->ident.i, id, i);
		newptr->sflags |= BIT(SFLAGS_IDENT_INLINE);
	    } else {
		char *new_id = osmalloc(i);
		memcpy(new_id, id, i);
		newptr->ident.p = new_id;
	    }
	    if (ptrPrev == NULL)
	       back_ptr->down = newptr;
	    else
	       ptrPrev->right = newptr;
	    newptr->right = ptr;
	    newptr->down = NULL;
	    newptr->pos = NULL;
	    newptr->stn = NULL;
	    newptr->up = back_ptr;
	    newptr->filename = file.filename;
	    newptr->line = file.line;
	    newptr->column = here.offset - file.lpos;
	    newptr->min_export = newptr->max_export = 0;
	    if (fSuspectTypo && !fImplicitPrefix)
	       newptr->sflags |= BIT(SFLAGS_SUSPECTTYPO);
	    ptr = newptr;
	    fNew = true;
	 }
	 cached_survey = back_ptr;
	 cached_station = ptr;
      }
      depth++;
      f_optional = false; /* disallow after first level */
      if (isSep(ch)) {
	 get_pos(&fp_firstsep);
	 if (!TSTBIT(ptr->sflags, SFLAGS_SURVEY)) {
	    /* TRANSLATORS: Here "station" is a survey station, not a train station.
	     *
	     * Here "survey" is a "cave map" rather than list of questions - it should be
	     * translated to the terminology that cavers using the language would use.
	     */
	    compile_diagnostic(DIAG_ERR|DIAG_FROM(here), /*“%s” can’t be both a station and a survey*/27,
			       sprint_prefix(ptr));
	 }
      }
   } while (isSep(ch));

   /* don't warn about a station that is referred to twice */
   if (!fNew) ptr->sflags &= ~BIT(SFLAGS_SUSPECTTYPO);

   if (fNew) {
      /* fNew means SFLAGS_SURVEY is currently set */
      SVX_ASSERT(TSTBIT(ptr->sflags, SFLAGS_SURVEY));
      if (!fSurvey) {
	 ptr->sflags &= ~BIT(SFLAGS_SURVEY);
	 if (TSTBIT(pcs->infer, INFER_EXPORTS)) ptr->min_export = USHRT_MAX;
      }
   } else {
      /* check that the same name isn't being used for a survey and station */
      if (fSurvey ^ TSTBIT(ptr->sflags, SFLAGS_SURVEY)) {
	 /* TRANSLATORS: Here "station" is a survey station, not a train station.
	  *
	  * Here "survey" is a "cave map" rather than list of questions - it should be
	  * translated to the terminology that cavers using the language would use.
	  */
	 compile_diagnostic(DIAG_ERR|DIAG_FROM(here), /*“%s” can’t be both a station and a survey*/27,
			    sprint_prefix(ptr));
      }
      if (!fSurvey && TSTBIT(pcs->infer, INFER_EXPORTS)) ptr->min_export = USHRT_MAX;
   }

   /* check the export level */
#if 0
   printf("R min %d max %d depth %d pfx %s\n",
	  ptr->min_export, ptr->max_export, depth, sprint_prefix(ptr));
#endif
   if (ptr->min_export == 0 || ptr->min_export == USHRT_MAX) {
      if (depth > ptr->max_export) ptr->max_export = depth;
   } else if (ptr->max_export < depth) {
      prefix *survey = ptr;
      char *s;
      const char *p;
      int level;
      for (level = ptr->max_export + 1; level; level--) {
	 survey = survey->up;
	 SVX_ASSERT(survey);
      }
      s = osstrdup(sprint_prefix(survey));
      p = sprint_prefix(ptr);
      if (survey->filename) {
	 compile_diagnostic_pfx(DIAG_ERR, survey,
				/*Station “%s” not exported from survey “%s”*/26,
				p, s);
      } else {
	 compile_diagnostic(DIAG_ERR, /*Station “%s” not exported from survey “%s”*/26, p, s);
      }
      free(s);
#if 0
      printf(" *** pfx %s warning not exported enough depth %d "
	     "ptr->max_export %d\n", sprint_prefix(ptr),
	     depth, ptr->max_export);
#endif
   }
   if (!fImplicitPrefix && (pfx_flags & PFX_WARN_SEPARATOR)) {
      filepos fp_tmp;
      get_pos(&fp_tmp);
      set_pos(&fp_firstsep);
      compile_diagnostic(DIAG_WARN|DIAG_COL, /*Separator in survey name*/392);
      set_pos(&fp_tmp);
   }
   return ptr;
}

char *
read_walls_prefix(void)
{
    string name = S_INIT;
    skipblanks();
    if (!isNames(ch))
	return NULL;
    do {
	s_appendch(&name, ch);
	nextch();
    } while (isNames(ch));
    return s_steal(&name);
}

prefix *
read_walls_station(char * const walls_prefix[3], bool anon_allowed, bool *p_new)
{
    if (p_new) *p_new = false;
//    bool f_optional = false; //!!(pfx_flags & PFX_OPT);
//    bool fSuspectTypo = false; //!!(pfx_flags & PFX_SUSPECT_TYPO);
//    prefix *back_ptr, *ptr;
    string component = S_INIT;
//    size_t i;
//    bool fNew;
//    bool fImplicitPrefix = true;
//    int depth = -1;
//    filepos fp_firstsep;

    filepos fp;
    get_pos(&fp);

    skipblanks();
    if (anon_allowed && ch == '-') {
	// - or -- is an anonymous wall point in a shot, but in #Fix they seem
	// to just be treated as ordinary station names.
	// FIXME: Issue warning for such a useless station?
	//
	// Not yet checked, but you can presumably use - and -- as a prefix
	// (FIXME check this).
	nextch();
	int dashes = 1;
	if (ch == '-') {
	    ++dashes;
	    nextch();
	}
	if (!isNames(ch) && ch != ':') {
	    // An anonymous station implies the leg it is on is a splay.
	    if (TSTBIT(pcs->flags, FLAGS_ANON_ONE_END)) {
		set_pos(&fp);
		// Walls also rejects this case.
		compile_diagnostic(DIAG_ERR|DIAG_TOKEN, /*Can't have a leg between two anonymous stations*/47);
		longjmp(jbSkipLine, 1);
	    }
	    pcs->flags |= BIT(FLAGS_ANON_ONE_END) | BIT(FLAGS_IMPLICIT_SPLAY);
	    prefix *pfx = new_anon_station();
	    pfx->sflags |= BIT(SFLAGS_WALL);
	    // An anonymous station is always new.
	    if (p_new) *p_new = true;
	    return pfx;
	}
	s_appendn(&component, dashes, '-');
    }

    char *w_prefix[3] = { NULL, NULL, NULL };
    int explicit_prefix_levels = 0;
    while (true) {
	while (isNames(ch)) {
	    s_appendch(&component, ch);
	    nextch();
	}
	//printf("component = '%s'\n", s_str(&component));
	if (ch == ':') {
	    nextch();

	    if (++explicit_prefix_levels > 3) {
		// FIXME Make this a proper error
		printf("too many prefix levels\n");
		s_free(&component);
		for (int i = 0; i < 3; ++i) free(w_prefix[i]);
		longjmp(jbSkipLine, 1);
	    }

	    if (!s_empty(&component)) {
		// printf("w_prefix[%d] = '%s'\n", explicit_prefix_levels - 1, s_str(&component));
		w_prefix[explicit_prefix_levels - 1] = s_steal(&component);
	    }

	    continue;
	}

	// printf("explicit_prefix_levels=%d %s:%s:%s\n", explicit_prefix_levels, w_prefix[0], w_prefix[1], w_prefix[2]);

	// component is the station name itself.
	if (s_empty(&component)) {
	    if (explicit_prefix_levels == 0) {
		compile_diagnostic(DIAG_ERR|DIAG_COL, /*Expecting station name*/28);
		s_free(&component);
		for (int i = 0; i < 3; ++i) free(w_prefix[i]);
		longjmp(jbSkipLine, 1);
	    }
	    // Walls allows an empty station name if there's an explicit prefix.
	    // This seems unlikely to be intended, so warn about it.
	    compile_diagnostic(DIAG_WARN|DIAG_COL, /*Expecting station name*/28);
	    // Use a name with a space in so it can't collide with a real
	    // Walls station name.
	    s_append(&component, "empty name");
	    static bool marked_space_as_used = false;
	    if (!marked_space_as_used) {
		marked_space_as_used = true;
		update_separator_map_for_foreign_name(" ");
		update_output_separator();
	    }
	}
	int len = s_len(&component);
	char *p = s_steal(&component);
	// Apply case treatment.
	switch (pcs->Case) {
	  case LOWER:
	    for (int i = 0; i < len; ++i)
		p[i] = tolower((unsigned char)p[i]);
	    break;
	  case UPPER:
	    for (int i = 0; i < len; ++i)
		p[i] = toupper((unsigned char)p[i]);
	    break;
	  case OFF:
	    // Avoid unhandled enum warning.
	    break;
	}

	prefix *ptr = root;
	for (int i = 0; i < 4; ++i) {
	    char *name;
	    int sflag = BIT(SFLAGS_SURVEY);
	    if (i == 3) {
		name = p;
		sflag = 0;
	    } else {
		if (i < 3 - explicit_prefix_levels) {
		    name = walls_prefix[i];
		    // printf("using walls_prefix[%d] = '%s'\n", 2 - i, name);
		} else {
		    name = w_prefix[i - (3 - explicit_prefix_levels)]; // FIXME: Could steal wprefix[i].
		    // printf("using w_prefix[%d] = '%s'\n", i - (3 - explicit_prefix_levels), name);
		}

		if (name == NULL) {
		    // FIXME: This means :X::Y is treated as the same as
		    // ::X:Y but is that right?  Walls docs don't really
		    // say.  Need to test (and is they're different then
		    // probably use a character not valid in Walls station
		    // names for the empty prefix level (e.g. space or
		    // `#`).
		    //
		    // Also, does Walls allow :::X as a station and
		    // ::X:Y which would mean X is a station and survey?
		    // If so, we probably want to keep every empty level.
		    continue;
		}
	    }
	    prefix *back_ptr = ptr;
	    ptr = ptr->down;
	    if (ptr == NULL) {
		/* Special case first time around at each level */
		/* No need to check if we're at the station level - if the
		 * prefix is new the station must be. */
		if (p_new) *p_new = true;
		ptr = osnew(prefix);
		ptr->sflags = sflag;
		if (strlen(name) < sizeof(ptr->ident.i)) {
		    strcpy(ptr->ident.i, name);
		    ptr->sflags |= BIT(SFLAGS_IDENT_INLINE);
		    if (i >= 3) free(name);
		} else {
		    ptr->ident.p = (i < 3 ? osstrdup(name) : name);
		}
		name = NULL;
		ptr->right = ptr->down = NULL;
		ptr->pos = NULL;
		ptr->stn = NULL;
		ptr->up = back_ptr;
		// FIXME: Or location of #Prefix, etc for it?
		ptr->filename = file.filename;
		ptr->line = file.line;
		ptr->column = fp.offset - file.lpos;

		ptr->min_export = ptr->max_export = 0;
		back_ptr->down = ptr;
	    } else {
		/* Use caching to speed up adding an increasing sequence to a
		 * large survey */
		static prefix *cached_survey = NULL, *cached_station = NULL;
		prefix *ptrPrev = NULL;
		int cmp = 1; /* result of strcmp ( -ve for <, 0 for =, +ve for > ) */
		if (cached_survey == back_ptr) {
		    cmp = strcmp(prefix_ident(cached_station), name);
		    if (cmp <= 0) ptr = cached_station;
		}
		while (ptr && (cmp = strcmp(prefix_ident(ptr), name))<0) {
		    ptrPrev = ptr;
		    ptr = ptr->right;
		}
		if (cmp) {
		    /* ie we got to one that was higher, or the end */
		    if (p_new) *p_new = true;
		    prefix *newptr = osnew(prefix);
		    newptr->sflags = sflag;
		    if (strlen(name) < sizeof(newptr->ident.i)) {
			strcpy(newptr->ident.i, name);
			newptr->sflags |= BIT(SFLAGS_IDENT_INLINE);
			if (i >= 3) free(name);
		    } else {
			newptr->ident.p = (i < 3 ? osstrdup(name) : name);
		    }
		    name = NULL;
		    if (ptrPrev == NULL)
			back_ptr->down = newptr;
		    else
			ptrPrev->right = newptr;
		    newptr->right = ptr;
		    newptr->down = NULL;
		    newptr->pos = NULL;
		    newptr->stn = NULL;
		    newptr->up = back_ptr;
		    // FIXME: Or location of #Prefix, etc for it?
		    newptr->filename = file.filename;
		    newptr->line = file.line;
		    newptr->column = fp.offset - file.lpos;

		    newptr->min_export = newptr->max_export = 0;
		    ptr = newptr;
		} else {
		    ptr->sflags |= sflag;
		}
		if (!TSTBIT(ptr->sflags, SFLAGS_SURVEY)) {
		    ptr->min_export = USHRT_MAX;
		}
		cached_survey = back_ptr;
		cached_station = ptr;
	    }
	    if (name == p) free(p);
	}

	// Do the equivalent of "*infer exports" for Walls stations with an
	// explicit prefix.
	if (ptr->min_export == 0 || ptr->min_export == USHRT_MAX) {
	    if (explicit_prefix_levels > ptr->max_export)
		ptr->max_export = explicit_prefix_levels;
	}

	for (int i = 0; i < 3; ++i) free(w_prefix[i]);

	return ptr;
    }
}

/* if numeric expr is omitted: if f_optional return HUGE_REAL, else longjmp */
real
read_number_or_int(bool f_optional, bool f_unsigned, bool* pf_decimal_point)
{
   if (pf_decimal_point) *pf_decimal_point = false;
   bool fPositive = true, fDigits = false;
   real n = (real)0.0;
   filepos fp;
   int ch_old;

   get_pos(&fp);
   ch_old = ch;
   if (!f_unsigned) {
      fPositive = !isMinus(ch);
      if (isSign(ch)) nextch();
   }

   while (isdigit(ch)) {
      n = n * (real)10.0 + (char)(ch - '0');
      nextch();
      fDigits = true;
   }

   if (isDecimal(ch)) {
      if (pf_decimal_point) *pf_decimal_point = true;
      real mult = (real)1.0;
      nextch();
      while (isdigit(ch)) {
	 mult *= (real).1;
	 n += (char)(ch - '0') * mult;
	 fDigits = true;
	 nextch();
      }
   }

   /* !'fRead' => !fDigits so fDigits => 'fRead' */
   if (fDigits) return (fPositive ? n : -n);

   /* didn't read a valid number.  If it's optional, reset filepos & return */
   set_pos(&fp);
   if (f_optional) {
      return HUGE_REAL;
   }

   if (isOmit(ch_old)) {
      compile_diagnostic(DIAG_ERR|DIAG_COL, /*Field may not be omitted*/114);
   } else {
      compile_diagnostic_token_show(DIAG_ERR, /*Expecting numeric field, found “%s”*/9);
   }
   longjmp(jbSkipLine, 1);
}

real
read_quadrant(bool f_optional)
{
   enum {
      POINT_N = 0,
      POINT_E = 1,
      POINT_S = 2,
      POINT_W = 3,
      POINT_NONE = -1
   };
   static const sztok pointtab[] = {
	{"E", POINT_E },
	{"N", POINT_N },
	{"S", POINT_S },
	{"W", POINT_W },
	{NULL, POINT_NONE }
   };
   static const sztok pointewtab[] = {
	{"E", POINT_E },
	{"W", POINT_W },
	{NULL, POINT_NONE }
   };
   if (f_optional && isOmit(ch)) {
      return HUGE_REAL;
   }
   const int quad = 90;
   filepos fp;
   get_pos(&fp);
   get_token_legacy_no_blanks();
   int first_point = match_tok(pointtab, TABSIZE(pointtab));
   if (first_point == POINT_NONE) {
      set_pos(&fp);
      if (isOmit(ch)) {
	 compile_diagnostic(DIAG_ERR|DIAG_COL, /*Field may not be omitted*/114);
      }
      // TRANSLATORS: Quadrant bearings have the format e.g. S34E or N65.5W.
      // They're enabled by `*units bearing quadrants` and supported in
      // Walls format data.
      compile_diagnostic_token_show(DIAG_ERR, /*Expecting quadrant bearing, found “%s”*/483);
      longjmp(jbSkipLine, 1);
   }
   real r = read_number(true, true);
   if (r == HUGE_REAL) {
      if (isSign(ch) || isDecimal(ch)) {
	 /* Give better errors for S-0E, N+10W, N.E, etc. */
	 set_pos(&fp);
	 compile_diagnostic_token_show(DIAG_ERR, /*Expecting quadrant bearing, found “%s”*/483);
	 longjmp(jbSkipLine, 1);
      }
      /* N, S, E or W. */
      return first_point * quad;
   }
   if (first_point == POINT_E || first_point == POINT_W) {
      set_pos(&fp);
      compile_diagnostic_token_show(DIAG_ERR, /*Expecting quadrant bearing, found “%s”*/483);
      longjmp(jbSkipLine, 1);
   }

   get_token_legacy_no_blanks();
   int second_point = match_tok(pointewtab, TABSIZE(pointewtab));
   if (second_point == POINT_NONE) {
      set_pos(&fp);
      compile_diagnostic_token_show(DIAG_ERR, /*Expecting quadrant bearing, found “%s”*/483);
      longjmp(jbSkipLine, 1);
   }

   if (r > quad) {
      set_pos(&fp);
      compile_diagnostic_token_show(DIAG_ERR, /*Suspicious compass reading*/59);
      longjmp(jbSkipLine, 1);
   }

   if (first_point == POINT_N) {
      if (second_point == POINT_W) {
	 r = quad * 4 - r;
      }
   } else {
      if (second_point == POINT_W) {
	 r += quad * 2;
      } else {
	 r = quad * 2 - r;
      }
   }
   return r;
}

extern real
read_numeric(bool f_optional)
{
   skipblanks();
   return read_number(f_optional, false);
}

extern real
read_numeric_multi(bool f_optional, bool f_quadrants, int *p_n_readings)
{
   size_t n_readings = 0;
   real tot = (real)0.0;

   skipblanks();
   if (!isOpen(ch)) {
      real r = 0;
      if (!f_quadrants) {
	  r = read_number(f_optional, false);
      } else {
	  r = read_quadrant(f_optional);
	  if (r != HUGE_REAL)
	      do_legacy_token_warning();
      }
      if (p_n_readings) *p_n_readings = (r == HUGE_REAL ? 0 : 1);
      return r;
   }
   nextch();

   skipblanks();
   do {
      if (!f_quadrants) {
	 tot += read_number(false, false);
      } else {
	 tot += read_quadrant(false);
	 do_legacy_token_warning();
      }
      ++n_readings;
      skipblanks();
   } while (!isClose(ch));
   nextch();

   if (p_n_readings) *p_n_readings = n_readings;
   /* FIXME: special averaging for bearings ... */
   /* And for percentage gradient */
   return tot / n_readings;
}

/* read numeric expr or omit (return HUGE_REAL); else longjmp */
extern real
read_bearing_multi_or_omit(bool f_quadrants, int *p_n_readings)
{
   real v;
   v = read_numeric_multi(true, f_quadrants, p_n_readings);
   if (v == HUGE_REAL) {
      if (!isOmit(ch)) {
	 compile_diagnostic_token_show(DIAG_ERR, /*Expecting numeric field, found “%s”*/9);
	 longjmp(jbSkipLine, 1);
      }
      nextch();
   }
   return v;
}

/* Don't skip blanks, variable error code */
unsigned int
read_uint_raw(int diag_type, int errmsg, const filepos *fp)
{
   unsigned int n = 0;
   if (!isdigit(ch)) {
      if (fp) set_pos(fp);
      if ((diag_type & DIAG_UINT)) {
	  compile_diagnostic(diag_type, errmsg);
      } else {
	  compile_diagnostic_token_show(diag_type, errmsg);
      }
      longjmp(jbSkipLine, 1);
   }
   while (isdigit(ch)) {
      n = n * 10 + (char)(ch - '0');
      nextch();
   }
   return n;
}

extern unsigned int
read_uint(void)
{
   skipblanks();
   return read_uint_raw(DIAG_ERR, /*Expecting numeric field, found “%s”*/9, NULL);
}

extern int
read_int(int min_val, int max_val)
{
    skipblanks();
    unsigned n = 0;
    filepos fp;

    get_pos(&fp);
    bool negated = isMinus(ch);
    unsigned limit;
    if (negated) {
	limit = (unsigned)(min_val == INT_MIN ? INT_MIN : -min_val);
    } else {
	limit = (unsigned)max_val;
    }
    if (isSign(ch)) nextch();

    if (!isdigit(ch)) {
bad_value:
	set_pos(&fp);
	/* TRANSLATORS: The first %d will be replaced by the (inclusive) lower
	 * bound and the second by the (inclusive) upper bound, for example:
	 * Expecting integer in range -60 to 60
	 */
	compile_diagnostic(DIAG_ERR|DIAG_NUM, /*Expecting integer in range %d to %d*/489);
	longjmp(jbSkipLine, 1);
    }

    while (isdigit(ch)) {
	unsigned old_n = n;
	n = n * 10 + (char)(ch - '0');
	if (n > limit || n < old_n) {
	    goto bad_value;
	}
	nextch();
    }
    if (isDecimal(ch)) goto bad_value;

    if (negated) {
	if (n > (unsigned)INT_MAX) {
	    // Avoid unportable casting.
	    return INT_MIN;
	}
	return -(int)n;
    }
    return (int)n;
}

static bool
read_string_(string *pstr, int diag_type)
{
   s_clear(pstr);

   skipblanks();
   if (ch == '\"') {
      /* String quoted in "" */
      nextch();
      while (1) {
	 if (isEol(ch)) {
	    compile_diagnostic(diag_type|DIAG_COL, /*Missing \"*/69);
	    if (diag_type == DIAG_ERR) longjmp(jbSkipLine, 1);
	    return false;
	 }

	 if (ch == '\"') break;

	 s_appendch(pstr, ch);
	 nextch();
      }
      nextch();
   } else {
      /* Unquoted string */
      while (1) {
	 if (isEol(ch) || isComm(ch)) {
	    if (s_empty(pstr)) {
	       compile_diagnostic(diag_type|DIAG_COL, /*Expecting string field*/121);
		if (diag_type == DIAG_ERR) longjmp(jbSkipLine, 1);
		return false;
	    }
	    return true;
	 }

	 if (isBlank(ch)) break;

	 s_appendch(pstr, ch);
	 nextch();
      }
   }
   return true;
}

extern void
read_string(string *pstr)
{
    (void)read_string_(pstr, DIAG_ERR);
}

extern bool
read_string_warning(string *pstr)
{
    return read_string_(pstr, DIAG_WARN);
}

extern void
read_walls_srv_date(int *py, int *pm, int *pd)
{
    skipblanks();

    filepos fp_date;
    get_pos(&fp_date);
    unsigned y = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date);
    int separator = -2;
    if (ch == '-' || ch == '/') {
	separator = ch;
	nextch();
    }
    filepos fp_month;
    get_pos(&fp_month);
    unsigned m = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date);
    if (ch == separator) {
	nextch();
    }
    filepos fp_day;
    get_pos(&fp_day);
    unsigned d = read_uint_raw(DIAG_ERR, /*Expecting date, found “%s”*/198, &fp_date);

    filepos fp_year;
    if (y < 100) {
	// Walls recommends ISO 8601 date format (yyyy-mm-dd and seemingly the
	// non-standard variant yyyy/mm/dd), but also accepts "some date formats
	// common in the U.S. (mm/dd/yy, mm-dd-yyyy, etc.)"
	unsigned tmp = y;
	y = d;
	fp_year = fp_day;
	d = m;
	fp_day = fp_month;
	m = tmp;
	fp_month = fp_date;

	if (y < 100) {
	    // FIXME: Are all 2 digit years 19xx?
	    y += 1900;

	    filepos fp_save;
	    get_pos(&fp_save);
	    set_pos(&fp_year);
	    /* TRANSLATORS: %d will be replaced by the assumed year, e.g. 1918 */
	    compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Assuming 2 digit year is %d*/76, y);
	    set_pos(&fp_save);
	}
    } else {
	if (y < 1900 || y > 2078) {
	    set_pos(&fp_date);
	    compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid year (< 1900 or > 2078)*/58);
	    longjmp(jbSkipLine, 1);
	}
	fp_year = fp_date;
    }

    if (m < 1 || m > 12) {
	set_pos(&fp_month);
	compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid month*/86);
	longjmp(jbSkipLine, 1);
    }

    if (d < 1 || d > (unsigned)last_day(y, m)) {
	set_pos(&fp_day);
	/* TRANSLATORS: e.g. 31st of April, or 32nd of any month */
	compile_diagnostic(DIAG_WARN|DIAG_UINT, /*Invalid day of the month*/87);
	longjmp(jbSkipLine, 1);
    }

    if (py) *py = y;
    if (pm) *pm = m;
    if (pd) *pd = d;
}
