pub mod preprocess;
pub mod track;

use std::cell::RefCell;
use std::sync::Arc;
use std::vec;

use crate::builtins;
use crate::calcit::{
  CORE_NS, Calcit, CalcitArgLabel, CalcitErr, CalcitErrKind, CalcitFn, CalcitFnArgs, CalcitImport, CalcitList, CalcitLocal, CalcitProc,
  CalcitScope, CalcitSyntax, MethodKind, NodeLocation,
};
use crate::call_stack::{CallStackList, StackKind, using_stack};
use crate::data::cirru;
use crate::program;
use crate::util::string::has_ns_part;

fn build_runtime_cell_error(ns: &str, def: &str, call_stack: &CallStackList, cell: program::RuntimeCell) -> CalcitErr {
  match cell {
    program::RuntimeCell::Resolving => CalcitErr::use_msg_stack(
      CalcitErrKind::Unexpected,
      format!("definition is still resolving: {ns}/{def}"),
      call_stack,
    ),
    program::RuntimeCell::Errored(message) => CalcitErr::use_msg_stack(
      CalcitErrKind::Unexpected,
      format!("definition is in errored state: {ns}/{def}\n{message}"),
      call_stack,
    ),
    program::RuntimeCell::Cold | program::RuntimeCell::Lazy { .. } | program::RuntimeCell::Ready(_) => CalcitErr::use_msg_stack(
      CalcitErrKind::Unexpected,
      format!("unexpected runtime state for {ns}/{def}"),
      call_stack,
    ),
  }
}

fn require_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  eval_symbol_from_program(sym, ns, call_stack).map(|value| value.expect("value"))
}

fn lookup_symbol_in_program_namespaces(sym: &str, file_ns: &str, call_stack: &CallStackList) -> Result<Option<Calcit>, CalcitErr> {
  if let Some(value) = eval_symbol_from_program(sym, CORE_NS, call_stack)? {
    Ok(Some(value))
  } else if let Some(value) = eval_symbol_from_program(sym, file_ns, call_stack)? {
    Ok(Some(value))
  } else {
    Ok(None)
  }
}

fn resolve_runtime_or_compiled_def(
  ns: &str,
  def: &str,
  def_id: Option<program::DefId>,
  call_stack: &CallStackList,
) -> Result<Option<Calcit>, CalcitErr> {
  program::resolve_runtime_or_compiled_def(ns, def, def_id, program::RuntimeResolveMode::Strict, call_stack).map_err(|err| match err {
    program::RuntimeResolveError::RuntimeCell(cell) => build_runtime_cell_error(ns, def, call_stack, cell),
    program::RuntimeResolveError::Eval(failure) => failure,
    program::RuntimeResolveError::RuntimeSeed(message) => CalcitErr::use_msg_stack(
      CalcitErrKind::Unexpected,
      format!("failed to seed runtime for {ns}/{def}: {message}"),
      call_stack,
    ),
  })
}

fn format_fn_arg_labels(args: &CalcitFnArgs) -> String {
  match args {
    CalcitFnArgs::Args(xs) => xs
      .iter()
      .map(|idx| CalcitLocal::read_name(*idx).to_string())
      .collect::<Vec<_>>()
      .join(" "),
    CalcitFnArgs::MarkedArgs(xs) => xs.iter().map(ToString::to_string).collect::<Vec<_>>().join(" "),
  }
}

fn format_runtime_values(values: &[Calcit]) -> String {
  if values.is_empty() {
    "[]".to_owned()
  } else {
    format!("{}", CalcitList::from(values))
  }
}

fn build_fn_arity_mismatch_error(info: &CalcitFn, values: &[Calcit], call_stack: &CallStackList, phase: &str) -> CalcitErr {
  let expected = info.args.param_len();
  let actual = values.len();
  let def_ref = info
    .def_ref
    .as_ref()
    .map(|r| format!("{}/{}", r.def_ns, r.def_name))
    .unwrap_or_else(|| format!("{}/{}", info.def_ns, info.name));
  let msg = format!(
    "function arity mismatch during {phase}: `{}` expected {expected} argument(s), got {actual}\n  params: ({})\n  values: {}\n  fn-namespace: {}\n  fn-name: {}\n  def-ref: {}",
    info.name,
    format_fn_arg_labels(info.args.as_ref()),
    format_runtime_values(values),
    info.def_ns,
    info.name,
    def_ref,
  );
  CalcitErr::use_msg_stack(CalcitErrKind::Unexpected, msg, call_stack)
}

