#include <inttypes.h>
#include <stdlib.h>

#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/executor.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/garray.h"
#include "nvim/gettext_defs.h"
#include "nvim/message.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"

#include "eval/executor.c.generated.h"

/// Handle "blob1 += blob2".
/// Returns OK or FAIL.
static int tv_op_blob(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  if (*op != '+' || tv2->v_type != VAR_BLOB) {
    return FAIL;
  }

  // Blob += Blob
  if (tv2->vval.v_blob == NULL) {
    return OK;
  }

  if (tv1->vval.v_blob == NULL) {
    tv1->vval.v_blob = tv2->vval.v_blob;
    tv1->vval.v_blob->bv_refcount++;
    return OK;
  }

  blob_T *const b1 = tv1->vval.v_blob;
  blob_T *const b2 = tv2->vval.v_blob;
  const int len = tv_blob_len(b2);

  if (len > 0) {
    ga_grow(&b1->bv_ga, len);
    memmove((uint8_t *)b1->bv_ga.ga_data + b1->bv_ga.ga_len,
            (uint8_t *)b2->bv_ga.ga_data, (size_t)len);
    b1->bv_ga.ga_len += len;
  }

  return OK;
}

/// Handle "list1 += list2".
/// Returns OK or FAIL.
static int tv_op_list(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  if (*op != '+' || tv2->v_type != VAR_LIST) {
    return FAIL;
  }

  // List += List
  if (tv2->vval.v_list == NULL) {
    return OK;
  }

  if (tv1->vval.v_list == NULL) {
    tv1->vval.v_list = tv2->vval.v_list;
    tv_list_ref(tv1->vval.v_list);
  } else {
    tv_list_extend(tv1->vval.v_list, tv2->vval.v_list, NULL);
  }

  return OK;
}

/// Handle number operations:
///      nr += nr , nr -= nr , nr *=nr , nr /= nr , nr %= nr
///
/// Returns OK or FAIL.
static int tv_op_number(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  varnumber_T n = tv_get_number(tv1);
  if (tv2->v_type == VAR_FLOAT) {
    float_T f = (float_T)n;
    if (*op == '%') {
      return FAIL;
    }
    switch (*op) {
    case '+':
      f += tv2->vval.v_float; break;
    case '-':
      f -= tv2->vval.v_float; break;
    case '*':
      f *= tv2->vval.v_float; break;
    case '/':
      f /= tv2->vval.v_float; break;
    }
    tv_clear(tv1);
    tv1->v_type = VAR_FLOAT;
    tv1->vval.v_float = f;
  } else {
    switch (*op) {
    case '+':
      n += tv_get_number(tv2); break;
    case '-':
      n -= tv_get_number(tv2); break;
    case '*':
      n *= tv_get_number(tv2); break;
    case '/':
      n = num_divide(n, tv_get_number(tv2)); break;
    case '%':
      n = num_modulus(n, tv_get_number(tv2)); break;
    }
    tv_clear(tv1);
    tv1->v_type = VAR_NUMBER;
    tv1->vval.v_number = n;
  }

  return OK;
}

/// Handle "str1 .= str2"
/// Returns OK or FAIL.
static int tv_op_string(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  if (tv2->v_type == VAR_FLOAT) {
    return FAIL;
  }

  char numbuf[NUMBUFLEN];
  // str .= str
  const char *s2 = tv_get_string_buf(tv2, numbuf);
  if (grow_string_tv(tv1, s2) == OK) {
    return OK;
  }

  const char *tvs = tv_get_string(tv1);
  char *const s = concat_str(tvs, s2);
  tv_clear(tv1);
  tv1->v_type = VAR_STRING;
  tv1->vval.v_string = s;

  return OK;
}

/// Handle "tv1 += tv2", "tv1 -= tv2", "tv1 *= tv2", "tv1 /= tv2", "tv1 %= tv2"
/// and "tv1 .= tv2"
/// Returns OK or FAIL.
static int tv_op_nr_or_string(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  if (tv2->v_type == VAR_LIST) {
    return FAIL;
  }

  if (vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) {
    return tv_op_number(tv1, tv2, op);
  }

  return tv_op_string(tv1, tv2, op);
}

/// Handle "f1 += f2", "f1 -= f2", "f1 *= f2", "f1 /= f2".
/// Returns OK or FAIL.
static int tv_op_float(typval_T *tv1, const typval_T *tv2, const char *op)
  FUNC_ATTR_NONNULL_ALL
{
  if (*op == '%' || *op == '.'
      || (tv2->v_type != VAR_FLOAT
          && tv2->v_type != VAR_NUMBER
          && tv2->v_type != VAR_STRING)) {
    return FAIL;
  }

  const float_T f = (tv2->v_type == VAR_FLOAT
                     ? tv2->vval.v_float
                     : (float_T)tv_get_number(tv2));
  switch (*op) {
  case '+':
    tv1->vval.v_float += f; break;
  case '-':
    tv1->vval.v_float -= f; break;
  case '*':
    tv1->vval.v_float *= f; break;
  case '/':
    tv1->vval.v_float /= f; break;
  }

  return OK;
}

/// Handle tv1 += tv2, -=, *=, /=,  %=, .=
///
/// @param[in,out]  tv1  First operand, modified typval.
/// @param[in]  tv2  Second operand.
/// @param[in]  op  Used operator.
///
/// @return OK or FAIL.
int eexe_mod_op(typval_T *const tv1, const typval_T *const tv2, const char *const op)
  FUNC_ATTR_NONNULL_ALL
{
  // Can't do anything with a Funcref or Dict on the right.
  // v:true and friends only work with "..=".
  if (tv2->v_type == VAR_FUNC || tv2->v_type == VAR_DICT
      || ((tv2->v_type == VAR_BOOL || tv2->v_type == VAR_SPECIAL) && *op == '.')) {
    semsg(_(e_letwrong), op);
    return FAIL;
  }

  int retval = FAIL;

  switch (tv1->v_type) {
  case VAR_DICT:
  case VAR_FUNC:
  case VAR_PARTIAL:
  case VAR_BOOL:
  case VAR_SPECIAL:
    break;
  case VAR_BLOB:
    retval = tv_op_blob(tv1, tv2, op);
    break;
  case VAR_LIST:
    retval = tv_op_list(tv1, tv2, op);
    break;
  case VAR_NUMBER:
  case VAR_STRING:
    retval = tv_op_nr_or_string(tv1, tv2, op);
    break;
  case VAR_FLOAT:
    retval = tv_op_float(tv1, tv2, op);
    break;
  case VAR_UNKNOWN:
    abort();
  }

  if (retval != OK) {
    semsg(_(e_letwrong), op);
  }

  return retval;
}
