import { DateTime } from 'luxon';
import { ensureArray } from '../../../internal/ensure-array.mjs';
import { isNil, isStringFull, isFunction } from '../../../internal/type-checks.mjs';
import { escape } from '../../../utilities/regex.mjs';
import { filterRuleSet, isRule, isRuleSet } from '../helpers.mjs';

class JavaScriptFilterError extends Error {
  constructor(msg) {
    super(`JavaScriptFilter error: ${msg}`);
  }
}
function isDateType(field) {
  return field.type === "datetime" || field.type === "date" || field.type === "time";
}
function isStringType(field) {
  return field.type === "string";
}
function convertDateTime(value, field) {
  switch (field.type) {
    case "datetime":
      return +DateTime.fromISO(value);
    case "date":
      return +DateTime.fromISO(value).startOf("day");
    case "time":
      return +DateTime.fromISO(value).set({ year: 1970, day: 1, month: 1 });
    default:
      return 0;
  }
}
function rulesetToJavascript(rule, config, options) {
  if (isRule(rule)) {
    if (!rule.enabled || !rule.field) {
      return void 0;
    }
    if (isNil(rule.field) || isNil(rule.operator)) {
      console.warn(`JavascriptFilterConverter: Skipping rule with missing field or operator`, rule);
      return void 0;
    }
    const field = config.fields[rule.field];
    if (isNil(rule.value) && field.nullable !== true) {
      console.warn(`JavascriptFilterConverter: Skipping nil field '${rule.field}' in rule (non-nullable)`, rule);
      return void 0;
    }
    let fieldName = rule.field;
    if (options?.renameFields && fieldName in options.renameFields) {
      fieldName = options.renameFields[fieldName];
    }
    return predicateFromRule(rule, config, options);
  } else if (isRuleSet(rule)) {
    const condition = rule.condition === "and" ? " && " : " || ";
    const rules = rule.rules.map((r) => rulesetToJavascript(r, config, options)).filter((r) => isStringFull(r));
    return rules.length ? rules.map((r) => `(${r})`).join(condition) : "true";
  } else ;
}
function predicateFromRule(rule, config, options) {
  const fieldConfig = config.fields[rule.field];
  let predicate;
  let fieldName = rule.field;
  if (options.renameFields && fieldName in options.renameFields) {
    fieldName = options.renameFields[fieldName];
  }
  let field = `get("${fieldName}", data)`;
  let operator = "";
  let value = rule.value;
  if (isFunction(fieldConfig.convertValue)) {
    value = fieldConfig.convertValue(value);
  }
  if (isDateType(fieldConfig)) {
    value = convertDateTime(value, fieldConfig);
  }
  const caseInsensitive = isStringType(fieldConfig) && !!fieldConfig.caseInsensitive;
  switch (rule.operator) {
    case "=":
      value = caseInsensitive ? String(value).toLowerCase() : value;
      operator = "==";
      break;
    case "!=":
      value = caseInsensitive ? String(value).toLowerCase() : value;
      operator = "!=";
      break;
    case "<":
      operator = "<";
      break;
    case "<=":
      operator = "<=";
      break;
    case ">":
      operator = ">";
      break;
    case ">=":
      operator = ">=";
      break;
    case "in":
      value = ensureArray(value);
      predicate = `[${value.map(JSON.stringify).join(", ")}].includes(${field})`;
      break;
    case "!in":
      value = ensureArray(value);
      predicate = `![${value.map(JSON.stringify).join(", ")}].includes(${field})`;
      break;
    case "contains":
      field = caseInsensitive ? `${field}.toLowerCase()` : field;
      value = caseInsensitive ? String(value).toLowerCase() : value;
      predicate = `${field}.includes(${JSON.stringify(value)})`;
      break;
    case "!contains":
      field = caseInsensitive ? `${field}.toLowerCase()` : field;
      value = caseInsensitive ? String(value).toLowerCase() : value;
      predicate = `!(${field}.includes(${JSON.stringify(value)}))`;
      break;
    case "like":
      value = escape(String(value));
      value = value.replace(/\\\?/g, ".");
      value = value.replace(/\\\*/g, ".*");
      value = `^${value}$`;
      predicate = `regex(${JSON.stringify(value)}, ${field}, ${caseInsensitive})`;
      break;
    case "!like":
      value = escape(String(value));
      value = value.replace(/\\\?/g, ".");
      value = value.replace(/\\\*/g, ".*");
      value = `^${value}$`;
      predicate = `!(regex(${JSON.stringify(value)}, ${field}, ${caseInsensitive}))`;
      break;
    case "regex":
      predicate = `regex(${JSON.stringify(value)}, ${field}, ${caseInsensitive})`;
      break;
    case "!regex":
      predicate = `!(regex(${JSON.stringify(value)}, ${field}, ${caseInsensitive}))`;
      break;
    default:
      throw new Error(`Unrecognized query operator '${rule.operator}'`);
  }
  predicate ?? (predicate = `${field} ${operator} ${JSON.stringify(value)}`);
  return predicate;
}
const converter = (ruleset, config, options) => {
  const warnings = {};
  const regExps = {};
  options = {
    throwOnError: false,
    ...options
  };
  if (options.excludeFields || options.includeFields || options.renameFields) {
    ruleset = filterRuleSet(ruleset, options);
  }
  const hasOwnProperty = (obj, prop) => {
    if (typeof obj === "object" || typeof obj === "function") {
      return Object.prototype.hasOwnProperty.call(obj, prop);
    }
    return false;
  };
  const getProp = (prop, obj) => {
    if (!config.fields[prop])
      throw new JavaScriptFilterError(`Unknown query builder field "${prop}"`);
    if (!hasOwnProperty(obj ?? {}, prop))
      throw new JavaScriptFilterError(`Missing property "${prop}" in supplied data object`);
    const fieldConfig = config.fields[prop];
    let value = obj[prop];
    if (isDateType(fieldConfig)) {
      value = convertDateTime(value, fieldConfig);
    }
    if (isStringType(fieldConfig) && fieldConfig.caseInsensitive) {
      value = String(value).toLowerCase();
    }
    return value;
  };
  const regex = (regex2, val, caseInsensitive = false) => {
    if (!regExps[regex2]) {
      regExps[regex2] = new RegExp(regex2, caseInsensitive ? "i" : "");
    }
    return regExps[regex2].test(val);
  };
  const js = ["return ", rulesetToJavascript(ruleset, config, options), ";"];
  const func = new Function("get", "regex", "data", js.join(""));
  const wrappedFunction = (data) => {
    try {
      return func(getProp, regex, data);
    } catch (e) {
      if (!warnings[e.message]) {
        warnings[e.message] = true;
        console.warn(e.message);
      }
      if (options?.throwOnError) {
        throw e;
      }
      return false;
    }
  };
  wrappedFunction.expression = js.join("");
  return wrappedFunction;
};
const converter$1 = converter;

export { JavaScriptFilterError, converter$1 as default };
