import type { FunctionMap, QueryExpression } from "../types.ts";

export function evalQueryExpression(
  val: QueryExpression,
  obj: any,
  variables: Record<string, any>,
  functionMap: FunctionMap,
): Promise<any> | any {
  const [type, op1] = val;

  switch (type) {
    // Logical operators
    case "and": {
      const op1Val = evalQueryExpression(
        op1,
        obj,
        variables,
        functionMap,
      );
      if (op1Val instanceof Promise) {
        return op1Val.then((v) =>
          v &&
          evalQueryExpression(val[2], obj, variables, functionMap)
        );
      } else if (op1Val) {
        return evalQueryExpression(val[2], obj, variables, functionMap);
      } else {
        return op1Val;
      }
    }
    case "or": {
      const op1Val = evalQueryExpression(
        op1,
        obj,
        variables,
        functionMap,
      );
      if (op1Val instanceof Promise) {
        return op1Val.then((v) =>
          v ||
          evalQueryExpression(val[2], obj, variables, functionMap)
        );
      } else if (op1Val) {
        return op1Val;
      } else {
        return evalQueryExpression(val[2], obj, variables, functionMap);
      }
    }
    // Value types
    case "null":
      return null;
    // TODO: Add this to the actualy query syntax
    case "not": {
      const val = evalQueryExpression(op1, obj, variables, functionMap);
      if (val instanceof Promise) {
        return val.then((v) => !v);
      } else {
        return !val;
      }
    }
    case "number":
    case "string":
    case "boolean":
      return op1;
    case "regexp":
      return [op1, val[2]];
    case "attr": {
      const attributeVal = obj;
      if (val.length === 3) {
        const attributeVal = evalQueryExpression(
          val[1],
          obj,
          variables,
          functionMap,
        );
        if (attributeVal instanceof Promise) {
          return attributeVal.then((v) => v[val[2]]);
        } else if (attributeVal) {
          return attributeVal[val[2]];
        } else {
          return null;
        }
      } else if (!val[1]) {
        return obj;
      } else {
        let attrVal = attributeVal[val[1]];
        const func = functionMap[val[1]];
        if (attrVal === undefined && func !== undefined) {
          // Fallback to function call, if one is defined with this name
          attrVal = func(variables);
        }
        return attrVal;
      }
    }
    case "global": {
      return variables[op1];
    }
    case "array": {
      const argValues = op1.map((v) =>
        evalQueryExpression(v, obj, variables, functionMap)
      );
      // Check if any arg value is a promise, and if so wait for it
      const waitForPromises: Promise<void>[] = [];
      for (let i = 0; i < argValues.length; i++) {
        if (argValues[i] instanceof Promise) {
          // Wait and replace in-place
          waitForPromises.push(argValues[i].then((v: any) => argValues[i] = v));
        }
      }
      if (waitForPromises.length > 0) {
        return Promise.all(waitForPromises).then(() => argValues);
      } else {
        return argValues;
      }
    }
    case "object": {
      const objEntries: [string, any][] = op1.map((
        [key, expr],
      ) => [key, evalQueryExpression(expr, obj, variables, functionMap)]);
      // Check if any arg value is a promise, and if so wait for it
      const waitForPromises: Promise<void>[] = [];
      for (let i = 0; i < objEntries.length; i++) {
        if (objEntries[i][1] instanceof Promise) {
          // Wait and replace in-place
          waitForPromises.push(
            objEntries[i][1].then((v: any) => objEntries[i] = v),
          );
        }
      }
      if (waitForPromises.length > 0) {
        return Promise.all(waitForPromises).then(() =>
          Object.fromEntries(objEntries)
        );
      } else {
        return Object.fromEntries(objEntries);
      }
    }
    case "pageref": {
      const readPageFunction = functionMap.readPage;
      if (!readPageFunction) {
        throw new Error(`No readPage function defined`);
      }
      return readPageFunction(op1);
    }
    case "query": {
      const parsedQuery = val[1];
      const queryFunction = functionMap.$query;
      if (!queryFunction) {
        throw new Error(`No $query function defined`);
      }
      return queryFunction(parsedQuery, variables);
    }
    case "call": {
      const fn = functionMap[op1];
      if (!fn) {
        throw new Error(`Unknown function: ${op1}`);
      }
      const argValues = val[2].map((v) =>
        evalQueryExpression(v, obj, variables, functionMap)
      );
      // Check if any arg value is a promise, and if so wait for it
      const waitForPromises: Promise<void>[] = [];
      for (let i = 0; i < argValues.length; i++) {
        if (argValues[i] instanceof Promise) {
          // Wait and replace in-place
          waitForPromises.push(argValues[i].then((v: any) => argValues[i] = v));
        }
      }
      if (waitForPromises.length > 0) {
        return Promise.all(waitForPromises).then(() => fn(...argValues));
      } else {
        return fn(...argValues);
      }
    }
    case "-": {
      if (val.length == 2) { // only unary minus
        const val = evalQueryExpression(op1, obj, variables, functionMap);
        return -val;
      }
    }
  }

  // Binary operators, here we can pre-calculate the two operand values
  const val1 = evalQueryExpression(
    op1,
    obj,
    variables,
    functionMap,
  );
  const val2 = evalQueryExpression(
    val[2],
    obj,
    variables,
    functionMap,
  );

  const val3 = val[3]
    ? evalQueryExpression(
      val[3],
      obj,
      variables,
      functionMap,
    )
    : undefined;

  if (
    val1 instanceof Promise || val2 instanceof Promise ||
    val3 instanceof Promise
  ) {
    return Promise.all([val1, val2, val3]).then(([v1, v2, v3]) =>
      evalSimpleExpression(type, v1, v2, v3)
    );
  } else {
    return evalSimpleExpression(type, val1, val2, val3);
  }
}
function evalSimpleExpression(type: string, val1: any, val2: any, val3: any) {
  switch (type) {
    case "+":
      return val1 + val2;
    case "-":
      return val1 - val2;
    case "*":
      return val1 * val2;
    case "/":
      return val1 / val2;
    case "%":
      return val1 % val2;
    case "=": {
      if (Array.isArray(val1) && !Array.isArray(val2)) {
        // Record property is an array, and value is a scalar: find the value in the array
        return val1.includes(val2);
      } else if (Array.isArray(val1) && Array.isArray(val2)) {
        // Record property is an array, and value is an array: compare the arrays
        return val1.length === val2.length &&
          val1.every((v) => val2.includes(v));
      }
      return val1 == val2;
    }
    case "!=": {
      if (Array.isArray(val1) && !Array.isArray(val2)) {
        // Record property is an array, and value is a scalar: find the value in the array
        return !val1.includes(val2);
      } else if (Array.isArray(val1) && Array.isArray(val2)) {
        // Record property is an array, and value is an array: compare the arrays
        return !(val1.length === val2.length &&
          val1.every((v) => val2.includes(v)));
      }
      return val1 != val2;
    }
    case "=~": {
      if (typeof val2 === "string") {
        val2 = [val2, "i"];
      }
      if (!Array.isArray(val2)) {
        throw new Error(`Invalid regexp: ${val2}`);
      }
      const r = new RegExp(val2[0], val2[1]);
      return r.test(val1);
    }
    case "!=~": {
      if (typeof val2 === "string") {
        val2 = [val2, "i"];
      }
      if (!Array.isArray(val2)) {
        throw new Error(`Invalid regexp: ${val2}`);
      }
      const r = new RegExp(val2[0], val2[1]);
      return !r.test(val1);
    }
    case "<":
      return val1 < val2;
    case "<=":
      return val1 <= val2;
    case ">":
      return val1 > val2;
    case ">=":
      return val1 >= val2;
    case "in":
      return val2.includes(val1);
    case "?":
      return val1 ? val2 : val3;
    case "??":
      return val1 ?? val2;
    default:
      throw new Error(`Unupported operator: ${type}`);
  }
}