pub fn evaluate_expr(expr: &Calcit, scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  // println!("eval code: {}", expr.lisp_str());
  use Calcit::*;

  match expr {
    Nil
    | Bool(_)
    | Number(_)
    | Registered(_)
    | Tag(_)
    | Str(_)
    | Ref(..)
    | Tuple { .. }
    | Buffer(..)
    | BufList(..)
    | CirruQuote(..)
    | Proc(_)
    | Macro { .. }
    | Fn { .. }
    | Struct { .. }
    | Enum { .. }
    | Trait { .. }
    | Impl { .. }
    | Syntax(_, _)
    | Method(..)
    | AnyRef(..) => Ok(expr.to_owned()),

    Thunk(thunk) => Ok(thunk.evaluated(scope, call_stack)?),
    Symbol { sym, info, location, .. } => {
      // println!("[Warn] slow path reading symbol: {}", sym);
      evaluate_symbol(sym, scope, &info.at_ns, &info.at_def, location, call_stack)
    }
    Local(CalcitLocal { idx, sym, .. }) => evaluate_symbol_from_scope(*idx, sym, scope),
    Import(CalcitImport { ns, def, def_id, .. }) => evaluate_symbol_from_program(def, ns, *def_id, call_stack),
    List(xs) => match xs.first() {
      None => Err(CalcitErr::use_msg_stack_location(
        CalcitErrKind::Arity,
        format!("cannot evaluate empty expr: {expr}"),
        call_stack,
        expr.get_location(),
      )),
      Some(x) => {
        // println!("eval expr: {}", expr.lisp_str());
        // println!("eval expr x: {}", x);

        if x.is_expr_evaluated() {
          call_expr(x, xs, scope, file_ns, call_stack, false)
        } else {
          let v = evaluate_expr(x, scope, file_ns, call_stack)?;
          call_expr(&v, xs, scope, file_ns, call_stack, false)
        }
      }
    },
    Recur(_) => unreachable!("recur not expected to be from symbol"),
    RawCode(_, code) => unreachable!("raw code `{}` cannot be called", code),
    Set(_) => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Unexpected,
      "unexpected set for expr",
      call_stack,
      expr.get_location(),
    )),
    Map(_) => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Unexpected,
      "unexpected map for expr",
      call_stack,
      expr.get_location(),
    )),
    Record { .. } => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Unexpected,
      "unexpected record for expr",
      call_stack,
      expr.get_location(),
    )),
  }
}

