//! two kinds of atoms
//! - defined with `defatom`, which is global atom that retains after hot swapping
//! - defined with `atom`, which is barely a piece of local mutable state

use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock, Mutex};

use cirru_edn::EdnTag;

use crate::builtins::meta::type_of;

use crate::calcit::{Calcit, CalcitErr, CalcitErrKind, CalcitImport, CalcitList, CalcitScope};
use crate::{call_stack::CallStackList, runner};

pub(crate) type ValueAndListeners = (Calcit, HashMap<EdnTag, Calcit>);

type RefListeners = HashMap<Arc<str>, Arc<Mutex<ValueAndListeners>>>;

static REFS_DICT: LazyLock<Mutex<RefListeners>> = LazyLock::new(|| Mutex::new(HashMap::new()));

fn modify_ref(locked_pair: Arc<Mutex<ValueAndListeners>>, v: Calcit, call_stack: &CallStackList) -> Result<(), CalcitErr> {
  let (listeners, prev) = {
    let mut pair = locked_pair.lock().expect("read ref");
    let prev = pair.0.to_owned();
    if prev == v {
      // not need to modify
      return Ok(());
    }
    let listeners = pair.1.to_owned();
    v.clone_into(&mut pair.0);

    (listeners, prev)
  };

  for f in listeners.values() {
    match f {
      Calcit::Fn { info, .. } => {
        runner::run_fn(&[v.to_owned(), prev.to_owned()], info, call_stack)?;
      }
      a => {
        return Err(CalcitErr::use_msg_stack_location(
          CalcitErrKind::Type,
          format!("modify-ref expected a function to trigger after `reset!`, but received: {a}"),
          call_stack,
          a.get_location(),
        ));
      }
    }
  }
  Ok(())
}

/// syntax to prevent expr re-evaluating
pub fn defatom(expr: &CalcitList, scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  match (expr.first(), expr.get(1)) {
    (Some(Calcit::Symbol { sym, info, .. }), Some(code)) => {
      let mut path: String = (*info.at_ns).to_owned();
      path.push('/');
      path.push_str(sym);

      let path_info: Arc<str> = path.into();

      // println!("defatom symbol {:?}", path_info);

      let defined = {
        let dict = REFS_DICT.lock().expect("read refs");
        dict.get(&path_info).map(ToOwned::to_owned)
        // need to release lock before calling `evaluate_expr`
      };

      match defined {
        Some(v) => Ok(Calcit::Ref(path_info, v.to_owned())),
        None => {
          let v = runner::evaluate_expr(code, scope, file_ns, call_stack)?;
          let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
          let mut dict = REFS_DICT.lock().expect("read refs");
          dict.insert(path_info.to_owned(), pair_value.to_owned());
          Ok(Calcit::Ref(path_info, pair_value))
        }
      }
    }

    (Some(Calcit::Import(CalcitImport { def, ns, .. })), Some(code)) => {
      let mut path: String = ns.to_string();
      path.push('/');
      path.push_str(def);

      let path_info: Arc<str> = path.into();

      // println!("defatom import {:?}", path_info);

      let defined = {
        let dict = REFS_DICT.lock().expect("read refs");
        dict.get(&path_info).map(ToOwned::to_owned)
        // need to release lock before calling `evaluate_expr`
      };

      match defined {
        Some(v) => Ok(Calcit::Ref(path_info, v.to_owned())),
        None => {
          let v = runner::evaluate_expr(code, scope, file_ns, call_stack)?;
          let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
          let mut dict = REFS_DICT.lock().expect("read refs");
          dict.insert(path_info.to_owned(), pair_value.to_owned());
          Ok(Calcit::Ref(path_info, pair_value))
        }
      }
    }
    (Some(a), Some(b)) => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Type,
      format!("defatom expected a symbol and an expression, but received: {a} , {b}"),
      call_stack,
      a.get_location().or_else(|| b.get_location()),
    )),
    _ => Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      "defatom expected 2 nodes, but received none",
      call_stack,
    )),
  }
}

/// dead simple counter for ID generator, better use nanoid in business
static ATOM_ID_GEN: AtomicUsize = AtomicUsize::new(0);

/// proc
pub fn atom(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(value) => Ok(quick_build_atom(value.to_owned())),
    _ => {
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::Atom).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(
        crate::calcit::CalcitErrKind::Arity,
        "atom requires 1 argument (initial value), but received none".to_string(),
        hint,
      )
    }
  }
}

/// this is a internal helper for `atom`, not exposed to Calcit
pub fn quick_build_atom(v: Calcit) -> Calcit {
  let atom_idx = ATOM_ID_GEN.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
  let path: String = format!("atom-{atom_idx}");

  let path_info: Arc<str> = path.into();
  // println!("atom {:?}", path_info);

  let pair_value = Arc::new(Mutex::new((v, HashMap::new())));
  Calcit::Ref(path_info, pair_value)
}

