///|
const VERSION = "0.1.0"

///|
fn main {
  let args = @env.args()
  let args = if args.length() >= 2 { args[2:] } else { [] }
  match args {
    ["--help"] | ["-h"] => print(help_text())
    ["--version"] | ["-V"] => print("moonfeed " + VERSION + "\n")
    [command, path] if command == "detect" ||
      command == "inspect" ||
      command == "normalize" => {
      let input = read_input_or_exit(path)
      if input == "" {
        fail("moonfeed: input is empty\n", 2)
      }
      if command == "detect" {
        match detect_name(input) {
          Ok(name) => print(name + "\n")
          Err(_) => fail("moonfeed: Unknown feed format\n", 2)
        }
      } else {
        match parse_feed(input) {
          Ok(feed) =>
            if command == "inspect" {
              match detect_name(input) {
                Ok(format) => print(inspect_feed(feed, format))
                Err(_) => print(inspect_feed(feed, "Unknown"))
              }
            } else {
              print(feed_json(feed).stringify(indent=2) + "\n")
            }
          Err(error) =>
            fail(
              "moonfeed: failed to parse \"" + path + "\":\n  " + error + "\n",
              2,
            )
        }
      }
    }
    _ => fail(help_text(), 2)
  }
}

///|
fn detect_name(input : String) -> Result[String, String] {
  try {
    match @detect.detect_format(input) {
      @detect.JsonFeed => Ok("JSON Feed 1.1")
      @detect.Rss => Ok("RSS 2.0")
      @detect.Atom => Ok("Atom 1.0")
    }
  } catch {
    error => Err(@debug.to_string(error))
  }
}

///|
fn parse_feed(input : String) -> Result[@model.Feed, String] {
  Ok(@moonfeed.parse(input)) catch {
    error => Err(@debug.to_string(error))
  }
}

///|
fn help_text() -> String {
  "MoonFeed - parse and normalize RSS, Atom and JSON Feed\n\nUsage:\n  moonfeed detect \n  moonfeed inspect \n  moonfeed normalize \n  moonfeed --help\n  moonfeed --version\n\nCommands:\n  detect      Detect the feed format\n  inspect     Show a human-readable feed summary\n  normalize   Convert a feed to normalized JSON\n"
}

///|
fn inspect_feed(feed : @model.Feed, format : String) -> String {
  let out = StringBuilder()
  out.write_string("Format: " + format + "\n")
  out.write_string("Title: " + feed.title + "\n")
  out.write_string("Home: " + optional(feed.home_page_url) + "\n")
  out.write_string("Feed URL: " + optional(feed.feed_url) + "\n")
  out.write_string("Language: " + optional(feed.language) + "\n")
  out.write_string("Items: " + feed.items.length().to_string() + "\n")
  out.write_string("Updated: " + optional(feed.updated) + "\n")
  out.write_string(
    "Updated UTC: " +
    optional_i64_text(feed.updated_at.map(value => value.unix_seconds)) +
    "\n\nItems:\n",
  )
  let limit = if feed.items.length() < 5 { feed.items.length() } else { 5 }
  for index = 0; index < limit; index = index + 1 {
    let item = feed.items[index]
    out.write_string(
      "\n" + (index + 1).to_string() + ". " + optional(item.title) + "\n",
    )
    out.write_string("   ID: " + item.id + "\n")
    out.write_string("   URL: " + optional(item.url) + "\n")
    out.write_string("   Published: " + optional(item.published) + "\n")
    out.write_string("   Categories: " + join(item.categories, ", ") + "\n")
  }
  out.to_string()
}

///|
fn feed_json(feed : @model.Feed) -> Json {
  object([
    ("title", Json::string(feed.title)),
    ("description", optional_json(feed.description)),
    ("home_page_url", optional_json(feed.home_page_url)),
    ("feed_url", optional_json(feed.feed_url)),
    ("language", optional_json(feed.language)),
    ("updated", optional_json(feed.updated)),
    (
      "updated_unix",
      optional_i64(feed.updated_at.map(value => value.unix_seconds)),
    ),
    ("authors", Json::array(feed.authors.map(author => author_json(author)))),
    (
      "categories",
      Json::array(feed.categories.map(value => Json::string(value))),
    ),
    ("items", Json::array(feed.items.map(item => item_json(item)))),
  ])
}