pub fn call_expr(
  v: &Calcit,
  xs: &CalcitList,
  scope: &CalcitScope,
  file_ns: &str,
  call_stack: &CallStackList,
  spreading: bool,
) -> Result<Calcit, CalcitErr> {
  // println!("calling expr: {}", xs);
  match v {
    Calcit::Proc(p) => {
      let values = if spreading {
        evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)?
      } else {
        evaluate_args_from(xs, 1, scope, file_ns, call_stack)?
      };
      builtins::handle_proc(*p, &values, call_stack)
    }
    Calcit::Syntax(s, def_ns) => {
      let rest_nodes = xs.skip(1).expect("expected syntax rest nodes");
      if using_stack() {
        let next_stack = call_stack.extend_owned(def_ns, s.as_ref(), StackKind::Syntax, Calcit::from(xs), rest_nodes.to_vec());
        builtins::handle_syntax(s, &rest_nodes, scope, file_ns, &next_stack).map_err(|e| {
          if e.stack.is_empty() {
            let mut e2 = e;
            call_stack.clone_into(&mut e2.stack);
            e2
          } else {
            e
          }
        })
      } else {
        builtins::handle_syntax(s, &rest_nodes, scope, file_ns, call_stack)
      }
    }
    Calcit::Method(name, kind) => {
      if matches!(kind, MethodKind::Invoke(_)) {
        let values = if spreading {
          evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)?
        } else {
          evaluate_args_from(xs, 1, scope, file_ns, call_stack)?
        };
        if using_stack() {
          let next_stack = call_stack.extend(file_ns, name, StackKind::Method, &Calcit::Nil, &values);
          builtins::meta::invoke_method(name, &values, &next_stack)
        } else {
          builtins::meta::invoke_method(name, &values, call_stack)
        }
      } else if matches!(kind, MethodKind::TagAccess) {
        if xs.len() == 2 {
          let obj = evaluate_expr(&xs[1], scope, file_ns, call_stack)?;
          let tag = evaluate_expr(&Calcit::tag(name), scope, file_ns, call_stack)?;
          if let Calcit::Map(m) = obj {
            match m.get(&tag) {
              Some(value) => Ok(value.to_owned()),
              None => Ok(Calcit::Nil),
            }
          } else {
            Err(CalcitErr::use_msg_stack_location(
              CalcitErrKind::Type,
              format!("expected a hashmap, got: {obj}"),
              call_stack,
              obj.get_location(),
            ))
          }
        } else {
          Err(CalcitErr::use_msg_stack_location(
            CalcitErrKind::Arity,
            format!("tag-accessor takes only 1 argument, {xs}"),
            call_stack,
            xs.first().and_then(|node| node.get_location()),
          ))
        }
      } else {
        Err(CalcitErr::use_msg_stack_location(
          CalcitErrKind::Unexpected,
          format!(
            "method kind `{kind}` (`.{prefix}{name}`) is only available in JS codegen, not supported in Rust runtime. \
             Use `cr js` to compile to JS, or avoid `.!` / `.-` syntax in server-side code. \
             Expression: `{xs}`",
            prefix = match kind {
              MethodKind::InvokeNative => "!",
              MethodKind::InvokeNativeOptional => "?!",
              MethodKind::Access => "-",
              MethodKind::AccessOptional => "?-",
              _ => "?",
            },
          ),
          call_stack,
          xs.first().and_then(|node| node.get_location()),
        ))
      }
    }
    Calcit::Fn { info, .. } => {
      let values = if spreading {
        evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)?
      } else {
        evaluate_args_from(xs, 1, scope, file_ns, call_stack)?
      };
      if using_stack() {
        let next_stack = call_stack.extend(&info.def_ns, &info.name, StackKind::Fn, &Calcit::from(xs), &values);
        run_fn_owned(values, info, &next_stack)
      } else {
        run_fn_owned(values, info, call_stack)
      }
    }
    Calcit::Macro { info, .. } => {
      println!(
        "[Warn] macro should already be handled during preprocessing: {}",
        &Calcit::from(xs.to_owned()).lisp_str()
      );

      let mut current_values: Vec<Calcit> = xs.iter().skip(1).cloned().collect();

      let next_stack = if using_stack() {
        call_stack.extend_owned(&info.def_ns, &info.name, StackKind::Macro, Calcit::from(xs), current_values.clone())
      } else {
        call_stack.to_owned()
      };

      // TODO moving to preprocess
      // println!("eval macro: {} {}", x, expr.lisp_str()));
      // println!("macro... {} {}", x, CrListWrap(current_values.to_owned()));

      let mut body_scope = CalcitScope::default();

      Ok(loop {
        // need to handle recursion
        bind_marked_args(&mut body_scope, &info.args, &current_values, call_stack)?;
        let code = evaluate_lines(info.body.as_ref().as_slice(), &body_scope, &info.def_ns, &next_stack)?;
        match code {
          Calcit::Recur(ys) => {
            current_values = ys;
          }
          _ => {
            // println!("gen code: {} {}", x, &code.lisp_str()));
            break evaluate_expr(&code, scope, file_ns, &next_stack)?;
          }
        }
      })
    }
    Calcit::Tag(k) => {
      if xs.len() == 2 {
        let v = evaluate_expr(&xs[1], scope, file_ns, call_stack)?;

        match &v {
          Calcit::Map(m) => match m.get(&Calcit::Tag(k.to_owned())) {
            Some(value) => Ok(value.to_owned()),
            None => Ok(Calcit::Nil),
          },
          Calcit::Record(record) => Ok(record.get(k.ref_str()).cloned().unwrap_or(Calcit::Nil)),
          _ => Err(CalcitErr::use_msg_stack_location(
            CalcitErrKind::Type,
            format!("expected a hashmap or record, got: {v}"),
            call_stack,
            v.get_location(),
          )),
        }
      } else {
        Err(CalcitErr::use_msg_stack_location(
          CalcitErrKind::Arity,
          format!("tag only takes 1 argument, got: {}", xs.len().saturating_sub(1)),
          call_stack,
          xs.first().and_then(|node| node.get_location()),
        ))
      }
    }
    Calcit::Registered(alias) => {
      let values = if spreading {
        evaluate_spreaded_args_from(xs, 1, scope, file_ns, call_stack)?
      } else {
        evaluate_args_from(xs, 1, scope, file_ns, call_stack)?
      };
      builtins::call_registered_proc(alias, values, call_stack).map_err(|e| {
        if e.kind == CalcitErrKind::Var {
          CalcitErr::use_msg_stack_location(
            CalcitErrKind::Var,
            format!("cannot evaluate symbol directly: {file_ns}/{alias}"),
            call_stack,
            xs.first().and_then(|node| node.get_location()),
          )
        } else {
          e
        }
      })
    }
    a => {
      let location = xs
        .first()
        .and_then(|node| node.get_location())
        .or_else(|| a.get_location())
        .or_else(|| xs.drop_left().first().and_then(|node| node.get_location()));
      let expr_one_liner = {
        let expr = Calcit::from(xs.to_owned());
        match cirru::calcit_to_cirru(&expr) {
          Ok(v) => match cirru_parser::format_expr_one_liner(&v) {
            Ok(s) => s,
            Err(_) => expr.lisp_str(),
          },
          Err(_) => expr.lisp_str(),
        }
      };
      let operator_desc = match cirru::calcit_to_cirru(a) {
        Ok(v) => match cirru_parser::format_expr_one_liner(&v) {
          Ok(s) => s,
          Err(_) => a.lisp_str(),
        },
        Err(_) => a.lisp_str(),
      };
      Err(CalcitErr::use_msg_stack_location_with_hint(
        CalcitErrKind::Type,
        format!("cannot be used as operator: {operator_desc} in {expr_one_liner}"),
        call_stack,
        location,
        "Possible: check if a leading `,` is needed to prevent a single-line call of Cirru syntax.",
      ))
    }
  }
}