/// previously `deref`, but `deref` now turned into a function calling `&atom:deref`
pub fn atom_deref(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match xs.first() {
    Some(Calcit::Ref(_path, locked_pair)) => {
      // println!("deref import {:?}", _path);
      let pair = (**locked_pair).lock().expect("read pair from block");
      Ok(pair.0.to_owned())
    }
    Some(a) => {
      let msg = format!(
        "&atom:deref requires a ref (atom), but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AtomDeref).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
    }
    _ => {
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AtomDeref).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(
        crate::calcit::CalcitErrKind::Arity,
        "&atom:deref requires 1 argument (a ref), but received none".to_string(),
        hint,
      )
    }
  }
}

/// need to be syntax since triggering internal functions requires program data
pub fn reset_bang(expr: &CalcitList, scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  if expr.len() < 2 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "reset! expected 2 arguments, but received:", &expr.to_vec());
  }
  // println!("reset! {:?}", expr[0]);
  let target = runner::evaluate_expr(&expr[0], scope, file_ns, call_stack)?;
  let new_value = runner::evaluate_expr(&expr[1], scope, file_ns, call_stack)?;
  match (target, &new_value) {
    (Calcit::Ref(_path, locked_pair), v) => {
      // println!("reset defatom {:?} {}", _path, v);
      modify_ref(locked_pair, v.to_owned(), call_stack)?;
      Ok(Calcit::Nil)
    }
    // if reset! called before deref, we need to trigger the thunk
    (Calcit::Thunk(thunk), _) => match &expr[0] {
      Calcit::Symbol { .. } | Calcit::Import(CalcitImport { .. }) => {
        let ret = thunk.evaluated(scope, call_stack)?;
        match (ret, &new_value) {
          (Calcit::Ref(_path, locked_pair), v) => {
            // println!("reset defatom {:?} {}", _path, v);
            modify_ref(locked_pair, v.to_owned(), call_stack)?;
            Ok(Calcit::Nil)
          }
          (a, _) => Err(CalcitErr::use_msg_stack_location(
            CalcitErrKind::Type,
            format!("reset! expected a ref, but received: {a}"),
            call_stack,
            a.get_location(),
          )),
        }
      }
      _ => CalcitErr::err_str(
        CalcitErrKind::Type,
        format!("reset! expected a symbol, but received: {:?}", expr[0]),
      ),
    },
    (a, b) => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Type,
      format!("reset! expected a ref and a value, but received: {a} {b}"),
      call_stack,
      a.get_location().or_else(|| b.get_location()),
    )),
  }
}

pub fn add_watch(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1), xs.get(2)) {
    (Some(Calcit::Ref(_path, locked_pair)), Some(Calcit::Tag(k)), Some(f @ Calcit::Fn { .. })) => {
      let mut pair = locked_pair.lock().expect("trying to modify locked pair");
      match pair.1.get(k) {
        Some(_) => CalcitErr::err_str(
          CalcitErrKind::Unexpected,
          format!("add-watch failed: listener with key `{k}` already existed"),
        ),
        None => {
          pair.1.insert(k.to_owned(), f.to_owned());
          Ok(Calcit::Nil)
        }
      }
    }
    (Some(Calcit::Ref(..)), Some(Calcit::Tag(_)), Some(a)) => {
      let msg = format!(
        "add-watch requires a function as 3rd argument, but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
    }
    (Some(Calcit::Ref(..)), Some(a), Some(_)) => {
      let msg = format!(
        "add-watch requires a tag as 2nd argument (watch key), but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
    }
    (Some(a), _, _) => {
      let msg = format!(
        "add-watch requires a ref (atom) as 1st argument, but received: {}",
        type_of(&[a.to_owned()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
    }
    (a, b, c) => {
      let msg = format!(
        "add-watch requires 3 arguments (ref, tag-key, function), but received: {}",
        if a.is_none() {
          0
        } else if b.is_none() {
          1
        } else if c.is_none() {
          2
        } else {
          3
        }
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::AddWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Arity, msg, hint)
    }
  }
}

pub fn remove_watch(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::Ref(_path, locked_pair)), Some(Calcit::Tag(k))) => {
      let mut pair = locked_pair.lock().expect("trying to modify locked pair");

      match pair.1.get(k) {
        None => CalcitErr::err_str(
          CalcitErrKind::Unexpected,
          format!("remove-watch failed: listener with key `{k}` not found"),
        ),
        Some(_) => {
          pair.1.remove(k);
          Ok(Calcit::Nil)
        }
      }
    }
    (Some(a), Some(b)) => {
      let msg = format!(
        "remove-watch requires a ref and a tag, but received: {} and {}",
        type_of(&[a.to_owned()])?.lisp_str(),
        type_of(&[b.to_owned()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::RemoveWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Type, msg, hint)
    }
    (a, b) => {
      let msg = format!(
        "remove-watch requires 2 arguments (ref and tag-key), but received: {} arguments",
        if a.is_none() {
          0
        } else if b.is_none() {
          1
        } else {
          2
        }
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::RemoveWatch).unwrap_or_default();
      crate::calcit::CalcitErr::err_str_with_hint(crate::calcit::CalcitErrKind::Arity, msg, hint)
    }
  }
}