///|
fn item_json(item : @model.FeedItem) -> Json {
  object([
    ("id", Json::string(item.id)),
    ("title", optional_json(item.title)),
    ("url", optional_json(item.url)),
    ("external_url", optional_json(item.external_url)),
    ("summary", optional_json(item.summary)),
    ("content_text", optional_json(item.content_text)),
    ("content_html", optional_json(item.content_html)),
    ("published", optional_json(item.published)),
    (
      "published_unix",
      optional_i64(item.published_at.map(value => value.unix_seconds)),
    ),
    ("updated", optional_json(item.updated)),
    (
      "updated_unix",
      optional_i64(item.updated_at.map(value => value.unix_seconds)),
    ),
    ("authors", Json::array(item.authors.map(author => author_json(author)))),
    (
      "categories",
      Json::array(item.categories.map(value => Json::string(value))),
    ),
    (
      "attachments",
      Json::array(item.attachments.map(value => attachment_json(value))),
    ),
  ])
}

///|
fn author_json(author : @model.Author) -> Json {
  object([
    ("name", optional_json(author.name)),
    ("url", optional_json(author.url)),
    ("avatar", optional_json(author.avatar)),
  ])
}

///|
fn attachment_json(value : @model.Attachment) -> Json {
  object([
    ("url", Json::string(value.url)),
    ("mime_type", Json::string(value.mime_type)),
    ("title", optional_json(value.title)),
    (
      "size_in_bytes",
      match value.size_in_bytes {
        Some(number) =>
          Json::number(number.to_double(), repr=number.to_string())
        None => Json::null()
      },
    ),
    (
      "duration_in_seconds",
      match value.duration_in_seconds {
        Some(number) => Json::number(number)
        None => Json::null()
      },
    ),
  ])
}

///|
fn object(fields : Array[(String, Json)]) -> Json {
  let result : Map[String, Json] = Map([])
  for (key, value) in fields {
    result.set(key, value)
  }
  Json::object(result)
}

///|
fn optional_json(value : String?) -> Json {
  match value {
    Some(text) => Json::string(text)
    None => Json::null()
  }
}

///|
fn optional_i64(value : Int64?) -> Json {
  match value {
    Some(number) => Json::number(number.to_double(), repr=number.to_string())
    None => Json::null()
  }
}

///|
fn optional_i64_text(value : Int64?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "-"
  }
}

///|
fn optional(value : String?) -> String {
  match value {
    Some(text) => text
    None => "-"
  }
}

///|
fn join(values : Array[String], separator : String) -> String {
  let out = StringBuilder()
  for index, value in values {
    if index > 0 {
      out.write_string(separator)
    }
    out.write_string(value)
  }
  out.to_string()
}

///|
fn print(text : String) -> Unit {
  host_write(text, false)
}

///|
fn fail(text : String, code : Int) -> Unit {
  host_write(text, true)
  host_exit(code)
}

///|
fn read_input_or_exit(path : String) -> String {
  let value = host_read(path)
  if value.has_prefix("\u{0}ERR:") {
    fail("moonfeed: " + value.to_string_view()[5:].to_owned() + "\n", 2)
    ""
  } else {
    value
  }
}

///|
#cfg(target="js")
extern "js" fn host_read(path : String) -> String =
  #| function(path) {
  #|   try {
  #|     const fs = require("fs");
  #|     return path === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(path, "utf8");
  #|   } catch (error) {
  #|     return "\u0000ERR:" + (error && error.code === "ENOENT" ? "file not found: " : "cannot read input: ") + String(error && error.message || error);
  #|   }
  #| }

///|
#cfg(not(target="js"))
fn host_read(_path : String) -> String {
  "\u{0}ERR:CLI file I/O requires JavaScript target"
}

///|
#cfg(target="js")
extern "js" fn host_write(text : String, is_error : Bool) -> Unit =
  #| function(text, is_error) { (is_error ? process.stderr : process.stdout).write(text); }

///|
#cfg(not(target="js"))
fn host_write(text : String, _is_error : Bool) -> Unit {
  println(text)
}

///|
#cfg(target="js")
extern "js" fn host_exit(code : Int) -> Unit =
  #| function(code) { process.exit(code); }

///|
#cfg(not(target="js"))
fn host_exit(_code : Int) -> Unit {
  ()
}