pub fn evaluate_symbol(
  sym: &str,
  scope: &CalcitScope,
  file_ns: &str,
  at_def: &str,
  location: &Option<Arc<Vec<u16>>>,
  call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
  let v = match parse_ns_def(sym) {
    Some((ns_part, def_part)) => match program::lookup_ns_target_in_import(file_ns, &ns_part) {
      Some(target_ns) => require_symbol_from_program(&def_part, &target_ns, call_stack),
      None => Err(CalcitErr::use_msg_stack_location(
        CalcitErrKind::Var,
        format!("unknown ns target: {ns_part}/{def_part}"),
        call_stack,
        Some(NodeLocation::new(
          Arc::from(file_ns),
          Arc::from(at_def),
          location.to_owned().unwrap_or_default(),
        )),
      )),
    },
    None => {
      if let Ok(v) = sym.parse::<CalcitSyntax>() {
        Ok(Calcit::Syntax(v, file_ns.into()))
      } else if let Some(v) = scope.get_by_name(sym) {
        // although scope is detected first, it would trigger warning during preprocess
        Ok(v.to_owned())
      } else if let Ok(p) = sym.parse::<CalcitProc>() {
        Ok(Calcit::Proc(p))
      } else if builtins::is_registered_proc(sym) {
        Ok(Calcit::Registered(sym.into()))
      } else if let Some(v) = lookup_symbol_in_program_namespaces(sym, file_ns, call_stack)? {
        Ok(v)
      } else if let Some(target_ns) = program::lookup_def_target_in_import(file_ns, sym) {
        require_symbol_from_program(sym, &target_ns, call_stack)
      } else {
        let vars = scope.get_names();
        Err(CalcitErr::use_msg_stack_location(
          CalcitErrKind::Var,
          format!("unknown symbol `{sym}` in {vars}"),
          call_stack,
          Some(NodeLocation::new(
            Arc::from(file_ns),
            Arc::from(at_def),
            location.to_owned().unwrap_or_default(),
          )),
        ))
      }
    }
  }?;
  match v {
    Calcit::Thunk(thunk) => thunk.evaluated(scope, call_stack),
    _ => Ok(v),
  }
}

