# Nim CLI for retrieving Biblical passages
# © 2023 Jonathan Bernard

## Simple command-line tool for retrieving Biblical passages.

import std/[json, logging, os, re, strutils, wordwrap]
import cliutils, docopt, zero_functional

import ./api_bible
import ./esv
import ./kjv
import ./mev
import ./passage_query

proc formatMarkdown(raw, translation: string): string =
  var reference = ""
  var inVerse = false
  var verseLines = newSeq[string]()

  for line in raw.splitLines:
    if reference.len == 0: reference = line.strip
    if inVerse:
      if line.startsWith("Footnotes"): inVerse = false
      elif line.isEmptyOrWhitespace and verseLines[^1] != "":
        verseLines.add("")
      elif not line.match(re"^\s+[^\s]"): continue
      elif line.match(re"$(.*)\(ESV\)$"): verseLines.add(line[0 ..< ^5])
      else: verseLines.add(line)
    elif line.match(re"^\s+\[\d+\]"):
      inVerse = true
      verseLines.add(line)

  let wrapped = (verseLines -->
    map(if it.len > 90: it.strip else: it & "  ").
    map(it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "**$1**")])).
    map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p")

  result = (wrapped.splitLines --> map("> " & it)).
    join("\p") & "\p> -- *" & reference & " (" &
      translation.toUpperAscii & ")*"

proc formatPlain(
    raw,
    translation: string,
    keepVerseNumbers = true): string =

  var reference = ""
  var inVerse = false
  var verseLines = newSeq[string]()

  for line in raw.splitLines:
    if reference.len == 0: reference = line.strip
    if inVerse:
      if line.startsWith("Footnotes"): inVerse = false
      elif line.isEmptyOrWhitespace and verseLines[^1] != "":
        verseLines.add("")
      elif not line.match(re"^\s+[^\s]"): continue
      elif line.match(re"$(.*)\(ESV\)$"): verseLines.add(line[0 ..< ^5])
      else: verseLines.add(line)
    elif line.match(re"^\s+\[\d+\]"):
      inVerse = true
      verseLines.add(line)

  let wrapped = (verseLines -->
    map(if it.len > 90: it.strip else: it & "  ").
    map(
      if keepVerseNumbers:
        it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "$1")])
      else:
        it.multiReplace([(re"\((\d+)\)", ""), (re"\[(\d+)\]", "")])).
    map(wrapWords(it, maxLineWidth = 74, newLine = "\p"))).join("\p")

  result = (wrapped.splitLines --> map(it)).
    join("\p") & "\p– " & reference & " (" & translation.toUpperAscii & ")"

proc fetchPassages(reference, translation: string, cfg: CombinedConfig): seq[string] =
  case translation
  of "esv":
    esv.fetchPassages(
      reference,
      cfg.getVal("esv-api-token"),
      cfg.getVal("esv-api-root", "https://api.esv.org"))
  of "akjv", "kjv":
    kjv.fetchPassages(reference)
  of "mev":
    mev.fetchPassages(reference)
  of "amp", "nkjv", "niv":
    api_bible.fetchPassages(
      reference,
      translation,
      cfg.getVal("api-bible-api-key"),
      cfg.getVal("api-bible-root", api_bible.apiBibleRoot),
      cfg.getVal(
        "api-bible-" & translation & "-bible-id",
        api_bible.defaultBibleId(translation)))
  else:
    raise newException(ValueError,
      "unsupported translation '" & translation &
        "'; supported translations: " & supportedTranslationsList())

proc fetchPlainPassages(
    reference,
    translation: string,
    keepVerseNumbers: bool,
    cfg: CombinedConfig): seq[string] =

  case translation
  of "akjv", "kjv":
    result = kjv.fetchPlainPassages(reference, keepVerseNumbers)
  of "mev":
    result = mev.fetchPlainPassages(reference, keepVerseNumbers)
  else:
    for passage in fetchPassages(reference, translation, cfg):
      result.add(formatPlain(passage, translation, keepVerseNumbers))

