/*
 This file is part of GNU Taler
 (C) 2020 Taler Systems S.A.
 GNU Taler 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, or (at your option) any later version.
 GNU Taler 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
 GNU Taler; see the file COPYING.  If not, see 
 */
/**
 * Utilities to handle Taler-style configuration files.
 *
 * @author Florian Dold 
 */
/**
 * Imports
 */
import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
import fs from "fs";
export class ConfigError extends Error {
  constructor(message: string) {
    super();
    Object.setPrototypeOf(this, ConfigError.prototype);
    this.name = "ConfigError";
    this.message = message;
  }
}
type OptionMap = { [optionName: string]: string };
type SectionMap = { [sectionName: string]: OptionMap };
export class ConfigValue {
  constructor(
    private sectionName: string,
    private optionName: string,
    private val: string | undefined,
    private converter: (x: string) => T,
  ) {}
  required(): T {
    if (!this.val) {
      throw new ConfigError(
        `required option [${this.sectionName}]/${this.optionName} not found`,
      );
    }
    return this.converter(this.val);
  }
  orUndefined(): T | undefined {
    if (this.val !== undefined) {
      return this.converter(this.val);
    } else {
      return undefined;
    }
  }
  orDefault(v: T): T | undefined {
    if (this.val !== undefined) {
      return this.converter(this.val);
    } else {
      return v;
    }
  }
  isDefined(): boolean {
    return this.val !== undefined;
  }
}
/**
 * Shell-style path substitution.
 *
 * Supported patterns:
 * "$x" (look up "x")
 * "${x}" (look up "x")
 * "${x:-y}" (look up "x", fall back to expanded y)
 */