pub fn evaluate_symbol_from_scope(idx: u16, sym: &str, scope: &CalcitScope) -> Result<Calcit, CalcitErr> {
  // Fast path: resolve by compiled local slot index.
  if let Some(v) = scope.get(idx) {
    return Ok(v.to_owned());
  }

  // Defensive fallback: resolve by symbol name so runtime does not panic when
  // local slot numbering drifts in edge macro/preprocess paths.
  if let Some(v) = scope.get_by_name(sym) {
    return Ok(v.to_owned());
  }

  let vars = scope.get_names();
  CalcitErr::err_str(CalcitErrKind::Var, format!("unknown local `{sym}`(#{idx}) in scope {vars}"))
}

/// a quick path of evaluating symbols, without checking scope and import
pub fn evaluate_symbol_from_program(
  sym: &str,
  file_ns: &str,
  def_id: Option<u32>,
  call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
  let v0 = resolve_runtime_or_compiled_def(file_ns, sym, def_id.map(program::DefId), call_stack)?;
  let v = if let Some(v) = v0 {
    v
  } else if let Some(v) = lookup_symbol_in_program_namespaces(sym, file_ns, call_stack)? {
    v
  } else {
    return Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Var,
      format!("expected symbol `{sym}` from path `{file_ns}`, this is a quick path, should succeed"),
      call_stack,
    ));
  };
  Ok(v)
}

pub fn parse_ns_def(s: &str) -> Option<(Arc<str>, Arc<str>)> {
  if !has_ns_part(s) {
    return None;
  }
  let pieces: Vec<&str> = s.split('/').collect();
  if pieces.len() == 2 {
    if !pieces[0].is_empty() && !pieces[1].is_empty() {
      Some((pieces[0].into(), pieces[1].into()))
    } else {
      None
    }
  } else {
    None
  }
}

/// resolve a program symbol to an available value for namespace lookup paths
pub fn eval_symbol_from_program(sym: &str, ns: &str, call_stack: &CallStackList) -> Result<Option<Calcit>, CalcitErr> {
  if let Some(v) = resolve_runtime_or_compiled_def(ns, sym, None, call_stack)? {
    return Ok(Some(v));
  }
  if program::has_def_code(ns, sym) {
    let warnings: RefCell<Vec<_>> = RefCell::new(vec![]);
    preprocess::ensure_ns_def_compiled(ns, sym, &warnings, call_stack)?;
    return resolve_runtime_or_compiled_def(ns, sym, None, call_stack);
  }
  Ok(None)
}

pub fn run_fn(values: &[Calcit], info: &CalcitFn, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  let mut body_scope = (*info.scope).to_owned();
  match &*info.args {
    CalcitFnArgs::Args(args) => {
      if args.len() != values.len() {
        return Err(build_fn_arity_mismatch_error(info, values, call_stack, "call"));
      }
      for (&arg, value) in args.iter().zip(values) {
        body_scope.insert_mut(arg, value.to_owned());
      }
    }
    CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, values, call_stack)?,
  }

  let v = evaluate_lines(info.body.as_slice(), &body_scope, &info.def_ns, call_stack)?;

  if let Calcit::Recur(xs) = v {
    let mut current_values = xs.to_vec();
    loop {
      match &*info.args {
        CalcitFnArgs::Args(args) => {
          if args.len() != current_values.len() {
            return Err(build_fn_arity_mismatch_error(info, &current_values, call_stack, "recur"));
          }
          for (&arg, value) in args.iter().zip(&current_values) {
            body_scope.insert_mut(arg, value.to_owned());
          }
        }
        CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, &current_values, call_stack)?,
      }
      let v = evaluate_lines(info.body.as_slice(), &body_scope, &info.def_ns, call_stack)?;
      match v {
        Calcit::Recur(xs) => current_values = xs.to_vec(),
        result => return Ok(result),
      }
    }
  }
  Ok(v)
}

