export type DistinguisherParser<T> = (distinguisher: string) => T;
export type DocumentParser<T> = (document: string[]) => T;

const LEADING_WHITESPACE = /^([ \t]+)/;
const ENTRY = /^(.+?):\s*(.*)/;

export class Idv {
  collections: Record<string, undefined | [string, string[]][]> = {};

  public static parse(input: string): Idv {
    const idv = new Idv();
    return idv.import(input);
  }
  public static parseLines(input: string[]): Idv {
    const idv = new Idv();
    return idv.importLines(input);
  }
  public import(input: string): Idv {
    const lines = input.split("\n").map((line) => line.trimEnd());
    return this.importLines(lines);
  }
  public importLines(input: string[]): Idv {
    let currentDocument: string[] | undefined = undefined;
    let currentIndent: string | undefined = undefined;
    let bufferedBlankLines: string[] = [];

    input.forEach((line) => {
      const indent = LEADING_WHITESPACE.exec(line)?.[1];
      if (indent) {
        if (currentDocument == undefined) {
          throw new Error("Indented document found before an entry");
        }
        if (currentIndent == undefined) {
          currentIndent = indent;
        }
        if (line.startsWith(currentIndent)) {
          currentDocument.push(...bufferedBlankLines);
          bufferedBlankLines = [];
          currentDocument.push(line.substring(currentIndent.length));
        } else {
          throw new Error(
            "Inconsistent indentation- line indented less than the first line of its document"
          );
        }
      } else if (line == "") {
        bufferedBlankLines.push("");
      } else if (line[0] == "#") {
        // skip
      } else {
        const matches = ENTRY.exec(line);
        if (matches) {
          const [, collection, distinguisher] = matches;

          if (this.collections[collection] == undefined) {
            this.collections[collection] = [];
          }

          currentDocument = [];
          currentIndent = undefined;
          this.collections[collection].push([distinguisher, currentDocument]);
        } else {
          throw new Error("Failed to parse a property");
        }
      }
    });

    return this;
  }

  public getProperty<T>(
    name: string,
    parseDistinguisher: DistinguisherParser<T>,
    parseDocument: DocumentParser<T>
  ): T | undefined {
    const firstEntry = this.collections[name]?.[0];
    return (
      firstEntry &&
      (firstEntry[1].length > 0
        ? parseDocument(firstEntry[1])
        : parseDistinguisher(firstEntry[0]))
    );
  }

  public getList<T>(
    name: string,
    parseDistinguisher: DistinguisherParser<T>,
    parseDocument: DocumentParser<T>
  ): T[] {
    return (this.collections[name] ?? []).map(([distinguisher, document]) =>
      document.length > 0
        ? parseDocument(document)
        : parseDistinguisher(distinguisher)
    );
  }

  public getMap<T>(
    name: string,
    parseDocument: DocumentParser<T>
  ): Record<string, T> {
    return Object.fromEntries(
      (this.collections[name] ?? []).map(([distinguisher, document]) => [
        distinguisher,
        parseDocument(document),
      ])
    );
  }
}

export const StringFromDocument = (lines: string[]) => lines.join("\n");