type Formatter = (d: Date | null) => string;
type Parser = (s: string) => Date | false;

type Pair = [Formatter, Parser];
type DateLike = { day: number, month: number, year: number };

//https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

/**
 * Converts an array of formatParts and strings to an object where the key is the type of the format part, and its value is the item from match with the same index
 * Note that parts is 0-indexed, while match is 1-indexed, necessitating the .shift() call
 */
const makeDateLike: (parts: Intl.DateTimeFormatPart[], match: string[]) => DateLike | false = (parts, match) => {
  match.shift();

  const date = {};
  for (let i = 0; i < parts.length; i++) {
    const { type } = parts[i];
    date[type] = match[i];
  }
  if ("day" in date && "month" in date && "year" in date) return date;
  else return false;
};

/**
 * The function makes a pair of formatter and parser which are each others inverse.
 * date === parser(formatter(date)) and value === formatter(parser(value)
 * The parser may return false if the value is not a valid date.
 *
 * The formatter uses the native browser function for this.
 * The parser works by creating a regex which is matched against the provided string
 * The parts it extracts are matched against the date parts the browser provides
 * and then loaded into a date object
 * If this causes any overflows (ie the 32 day of the month) the date is rejected
 */
export const makeParserPair: (l: string, o?: Intl.DateTimeFormatOptions) => Pair = (locale, options) => {
  const format = new Intl.DateTimeFormat(locale, options);
  const formatter: Formatter = date => {
    if (date === null) return "";
    else return format.format(date);
  };

  const parts = format.formatToParts(new Date());
  const pattern = parts.map(part => part.type === "literal" ? escapeRegExp(part.value) : "(.+)").join("");
  const regex = new RegExp(pattern);

  const variableParts = parts.filter(({ type }) => type !== "literal");

  const parser: Parser = value => {
    if (value === null) return false;
    const match = value.match(regex);
    if (match == null) return false;
    if (match.length !== variableParts.length + 1) return false; // This should never happen, but it can't hurt to check

    const dateLike = makeDateLike(variableParts, match);
    if (dateLike === false) return false;

    const date = new Date();
    date.setFullYear(dateLike.year, dateLike.month - 1, dateLike.day);

    if (date.getMonth() != dateLike.month - 1) return false;
    if (date.getDate() != dateLike.day) return false;
    if (isNaN(date.getTime())) return false;

    return date;
  };

  return [formatter, parser];
};