/// quick path for `run_fn` which takes ownership of values
pub fn run_fn_owned(values: Vec<Calcit>, info: &CalcitFn, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  let mut body_scope = (*info.scope).to_owned();
  match &*info.args {
    CalcitFnArgs::Args(args) => {
      if args.len() != values.len() {
        return Err(build_fn_arity_mismatch_error(info, &values, call_stack, "call"));
      }
      for (&arg, value) in args.iter().zip(values) {
        body_scope.insert_mut(arg, value);
      }
    }
    CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, &values, call_stack)?,
  }

  let v = evaluate_lines(&info.body, &body_scope, &info.def_ns, call_stack)?;

  if let Calcit::Recur(xs) = v {
    let mut current_values = xs.to_vec();
    loop {
      match &*info.args {
        CalcitFnArgs::Args(args) => {
          if args.len() != current_values.len() {
            return Err(build_fn_arity_mismatch_error(info, &current_values, call_stack, "recur"));
          }
          for (&arg, value) in args.iter().zip(current_values) {
            body_scope.insert_mut(arg, value);
          }
        }
        CalcitFnArgs::MarkedArgs(args) => bind_marked_args(&mut body_scope, args, &current_values, call_stack)?,
      }
      let v = evaluate_lines(&info.body, &body_scope, &info.def_ns, call_stack)?;
      match v {
        Calcit::Recur(xs) => current_values = xs.to_vec(),
        result => return Ok(result),
      }
    }
  }
  Ok(v)
}

/// syntax sugar for index value
#[derive(Debug, Default, PartialEq, PartialOrd)]
struct MutIndex(usize);

impl MutIndex {
  /// get value first, ant then increase value
  fn get_and_inc(&mut self) -> usize {
    let ret = self.0;
    self.0 += 1;
    ret
  }
}

/// create new scope by writing new args
/// notice that `&` is a mark for spreading, `?` for optional arguments
pub fn bind_marked_args(
  scope: &mut CalcitScope,
  args: &[CalcitArgLabel],
  values: &[Calcit],
  call_stack: &CallStackList,
) -> Result<(), CalcitErr> {
  // println!("bind args: {:?} {}", args, values);

  let mut spreading = false;
  let mut optional = false;

  let mut pop_args_idx = MutIndex::default();
  let mut pop_values_idx = MutIndex::default();

  while let Some(arg) = args.get(pop_args_idx.get_and_inc()) {
    if spreading {
      match arg {
        CalcitArgLabel::Idx(idx) => {
          let chunk = values[pop_values_idx.0..].to_vec();
          pop_values_idx.0 = values.len();
          scope.insert_mut(*idx, Calcit::from(CalcitList::Vector(chunk)));
          if pop_args_idx.0 < args.len() {
            return Err(CalcitErr::use_msg_stack(
              CalcitErrKind::Arity,
              format!("invalid argument declaration after `&` in signature `{}`", render_marked_args(args)),
              call_stack,
            ));
          }
        }
        _ => {
          return Err(CalcitErr::use_msg_stack(
            CalcitErrKind::Arity,
            format!("invalid argument declaration after `&` in signature `{}`", render_marked_args(args)),
            call_stack,
          ));
        }
      }
    } else {
      match arg {
        CalcitArgLabel::RestMark => spreading = true,
        CalcitArgLabel::OptionalMark => optional = true,
        CalcitArgLabel::Idx(idx) => match values.get(pop_values_idx.get_and_inc()) {
          Some(v) => {
            scope.insert_mut(*idx, v.to_owned());
          }
          None => {
            if optional {
              scope.insert_mut(*idx, Calcit::Nil);
            } else {
              return Err(CalcitErr::use_msg_stack(
                CalcitErrKind::Arity,
                format!(
                  "too few values `{values:?}` for arguments `{}`; missing required argument `{}`",
                  render_marked_args(args),
                  CalcitLocal::read_name(*idx)
                ),
                call_stack,
              ));
            }
          }
        },
      }
    }
  }

  if pop_values_idx.0 >= values.len() {
    Ok(())
  } else {
    let extra_count = values.len() - pop_values_idx.0;
    Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      format!(
        "too many values `{values:?}` for arguments `{}`; {} extra value(s) are not handled",
        render_marked_args(args),
        extra_count
      ),
      call_stack,
    ))
  }
}

