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

// TODO(ry) This module builds up output by appending to a string. Instead it
// should either use a formatting trait
// https://doc.rust-lang.org/std/fmt/index.html#formatting-traits
// Or perhaps implement a Serializer for serde
// https://docs.serde.rs/serde/ser/trait.Serializer.html

// TODO(ry) The methods in this module take ownership of the DocNodes, this is
// unnecessary and can result in unnecessary copying. Instead they should take
// references.

use crate::colors;
use crate::doc;
use crate::doc::display::{
  display_abstract, display_async, display_generator, Indent, SliceDisplayer,
};
use crate::doc::DocNodeKind;
use crate::swc_ecma_ast;
use std::fmt::{Display, Formatter, Result as FmtResult};

pub struct DocPrinter<'a> {
  doc_nodes: &'a [doc::DocNode],
  details: bool,
  private: bool,
}

impl<'a> DocPrinter<'a> {
  pub fn new(
    doc_nodes: &[doc::DocNode],
    details: bool,
    private: bool,
  ) -> DocPrinter {
    DocPrinter {
      doc_nodes,
      details,
      private,
    }
  }

  pub fn format(&self, w: &mut Formatter<'_>) -> FmtResult {
    if self.details {
      self.format_details(w, self.doc_nodes, 0)
    } else {
      self.format_summary(w, self.doc_nodes, 0)
    }
  }

  fn format_summary(
    &self,
    w: &mut Formatter<'_>,
    doc_nodes: &[doc::DocNode],
    indent: i64,
  ) -> FmtResult {
    let mut sorted = Vec::from(doc_nodes);
    sorted.sort_unstable_by(|a, b| {
      let kind_cmp = self.kind_order(&a.kind).cmp(&self.kind_order(&b.kind));
      if kind_cmp == core::cmp::Ordering::Equal {
        a.name.cmp(&b.name)
      } else {
        kind_cmp
      }
    });

    for node in sorted {
      self.format_signature(w, &node, indent)?;

      if let Some(js_doc) = &node.js_doc {
        self.format_jsdoc(w, js_doc, indent + 1, self.details)?;
      }

      writeln!(w)?;

      if DocNodeKind::Namespace == node.kind {
        self.format_summary(
          w,
          &node.namespace_def.as_ref().unwrap().elements,
          indent + 1,
        )?;

        writeln!(w)?;
      };
    }

    Ok(())
  }

  fn format_details(
    &self,
    w: &mut Formatter<'_>,
    doc_nodes: &[doc::DocNode],
    indent: i64,
  ) -> FmtResult {
    for node in doc_nodes {
      write!(
        w,
        "{}",
        colors::italic_gray(&format!(
          "Defined in {}:{}:{} \n\n",
          node.location.filename, node.location.line, node.location.col
        ))
      )?;

      self.format_signature(w, &node, indent)?;

      let js_doc = &node.js_doc;
      if let Some(js_doc) = js_doc {
        self.format_jsdoc(w, js_doc, indent + 1, self.details)?;
      }
      writeln!(w)?;

      match node.kind {
        DocNodeKind::Class => self.format_class_details(w, node)?,
        DocNodeKind::Enum => self.format_enum_details(w, node)?,
        DocNodeKind::Interface => self.format_interface_details(w, node)?,
        DocNodeKind::Namespace => self.format_namespace_details(w, node)?,
        _ => {}
      }
    }

    Ok(())
  }

  fn kind_order(&self, kind: &doc::DocNodeKind) -> i64 {
    match kind {
      DocNodeKind::Function => 0,
      DocNodeKind::Variable => 1,
      DocNodeKind::Class => 2,
      DocNodeKind::Enum => 3,
      DocNodeKind::Interface => 4,
      DocNodeKind::TypeAlias => 5,
      DocNodeKind::Namespace => 6,
    }
  }

