package com.onthegomap.planetiler.custommap.expression.stdlib;

import static org.projectnessie.cel.checker.Decls.newOverload;

import com.google.api.expr.v1alpha1.Type;
import java.util.List;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
import java.util.function.LongBinaryOperator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.projectnessie.cel.checker.Decls;
import org.projectnessie.cel.common.types.BoolT;
import org.projectnessie.cel.common.types.DoubleT;
import org.projectnessie.cel.common.types.Err;
import org.projectnessie.cel.common.types.IntT;
import org.projectnessie.cel.common.types.NullT;
import org.projectnessie.cel.common.types.StringT;
import org.projectnessie.cel.common.types.ref.Val;
import org.projectnessie.cel.common.types.traits.Lister;
import org.projectnessie.cel.common.types.traits.Mapper;
import org.projectnessie.cel.interpreter.functions.Overload;

/**
 * Built-in functions to expose to all dynamic expression used in planetiler configs.
 */
public class PlanetilerStdLib extends PlanetilerLib {
  private static final int VARARG_LIMIT = 32;
  private static final Type T = Decls.newTypeParamType("T");
  private static final Type K = Decls.newTypeParamType("K");
  private static final Type V = Decls.newTypeParamType("V");

  public PlanetilerStdLib() {
    super(List.of(
      // coalesce(a, b, c...) -> first non-null value
      new BuiltInFunction(
        Decls.newFunction("coalesce",
          IntStream.range(0, VARARG_LIMIT)
            .mapToObj(
              i -> newOverload("coalesce_" + i, IntStream.range(0, i).mapToObj(d -> Decls.Any).toList(), Decls.Any))
            .toList()
        ),
        Overload.overload("coalesce",
          null,
          null,
          (a, b) -> a == null || a instanceof NullT ? b : a,
          args -> {
            for (var arg : args) {
              if (!(arg instanceof NullT)) {
                return arg;
              }
            }
            return NullT.NullValue;
          })
      ),

      // nullif(a, b) -> null if a == b, otherwise a
      new BuiltInFunction(
        Decls.newFunction("nullif",
          Decls.newOverload("nullif", List.of(T, T), T)
        ),
        Overload.binary("nullif", (a, b) -> Objects.equals(a, b) ? NullT.NullValue : a)
      ),

      // string.replaceRegex(regex, replacement) -> replaces all matches for regex in string with replacement
      new BuiltInFunction(
        Decls.newFunction("replaceRegex",
          Decls.newInstanceOverload("replaceRegex", List.of(Decls.String, Decls.String, Decls.String), Decls.String)
        ),
        Overload.function("replaceRegex", values -> {
          try {
            String string = ((String) values[0].value());
            String regexp = ((String) values[1].value());
            String replace = ((String) values[2].value());
            return StringT.stringOf(string.replaceAll(regexp, replace));
          } catch (RuntimeException e) {
            return Err.newErr(e, "%s", e.getMessage());
          }
        })
      ),

      // map.has(key) -> true if key is present in map
      // map.has(key, value...) true if the value for key is in the list of values provided
      new BuiltInFunction(
        Decls.newFunction("has",
          IntStream.range(0, VARARG_LIMIT)
            .mapToObj(
              i -> Decls.newInstanceOverload("map_has_" + i, Stream.concat(
                Stream.of(Decls.newMapType(K, V), K),
                IntStream.range(0, i).mapToObj(n -> V)
              ).toList(), Decls.Bool)
            ).toList()
        ),
        Overload.overload("has",
          null,
          null,
          (map, key) -> {
            try {
              return getFromMap(map, key) != null ? BoolT.True : BoolT.False;
            } catch (RuntimeException e) {
              return Err.newErr(e, "%s", e.getMessage());
            }
          },
          args -> {
            try {
              Val elem = getFromMap(args[0], args[1]);
              if (elem == null) {
                return BoolT.False;
              }
              for (int i = 2; i < args.length; i++) {
                if (args[i].equals(elem)) {
                  return BoolT.True;
                }
              }
              return BoolT.False;
            } catch (RuntimeException e) {
              return Err.newErr(e, "%s", e.getMessage());
            }
          })
      ),

      // map.get(key) -> the value for key, or null if missing
      new BuiltInFunction(
        Decls.newFunction("get", Decls.newInstanceOverload("get", List.of(Decls.newMapType(K, V), K), V)),
        Overload.binary("get", (map, key) -> {
          try {
            var value = getFromMap(map, key);
            return value == null ? NullT.NullValue : value;
          } catch (RuntimeException e) {
            return Err.newErr(e, "%s", e.getMessage());
          }
        })
      ),

      // map.getOrDefault(key, default) -> the value for key, or default if missing
      new BuiltInFunction(
        Decls.newFunction("getOrDefault",
          Decls.newInstanceOverload("getOrDefault", List.of(Decls.newMapType(K, V), K, V), V)),
        Overload.function("getOrDefault", args -> {
          try {
            var value = getFromMap(args[0], args[1]);
            return value == null ? args[2] : value;
          } catch (RuntimeException e) {
            return Err.newErr(e, "%s", e.getMessage());
          }
        })
      ),

      // min(list) -> the minimum value from the list, or null if empty
      new BuiltInFunction(
        Decls.newFunction("min",
          Decls.newOverload("min_int", List.of(Decls.newListType(Decls.Int)), Decls.Int),
          Decls.newOverload("min_double", List.of(Decls.newListType(Decls.Double)), Decls.Double)
        ),
        Overload.unary("min", list -> reduceNumeric(list, Math::min, Math::min))
      ),

      // max(list) -> the maximum value from the list, or null if empty
      new BuiltInFunction(
        Decls.newFunction("max",
          Decls.newOverload("max_int", List.of(Decls.newListType(Decls.Int)), Decls.Int),
          Decls.newOverload("max_double", List.of(Decls.newListType(Decls.Double)), Decls.Double)
        ),
        Overload.unary("max", list -> reduceNumeric(list, Math::max, Math::max))
      )
    ));
  }

  private static Val getFromMap(Val map, Val key) {
    return map instanceof Mapper mapper ? mapper.find(key) : null;
  }

  private static Val reduceNumeric(Val list, LongBinaryOperator intFn, DoubleBinaryOperator doubleFn) {
    try {
      var iterator = ((Lister) list).iterator();
      if (!iterator.hasNext().booleanValue()) {
        return NullT.NullValue;
      }
      var next = iterator.next();
      if (next instanceof IntT intT) {
        long acc = intT.intValue();
        while (iterator.hasNext().booleanValue()) {
          acc = intFn.applyAsLong(iterator.next().convertToNative(Long.class), acc);
        }
        return IntT.intOf(acc);
      } else if (next instanceof DoubleT doubleT) {
        double acc = doubleT.convertToNative(Double.class);
        while (iterator.hasNext().booleanValue()) {
          acc = doubleFn.applyAsDouble(iterator.next().convertToNative(Double.class), acc);
        }
        return DoubleT.doubleOf(acc);
      } else {
        return Err.newErr("Bad element of list for min(): %s", next);
      }
    } catch (RuntimeException e) {
      return Err.newErr(e, "%s", e.getMessage());
    }
  }
}