fn render_marked_args(args: &[CalcitArgLabel]) -> String {
  let mut parts: Vec<String> = vec![];
  for arg in args {
    match arg {
      CalcitArgLabel::RestMark => parts.push("&".to_owned()),
      CalcitArgLabel::OptionalMark => parts.push("?".to_owned()),
      CalcitArgLabel::Idx(idx) => parts.push(CalcitLocal::read_name(*idx).to_string()),
    }
  }
  format!("({})", parts.join(" "))
}

pub fn evaluate_lines(lines: &[Calcit], scope: &CalcitScope, file_ns: &str, call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  let mut ret: Calcit = Calcit::Nil;
  for line in lines {
    match evaluate_expr(line, scope, file_ns, call_stack) {
      Ok(v) => ret = v,
      Err(e) => return Err(e),
    }
  }
  Ok(ret)
}

/// quick path evaluate symbols before calling a function, not need to check `&` for spreading
pub fn evaluate_args(
  items: CalcitList,
  scope: &CalcitScope,
  file_ns: &str,
  call_stack: &CallStackList,
) -> Result<Vec<Calcit>, CalcitErr> {
  evaluate_args_from(&items, 0, scope, file_ns, call_stack)
}

pub fn evaluate_args_from(
  items: &CalcitList,
  start: usize,
  scope: &CalcitScope,
  file_ns: &str,
  call_stack: &CallStackList,
) -> Result<Vec<Calcit>, CalcitErr> {
  let mut ret: Vec<Calcit> = Vec::with_capacity(items.len().saturating_sub(start));
  let mut idx = 0;
  items.traverse_result::<CalcitErr>(&mut |item| {
    if idx < start {
      idx += 1;
      return Ok(());
    }
    idx += 1;

    if item.is_expr_evaluated() {
      ret.push(item.to_owned());
    } else {
      ret.push(evaluate_expr(item, scope, file_ns, call_stack)?);
    }
    Ok(())
  })?;
  // println!("Evaluated args: {}", ret);
  Ok(ret)
}

// evaluate symbols before calling a function
/// notice that `&` is used to spread a list
pub fn evaluate_spreaded_args(
  items: CalcitList,
  scope: &CalcitScope,
  file_ns: &str,
  call_stack: &CallStackList,
) -> Result<Vec<Calcit>, CalcitErr> {
  evaluate_spreaded_args_from(&items, 0, scope, file_ns, call_stack)
}

pub fn evaluate_spreaded_args_from(
  items: &CalcitList,
  start: usize,
  scope: &CalcitScope,
  file_ns: &str,
  call_stack: &CallStackList,
) -> Result<Vec<Calcit>, CalcitErr> {
  let mut ret: Vec<Calcit> = Vec::with_capacity(items.len().saturating_sub(start));
  let mut spreading = false;

  let mut idx = 0;
  items.traverse_result::<CalcitErr>(&mut |item| {
    if idx < start {
      idx += 1;
      return Ok(());
    }
    idx += 1;

    match item {
      Calcit::Syntax(CalcitSyntax::ArgSpread, _) => {
        spreading = true;
      }
      _ => {
        if item.is_expr_evaluated() {
          if spreading {
            match item {
              Calcit::List(xs) => {
                xs.traverse(&mut |x| {
                  ret.push(x.to_owned());
                });
                spreading = false;
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  format!("expected list for spreading, got: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            }
          } else {
            ret.push(item.to_owned());
          }
        } else {
          let v = evaluate_expr(item, scope, file_ns, call_stack)?;

          if spreading {
            match v {
              Calcit::List(xs) => {
                xs.traverse(&mut |x| {
                  ret.push(x.to_owned());
                });
                spreading = false;
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  format!("expected list for spreading, got: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            }
          } else {
            ret.push(v);
          }
        }
      }
    }
    Ok(())
  })?;
  // println!("Evaluated args: {}", ret);
  Ok(ret)
}
