// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

use crate::ast;
use crate::colors;
use crate::file_fetcher::File;
use crate::flags::Flags;
use crate::get_types;
use crate::media_type::MediaType;
use crate::module_graph;
use crate::program_state::ProgramState;
use crate::specifier_handler::FetchHandler;
use crate::write_json_to_stdout;
use crate::write_to_stdout_ignore_sigpipe;
use deno_core::error::AnyError;
use deno_core::futures::future::FutureExt;
use deno_core::futures::Future;
use deno_core::parking_lot::Mutex;
use deno_core::resolve_url_or_path;
use deno_doc as doc;
use deno_doc::parser::DocFileLoader;
use deno_runtime::permissions::Permissions;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use swc_ecmascript::parser::Syntax;

type DocResult = Result<(Syntax, String), doc::DocError>;

/// When parsing lib.deno.d.ts, only `DocParser::parse_source` is used,
/// which never even references the loader, so this is just a stub for that scenario.
///
/// TODO(Liamolucko): Refactor `deno_doc` so this isn't necessary.
struct StubDocLoader;

impl DocFileLoader for StubDocLoader {
  fn resolve(
    &self,
    _specifier: &str,
    _referrer: &str,
  ) -> Result<String, doc::DocError> {
    unreachable!()
  }

  fn load_source_code(
    &self,
    _specifier: &str,
  ) -> Pin<Box<dyn Future<Output = DocResult>>> {
    unreachable!()
  }
}

impl DocFileLoader for module_graph::Graph {
  fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
  ) -> Result<String, doc::DocError> {
    let referrer =
      resolve_url_or_path(referrer).expect("Expected valid specifier");
    match self.resolve(specifier, &referrer, true) {
      Ok(specifier) => Ok(specifier.to_string()),
      Err(e) => Err(doc::DocError::Resolve(e.to_string())),
    }
  }

  fn load_source_code(
    &self,
    specifier: &str,
  ) -> Pin<Box<dyn Future<Output = DocResult>>> {
    let specifier =
      resolve_url_or_path(specifier).expect("Expected valid specifier");
    let source = self.get_source(&specifier).expect("Unknown dependency");
    let media_type =
      self.get_media_type(&specifier).expect("Unknown media type");
    let syntax = ast::get_syntax(&media_type);
    async move { Ok((syntax, source)) }.boxed_local()
  }
}

pub async fn print_docs(
  flags: Flags,
  source_file: Option<String>,
  json: bool,
  maybe_filter: Option<String>,
  private: bool,
) -> Result<(), AnyError> {
  let program_state = ProgramState::build(flags.clone()).await?;
  let source_file = source_file.unwrap_or_else(|| "--builtin".to_string());

  let parse_result = if source_file == "--builtin" {
    let loader = Box::new(StubDocLoader);
    let doc_parser = doc::DocParser::new(loader, private);

    let syntax = ast::get_syntax(&MediaType::Dts);
    doc_parser.parse_source(
      "lib.deno.d.ts",
      syntax,
      get_types(flags.unstable).as_str(),
    )
  } else {
    let module_specifier = resolve_url_or_path(&source_file)?;

    // If the root module has external types, the module graph won't redirect it,
    // so instead create a dummy file which exports everything from the actual file being documented.
    let root_specifier = resolve_url_or_path("./$deno$doc.ts").unwrap();
    let root = File {
      local: PathBuf::from("./$deno$doc.ts"),
      maybe_types: None,
      media_type: MediaType::TypeScript,
      source: format!("export * from \"{}\";", module_specifier),
      specifier: root_specifier.clone(),
    };

    // Save our fake file into file fetcher cache.
    program_state.file_fetcher.insert_cached(root);

    let handler = Arc::new(Mutex::new(FetchHandler::new(
      &program_state,
      Permissions::allow_all(),
      Permissions::allow_all(),
    )?));
    let mut builder = module_graph::GraphBuilder::new(
      handler,
      program_state.maybe_import_map.clone(),
      program_state.lockfile.clone(),
    );
    builder.add(&root_specifier, false).await?;
    builder
      .analyze_config_file(&program_state.maybe_config_file)
      .await?;
    let graph = builder.get_graph();

    let doc_parser = doc::DocParser::new(Box::new(graph), private);
    doc_parser
      .parse_with_reexports(root_specifier.as_str())
      .await
  };

  let mut doc_nodes = match parse_result {
    Ok(nodes) => nodes,
    Err(e) => {
      eprintln!("{}", e);
      std::process::exit(1);
    }
  };

  if json {
    write_json_to_stdout(&doc_nodes)
  } else {
    doc_nodes.retain(|doc_node| doc_node.kind != doc::DocNodeKind::Import);
    let details = if let Some(filter) = maybe_filter {
      let nodes =
        doc::find_nodes_by_name_recursively(doc_nodes, filter.clone());
      if nodes.is_empty() {
        eprintln!("Node {} was not found!", filter);
        std::process::exit(1);
      }
      format!(
        "{}",
        doc::DocPrinter::new(&nodes, colors::use_color(), private)
      )
    } else {
      format!(
        "{}",
        doc::DocPrinter::new(&doc_nodes, colors::use_color(), private)
      )
    };

    write_to_stdout_ignore_sigpipe(details.as_bytes()).map_err(AnyError::from)
  }
}