  fn format_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    match node.kind {
      DocNodeKind::Function => self.format_function_signature(w, node, indent),
      DocNodeKind::Variable => self.format_variable_signature(w, node, indent),
      DocNodeKind::Class => self.format_class_signature(w, node, indent),
      DocNodeKind::Enum => self.format_enum_signature(w, node, indent),
      DocNodeKind::Interface => {
        self.format_interface_signature(w, node, indent)
      }
      DocNodeKind::TypeAlias => {
        self.format_type_alias_signature(w, node, indent)
      }
      DocNodeKind::Namespace => {
        self.format_namespace_signature(w, node, indent)
      }
    }
  }

  // TODO(SyrupThinker) this should use a JSDoc parser
  fn format_jsdoc(
    &self,
    w: &mut Formatter<'_>,
    jsdoc: &str,
    indent: i64,
    details: bool,
  ) -> FmtResult {
    for line in jsdoc.lines() {
      // Only show the first paragraph when summarising
      // This should use the @summary JSDoc tag instead
      if !details && line.is_empty() {
        break;
      }

      writeln!(w, "{}{}", Indent(indent), colors::gray(&line))?;
    }

    Ok(())
  }

  fn format_class_details(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
  ) -> FmtResult {
    let class_def = node.class_def.as_ref().unwrap();
    for node in &class_def.constructors {
      writeln!(w, "{}{}", Indent(1), node,)?;
      if let Some(js_doc) = &node.js_doc {
        self.format_jsdoc(w, &js_doc, 2, self.details)?;
      }
    }
    for node in class_def.properties.iter().filter(|node| {
      self.private
        || node
          .accessibility
          .unwrap_or(swc_ecma_ast::Accessibility::Public)
          != swc_ecma_ast::Accessibility::Private
    }) {
      writeln!(w, "{}{}", Indent(1), node,)?;
      if let Some(js_doc) = &node.js_doc {
        self.format_jsdoc(w, &js_doc, 2, self.details)?;
      }
    }
    for index_sign_def in &class_def.index_signatures {
      writeln!(w, "{}{}", Indent(1), index_sign_def)?;
    }
    for node in class_def.methods.iter().filter(|node| {
      self.private
        || node
          .accessibility
          .unwrap_or(swc_ecma_ast::Accessibility::Public)
          != swc_ecma_ast::Accessibility::Private
    }) {
      writeln!(w, "{}{}", Indent(1), node,)?;
      if let Some(js_doc) = &node.js_doc {
        self.format_jsdoc(w, js_doc, 2, self.details)?;
      }
    }
    writeln!(w)
  }

  fn format_enum_details(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
  ) -> FmtResult {
    let enum_def = node.enum_def.as_ref().unwrap();
    for member in &enum_def.members {
      writeln!(w, "{}{}", Indent(1), colors::bold(&member.name))?;
    }
    writeln!(w)
  }

  fn format_interface_details(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
  ) -> FmtResult {
    let interface_def = node.interface_def.as_ref().unwrap();

    for property_def in &interface_def.properties {
      writeln!(w, "{}{}", Indent(1), property_def)?;
      if let Some(js_doc) = &property_def.js_doc {
        self.format_jsdoc(w, js_doc, 2, self.details)?;
      }
    }
    for method_def in &interface_def.methods {
      writeln!(w, "{}{}", Indent(1), method_def)?;
      if let Some(js_doc) = &method_def.js_doc {
        self.format_jsdoc(w, js_doc, 2, self.details)?;
      }
    }
    for index_sign_def in &interface_def.index_signatures {
      writeln!(w, "{}{}", Indent(1), index_sign_def)?;
    }
    writeln!(w)
  }

  fn format_namespace_details(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
  ) -> FmtResult {
    let elements = &node.namespace_def.as_ref().unwrap().elements;
    for node in elements {
      self.format_signature(w, &node, 1)?;
      if let Some(js_doc) = &node.js_doc {
        self.format_jsdoc(w, js_doc, 2, false)?;
      }
    }
    writeln!(w)
  }

  fn format_class_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    let class_def = node.class_def.as_ref().unwrap();
    write!(
      w,
      "{}{}{} {}",
      Indent(indent),
      display_abstract(class_def.is_abstract),
      colors::magenta("class"),
      colors::bold(&node.name),
    )?;
    if !class_def.type_params.is_empty() {
      write!(
        w,
        "<{}>",
        SliceDisplayer::new(&class_def.type_params, ", ", false)
      )?;
    }

    if let Some(extends) = &class_def.extends {
      write!(w, " {} {}", colors::magenta("extends"), extends)?;
    }
    if !class_def.super_type_params.is_empty() {
      write!(
        w,
        "<{}>",
        SliceDisplayer::new(&class_def.super_type_params, ", ", false)
      )?;
    }

    if !class_def.implements.is_empty() {
      write!(
        w,
        " {} {}",
        colors::magenta("implements"),
        SliceDisplayer::new(&class_def.implements, ", ", false)
      )?;
    }

    writeln!(w)
  }

  fn format_enum_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    writeln!(
      w,
      "{}{} {}",
      Indent(indent),
      colors::magenta("enum"),
      colors::bold(&node.name)
    )
  }

  fn format_function_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    let function_def = node.function_def.as_ref().unwrap();
    write!(
      w,
      "{}{}{}{} {}",
      Indent(indent),
      display_async(function_def.is_async),
      colors::magenta("function"),
      display_generator(function_def.is_generator),
      colors::bold(&node.name)
    )?;
    if !function_def.type_params.is_empty() {
      write!(
        w,
        "<{}>",
        SliceDisplayer::new(&function_def.type_params, ", ", false)
      )?;
    }
    write!(
      w,
      "({})",
      SliceDisplayer::new(&function_def.params, ", ", false)
    )?;
    if let Some(return_type) = &function_def.return_type {
      write!(w, ": {}", return_type)?;
    }
    writeln!(w)
  }

  fn format_interface_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    let interface_def = node.interface_def.as_ref().unwrap();
    write!(
      w,
      "{}{} {}",
      Indent(indent),
      colors::magenta("interface"),
      colors::bold(&node.name)
    )?;

    if !interface_def.type_params.is_empty() {
      write!(
        w,
        "<{}>",
        SliceDisplayer::new(&interface_def.type_params, ", ", false)
      )?;
    }

    if !interface_def.extends.is_empty() {
      write!(
        w,
        " {} {}",
        colors::magenta("extends"),
        SliceDisplayer::new(&interface_def.extends, ", ", false)
      )?;
    }

    writeln!(w)
  }

  fn format_type_alias_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    let type_alias_def = node.type_alias_def.as_ref().unwrap();
    write!(
      w,
      "{}{} {}",
      Indent(indent),
      colors::magenta("type"),
      colors::bold(&node.name),
    )?;

    if !type_alias_def.type_params.is_empty() {
      write!(
        w,
        "<{}>",
        SliceDisplayer::new(&type_alias_def.type_params, ", ", false)
      )?;
    }

    writeln!(w, " = {}", type_alias_def.ts_type)
  }

  fn format_namespace_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    writeln!(
      w,
      "{}{} {}",
      Indent(indent),
      colors::magenta("namespace"),
      colors::bold(&node.name)
    )
  }

  fn format_variable_signature(
    &self,
    w: &mut Formatter<'_>,
    node: &doc::DocNode,
    indent: i64,
  ) -> FmtResult {
    let variable_def = node.variable_def.as_ref().unwrap();
    write!(
      w,
      "{}{} {}",
      Indent(indent),
      colors::magenta(match variable_def.kind {
        swc_ecma_ast::VarDeclKind::Const => "const",
        swc_ecma_ast::VarDeclKind::Let => "let",
        swc_ecma_ast::VarDeclKind::Var => "var",
      }),
      colors::bold(&node.name),
    )?;
    if let Some(ts_type) = &variable_def.ts_type {
      write!(w, ": {}", ts_type)?;
    }
    writeln!(w)
  }
}

impl<'a> Display for DocPrinter<'a> {
  fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
    self.format(f)
  }
}