export function pathsub(
  x: string,
  lookup: (s: string, depth: number) => string | undefined,
  depth = 0,
): string {
  if (depth >= 10) {
    throw Error("recursion in path substitution");
  }
  let s = x;
  let l = 0;
  while (l < s.length) {
    if (s[l] === "$") {
      if (s[l + 1] === "{") {
        let depth = 1;
        const start = l;
        let p = start + 2;
        let insideNamePart = true;
        let hasDefault = false;
        for (; p < s.length; p++) {
          if (s[p] == "}") {
            insideNamePart = false;
            depth--;
          } else if (s[p] === "$" && s[p + 1] === "{") {
            insideNamePart = false;
            depth++;
          }
          if (insideNamePart && s[p] === ":" && s[p + 1] === "-") {
            hasDefault = true;
          }
          if (depth == 0) {
            break;
          }
        }
        if (depth == 0) {
          const inner = s.slice(start + 2, p);
          let varname: string;
          let defaultValue: string | undefined;
          if (hasDefault) {
            [varname, defaultValue] = inner.split(":-", 2);
          } else {
            varname = inner;
            defaultValue = undefined;
          }
          const r = lookup(inner, depth + 1);
          if (r !== undefined) {
            s = s.substr(0, start) + r + s.substr(p + 1);
            l = start + r.length;
            continue;
          } else if (defaultValue !== undefined) {
            const resolvedDefault = pathsub(defaultValue, lookup, depth + 1);
            s = s.substr(0, start) + resolvedDefault + s.substr(p + 1);
            l = start + resolvedDefault.length;
            continue;
          }
        }
        l = p;
        continue;
      } else {
        const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1));
        if (m && m[0]) {
          const r = lookup(m[0], depth + 1);
          if (r !== undefined) {
            s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
            l = l + r.length;
            continue;
          }
        }
      }
    }
    l++;
  }
  return s;
}
export class Configuration {
  private sectionMap: SectionMap = {};
  loadFromString(s: string): void {
    const reComment = /^\s*#.*$/;
    const reSection = /^\s*\[\s*([^\]]*)\s*\]\s*$/;
    const reParam = /^\s*([^=]+?)\s*=\s*(.*?)\s*$/;
    const reEmptyLine = /^\s*$/;
    let currentSection: string | undefined = undefined;
    const lines = s.split("\n");
    for (const line of lines) {
      if (reEmptyLine.test(line)) {
        continue;
      }
      if (reComment.test(line)) {
        continue;
      }
      const secMatch = line.match(reSection);
      if (secMatch) {
        currentSection = secMatch[1];
        continue;
      }
      if (currentSection === undefined) {
        throw Error("invalid configuration, expected section header");
      }
      currentSection = currentSection.toUpperCase();
      const paramMatch = line.match(reParam);
      if (paramMatch) {
        const optName = paramMatch[1].toUpperCase();
        let val = paramMatch[2];
        if (val.startsWith('"') && val.endsWith('"')) {
          val = val.slice(1, val.length - 1);
        }
        const sec = this.sectionMap[currentSection] ?? {};
        this.sectionMap[currentSection] = Object.assign(sec, {
          [optName]: val,
        });
        continue;
      }
      throw Error(
        "invalid configuration, expected section header or option assignment",
      );
    }
  }
  setString(section: string, option: string, value: string): void {
    const secNorm = section.toUpperCase();
    const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {});
    sec[option.toUpperCase()] = value;
  }
  getString(section: string, option: string): ConfigValue {
    const secNorm = section.toUpperCase();
    const optNorm = option.toUpperCase();
    const val = (this.sectionMap[secNorm] ?? {})[optNorm];
    return new ConfigValue(secNorm, optNorm, val, (x) => x);
  }
  getPath(section: string, option: string): ConfigValue {
    const secNorm = section.toUpperCase();
    const optNorm = option.toUpperCase();
    const val = (this.sectionMap[secNorm] ?? {})[optNorm];
    return new ConfigValue(secNorm, optNorm, val, (x) =>
      pathsub(x, (v, d) => this.lookupVariable(v, d + 1)),
    );
  }
  getYesNo(section: string, option: string): ConfigValue {
    const secNorm = section.toUpperCase();
    const optNorm = option.toUpperCase();
    const val = (this.sectionMap[secNorm] ?? {})[optNorm];
    const convert = (x: string): boolean => {
      x = x.toLowerCase();
      if (x === "yes") {
        return true;
      } else if (x === "no") {
        return false;
      }
      throw Error(
        `invalid config value for [${secNorm}]/${optNorm}, expected yes/no`,
      );
    };
    return new ConfigValue(secNorm, optNorm, val, convert);
  }
  getNumber(section: string, option: string): ConfigValue {
    const secNorm = section.toUpperCase();
    const optNorm = option.toUpperCase();
    const val = (this.sectionMap[secNorm] ?? {})[optNorm];
    const convert = (x: string): number => {
      try {
        return Number.parseInt(x, 10);
      } catch (e) {
        throw Error(
          `invalid config value for [${secNorm}]/${optNorm}, expected number`,
        );
      }
    };
    return new ConfigValue(secNorm, optNorm, val, convert);
  }
  lookupVariable(x: string, depth: number = 0): string | undefined {
    // We loop up options in PATHS in upper case, as option names
    // are case insensitive
    const val = (this.sectionMap["PATHS"] ?? {})[x.toUpperCase()];
    if (val !== undefined) {
      return pathsub(val, (v, d) => this.lookupVariable(v, d), depth);
    }
    // Environment variables can be case sensitive, respect that.
    const envVal = process.env[x];
    if (envVal !== undefined) {
      return envVal;
    }
    return;
  }
  getAmount(section: string, option: string): ConfigValue {
    const val = (this.sectionMap[section] ?? {})[option];
    return new ConfigValue(section, option, val, (x) =>
      Amounts.parseOrThrow(x),
    );
  }
  static load(filename: string): Configuration {
    const s = fs.readFileSync(filename, "utf-8");
    const cfg = new Configuration();
    cfg.loadFromString(s);
    return cfg;
  }
  write(filename: string): void {
    let s = "";
    for (const sectionName of Object.keys(this.sectionMap)) {
      s += `[${sectionName}]\n`;
      for (const optionName of Object.keys(
        this.sectionMap[sectionName] ?? {},
      )) {
        const val = this.sectionMap[sectionName][optionName];
        if (val !== undefined) {
          s += `${optionName} = ${val}\n`;
        }
      }
      s += "\n";
    }
    fs.writeFileSync(filename, s);
  }
}