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

use crate::ParamDef;
use crate::decorators::DecoratorDef;
use crate::decorators::decorators_to_defs;
use crate::params::param_to_param_def;
use crate::ts_type::TsTypeDef;
use crate::ts_type_param::TsTypeParamDef;
use crate::ts_type_param::maybe_type_param_decl_to_type_param_defs;
use crate::util::swc::is_false;
use deno_ast::swc::ast::ReturnStmt;
use deno_ast::swc::ast::Stmt;
use deno_graph::symbols::EsModuleInfo;
use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FunctionDef {
  #[serde(skip_serializing_if = "Option::is_none", default)]
  /// set when the function is a default export and has a name in its declaration
  pub def_name: Option<String>,
  pub params: Vec<ParamDef>,
  #[serde(skip_serializing_if = "Option::is_none", default)]
  pub return_type: Option<TsTypeDef>,
  #[serde(skip_serializing_if = "is_false", default)]
  pub has_body: bool,
  #[serde(skip_serializing_if = "is_false", default)]
  pub is_async: bool,
  #[serde(skip_serializing_if = "is_false", default)]
  pub is_generator: bool,
  #[serde(skip_serializing_if = "<[_]>::is_empty", default)]
  pub type_params: Box<[TsTypeParamDef]>,
  #[serde(skip_serializing_if = "<[_]>::is_empty", default)]
  pub decorators: Box<[DecoratorDef]>,
}

pub fn function_to_function_def(
  module_info: &EsModuleInfo,
  function: &deno_ast::swc::ast::Function,
  def_name: Option<String>,
) -> FunctionDef {
  let params = function
    .params
    .iter()
    .map(|param| param_to_param_def(module_info, param))
    .collect();

  let maybe_return_type = match function
    .return_type
    .as_deref()
    .map(|return_type| TsTypeDef::new(module_info, &return_type.type_ann))
  {
    Some(return_type) => Some(return_type),
    None
      if !function.is_generator
        && function.body.is_some()
        && get_return_stmt_with_arg_from_function(function).is_none() =>
    {
      if function.is_async {
        Some(TsTypeDef {
          repr: "Promise".to_string(),
          kind: crate::ts_type::TsTypeDefKind::TypeRef(
            crate::ts_type::TsTypeRefDef {
              type_params: Some(Box::new([TsTypeDef::keyword("void")])),
              type_name: "Promise".to_string(),
              resolution: None,
            },
          ),
        })
      } else {
        Some(TsTypeDef::keyword("void"))
      }
    }
    None => None,
  };

  let type_params = maybe_type_param_decl_to_type_param_defs(
    module_info,
    function.type_params.as_deref(),
  );

  let has_body = function.body.is_some();

  let decorators = decorators_to_defs(module_info, &function.decorators);

  FunctionDef {
    def_name,
    params,
    return_type: maybe_return_type,
    has_body,
    is_async: function.is_async,
    is_generator: function.is_generator,
    type_params,
    decorators,
  }
}

pub fn arrow_to_function_def(
  module_info: &EsModuleInfo,
  arrow: &deno_ast::swc::ast::ArrowExpr,
) -> FunctionDef {
  let params = arrow
    .params
    .iter()
    .map(|pat| crate::params::pat_to_param_def(module_info, pat))
    .collect();

  let maybe_return_type = arrow
    .return_type
    .as_deref()
    .map(|return_type| TsTypeDef::new(module_info, &return_type.type_ann));

  let type_params = maybe_type_param_decl_to_type_param_defs(
    module_info,
    arrow.type_params.as_deref(),
  );

  FunctionDef {
    def_name: None,
    params,
    return_type: maybe_return_type,
    has_body: true,
    is_async: arrow.is_async,
    is_generator: arrow.is_generator,
    type_params,
    decorators: Box::new([]),
  }
}

pub fn get_doc_for_fn_decl(
  module_info: &EsModuleInfo,
  fn_decl: &deno_ast::swc::ast::FnDecl,
) -> FunctionDef {
  function_to_function_def(module_info, &fn_decl.function, None)
}

fn get_return_stmt_with_arg_from_function(
  func: &deno_ast::swc::ast::Function,
) -> Option<&ReturnStmt> {
  let body = func.body.as_ref()?;
  let stmt = get_return_stmt_with_arg_from_stmts(&body.stmts)?;
  debug_assert!(stmt.arg.is_some());
  Some(stmt)
}

fn get_return_stmt_with_arg_from_stmts(stmts: &[Stmt]) -> Option<&ReturnStmt> {
  for stmt in stmts {
    if let Some(return_stmt) = get_return_stmt_with_arg_from_stmt(stmt) {
      return Some(return_stmt);
    }
  }

  None
}

fn get_return_stmt_with_arg_from_stmt(stmt: &Stmt) -> Option<&ReturnStmt> {
  match stmt {
    Stmt::Block(n) => get_return_stmt_with_arg_from_stmts(&n.stmts),
    Stmt::With(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::Return(n) => {
      if n.arg.is_none() {
        None
      } else {
        Some(n)
      }
    }
    Stmt::Labeled(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::If(n) => get_return_stmt_with_arg_from_stmt(&n.cons),
    Stmt::Switch(n) => n
      .cases
      .iter()
      .find_map(|case| get_return_stmt_with_arg_from_stmts(&case.cons)),
    Stmt::Try(n) => get_return_stmt_with_arg_from_stmts(&n.block.stmts)
      .or_else(|| {
        n.handler
          .as_ref()
          .and_then(|h| get_return_stmt_with_arg_from_stmts(&h.body.stmts))
      })
      .or_else(|| {
        n.finalizer
          .as_ref()
          .and_then(|f| get_return_stmt_with_arg_from_stmts(&f.stmts))
      }),
    Stmt::While(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::DoWhile(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::For(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::ForIn(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::ForOf(n) => get_return_stmt_with_arg_from_stmt(&n.body),
    Stmt::Break(_)
    | Stmt::Continue(_)
    | Stmt::Throw(_)
    | Stmt::Debugger(_)
    | Stmt::Decl(_)
    | Stmt::Expr(_)
    | Stmt::Empty(_) => None,
  }
}