proc fetchMarkdownPassages(
    reference,
    translation: string,
    cfg: CombinedConfig): seq[string] =

  case translation
  of "akjv", "kjv":
    result = kjv.fetchMarkdownPassages(reference)
  of "mev":
    result = mev.fetchMarkdownPassages(reference)
  else:
    for passage in fetchPassages(reference, translation, cfg):
      result.add(formatMarkdown(passage, translation))

when isMainModule:
  const USAGE = """Usage:
  bibleref <reference> [options]

Options:

  --debug                       Log debug information.

  --echo-args                   Echo back the arguments that were passed on the
                                command line for debugging purposes.

  -f, --output-format <format>  Select a specific output format. Valid values
                                are 'raw', 'markdown', 'plain', 'reading'.

  -t, --translation <translation>
                                Select a specific translation. Supported values
                                are 'akjv', 'amp', 'esv', 'kjv', 'mev',
                                'nkjv', and 'niv'. Defaults to 'esv'.
                                Individual references may override this with a
                                trailing marker, for example:
                                'John 3:16 (KJV); John 3:16 (ESV)'.

  --esv-api-token <token>       Provide the API token on the command line. By
                                default this will be read either from the
                                .bibleref.cfg.json file or the ESV_API_TOKEN
                                envionment variable.

  --api-bible-api-key <key>     Provide the API.Bible API key for translations
                                backed by api.bible.

  --api-bible-root <url>        Override the API.Bible API root. Defaults to
                                https://rest.api.bible/v1.

  --api-bible-amp-bible-id <id> Override the API.Bible Bible ID for AMP.

  --api-bible-niv-bible-id <id> Override the API.Bible Bible ID for NIV.

  --api-bible-nkjv-bible-id <id>
                                Override the API.Bible Bible ID for NKJV.
"""
  const VERSION = "1.1.1"

  let consoleLogger = newConsoleLogger(
    levelThreshold=lvlInfo,
    fmtStr="bibleref - $levelname: ")
  logging.addHandler(consoleLogger)

  try:
    # Parse arguments
    let args = docopt(USAGE, version = VERSION)

    if args["--debug"]:
      consoleLogger.levelThreshold = lvlDebug

    if args["--echo-args"]: stderr.writeLine($args)

    let cfgFilePath = getEnv("HOME") / ".bibleref.cfg.json"
    var cfgFileJson = newJObject()
    if fileExists(cfgFilePath):
      debug "Loading config from " & cfgFilePath
      cfgFileJson = parseFile(cfgFilePath)

    let cfg = CombinedConfig(docopt: args, json: cfgFileJson)
    let defaultTranslation = cfg.getVal("translation", "esv")
    let reference = $args["<reference>"]
    let queries = parsePassageQueries(reference, defaultTranslation)

    var formattedPassages: seq[string] = @[]
    for query in queries:
      case $args["--output-format"]
      of "plain":
        formattedPassages.add(fetchPlainPassages(
          query.referenceText,
          query.translation,
          keepVerseNumbers = true,
          cfg))
      of "reading", "text", "raw":
        for passage in fetchPassages(query.referenceText, query.translation, cfg):
          formattedPassages.add(
            case $args["--output-format"]:
            of "reading":
              formatPlain(passage, query.translation, keepVerseNumbers = false)
            of "text":
              passage.multiReplace([(re"\[(\d+)\]", "$1")])
            of "raw":
              passage
            else:
              formatMarkdown(passage, query.translation))
      else:
        formattedPassages.add(fetchMarkdownPassages(
          query.referenceText,
          query.translation,
          cfg))

    echo formattedPassages.join("\p\p")

  except CatchableError:
    fatal getCurrentExceptionMsg()
    debug getCurrentException().getStackTrace()
    quit(QuitFailure)
