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

use super::Change;
use crate::js_doc::JsDoc;
use crate::js_doc::JsDocTag;
use serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsDocDiff {
  #[serde(skip_serializing_if = "Option::is_none")]
  pub doc_change: Option<Change<Option<Box<str>>>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub tags_change: Option<TagsDiff>,
}

impl JsDocDiff {
  pub fn diff(old: &JsDoc, new: &JsDoc) -> Option<Self> {
    let doc_change = if old.doc != new.doc {
      Some(Change::new(old.doc.clone(), new.doc.clone()))
    } else {
      None
    };

    let tags_change = TagsDiff::diff(&old.tags, &new.tags);

    if doc_change.is_none() && tags_change.is_none() {
      return None;
    }

    Some(JsDocDiff {
      doc_change,
      tags_change,
    })
  }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TagsDiff {
  #[serde(skip_serializing_if = "Vec::is_empty", default)]
  pub added: Vec<JsDocTag>,
  #[serde(skip_serializing_if = "Vec::is_empty", default)]
  pub removed: Vec<JsDocTag>,
  #[serde(skip_serializing_if = "Vec::is_empty", default)]
  pub modified: Vec<TagDiff>,
}

impl TagsDiff {
  pub fn diff(old: &[JsDocTag], new: &[JsDocTag]) -> Option<Self> {
    let mut added = Vec::new();
    let mut removed = Vec::new();
    let mut modified = Vec::new();
    let mut consumed_old = vec![false; old.len()];
    let mut consumed_new = vec![false; new.len()];

    for (ni, new_tag) in new.iter().enumerate() {
      let matching_old = old
        .iter()
        .enumerate()
        .find(|(oi, t)| !consumed_old[*oi] && tags_same_kind(t, new_tag));
      match matching_old {
        Some((oi, old_tag)) if old_tag != new_tag => {
          consumed_old[oi] = true;
          consumed_new[ni] = true;
          modified.push(TagDiff {
            old: old_tag.clone(),
            new: new_tag.clone(),
          });
        }
        Some((oi, _)) => {
          consumed_old[oi] = true;
          consumed_new[ni] = true;
        }
        None => {
          added.push(new_tag.clone());
        }
      }
    }

    for (oi, old_tag) in old.iter().enumerate() {
      if !consumed_old[oi] {
        removed.push(old_tag.clone());
      }
    }

    if added.is_empty() && removed.is_empty() && modified.is_empty() {
      return None;
    }

    Some(TagsDiff {
      added,
      removed,
      modified,
    })
  }
}

fn tags_same_kind(a: &JsDocTag, b: &JsDocTag) -> bool {
  use JsDocTag::*;

  match (a, b) {
    (Callback { name: n1, .. }, Callback { name: n2, .. }) => n1 == n2,
    (Category { .. }, Category { .. }) => true,
    (Constructor, Constructor) => true,
    (Default { value: v1, .. }, Default { value: v2, .. }) => v1 == v2,
    (Deprecated { .. }, Deprecated { .. }) => true,
    (Enum { ts_type: t1, .. }, Enum { ts_type: t2, .. }) => t1 == t2,
    (Example { .. }, Example { .. }) => true,
    (Experimental, Experimental) => true,
    (Extends { ts_type: t1, .. }, Extends { ts_type: t2, .. }) => t1 == t2,
    (Ignore, Ignore) => true,
    (Internal, Internal) => true,
    (Module { .. }, Module { .. }) => true,
    (Param { name: n1, .. }, Param { name: n2, .. }) => n1 == n2,
    (Public, Public) => true,
    (Private, Private) => true,
    (Property { name: n1, .. }, Property { name: n2, .. }) => n1 == n2,
    (Protected, Protected) => true,
    (ReadOnly, ReadOnly) => true,
    (Return { .. }, Return { .. }) => true,
    (Tags { .. }, Tags { .. }) => true,
    (Template { name: n1, .. }, Template { name: n2, .. }) => n1 == n2,
    (This { ts_type: t1, .. }, This { ts_type: t2, .. }) => t1 == t2,
    (Throws { ts_type: t1, .. }, Throws { ts_type: t2, .. }) => t1 == t2,
    (TypeDef { name: n1, .. }, TypeDef { name: n2, .. }) => n1 == n2,
    (TypeRef { ts_type: t1, .. }, TypeRef { ts_type: t2, .. }) => t1 == t2,
    (See { .. }, See { .. }) => true,
    (Since { .. }, Since { .. }) => true,
    (Priority { .. }, Priority { .. }) => true,
    (Unsupported { .. }, Unsupported { .. }) => true,
    _ => false,
  }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TagDiff {
  pub old: JsDocTag,
  pub new: JsDocTag,
}
