package cljtest;

import clojure.java.api.Clojure;
import clojure.lang.ArraySeq;
import clojure.lang.IFn;
import common.Engine;
import common.EngineException;

import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Collectors;

/**
 * Utility class for Clojure tests.
 *
 * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
 */
public final class ClojureScript {
    public static final IFn LOAD_STRING = var("clojure.core/load-string");
    public static final IFn LOAD_FILE = asUser("load-file");
    public static final IFn LOAD_STRING_IN = asUser("load-string");
    public static Path CLOJURE_ROOT = Path.of(".");

    private ClojureScript() {
    }

    private static IFn asUser(final String function) {
        return (IFn) LOAD_STRING.invoke(
                "(fn " + function +  "-in [arg]" +
                        "  (binding [*ns* *ns*]" +
                        "    (in-ns 'user)" +
                        "    (" + function + " arg)))"
        );
    }

    public static void loadScript(final String script) {
        final String escaped = CLOJURE_ROOT.toString().replace("\\", "\\\\");
        LOAD_STRING_IN.invoke("(defn load-file [file] (clojure.core/load-file (str \"" + escaped + "/\" file)))");
        LOAD_FILE.invoke(CLOJURE_ROOT.resolve(script).toString());
    }

    static <T> Engine.Result<T> call(final IFn f, final Class<T> type, final String context, final Object[] args) {
        final Object result;
        try {
            result = callUnsafe(f, args);
        } catch (final AssertionError e) {
            throw e;
        } catch (final Throwable e) {
            throw new EngineException("No error expected in " + context, e);
        }
        if (result == null) {
            throw new EngineException("Expected %s, found null\n%s".formatted(type.getSimpleName(), context), new NullPointerException());
        }
        if (!type.isInstance(result)) {
            throw new EngineException("Expected %s, found %s (%s)\n%s".formatted(type.getSimpleName(), result, result.getClass().getSimpleName(), context), null);
        }
        return new Engine.Result<>(context, type.cast(result));
    }

    private static Object callUnsafe(final IFn f, final Object[] args) {
        return switch (args.length) {
            case 0 -> f.invoke();
            case 1 -> f.invoke(args[0]);
            case 2 -> f.invoke(args[0], args[1]);
            case 3 -> f.invoke(args[0], args[1], args[2]);
            case 4 -> f.invoke(args[0], args[1], args[2], args[3]);
            case 5 -> f.invoke(args[0], args[1], args[2], args[3], args[4]);
            case 6 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5]);
            case 7 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
            case 8 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
            case 9 -> f.invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
            default -> f.applyTo(ArraySeq.create(args));
        };
    }

    public static Engine.Result<Throwable> expectException(final IFn f, final Object[] args, final String context) {
        try {
            callUnsafe(f, args);
        } catch (final Throwable e) {
            return new Engine.Result<>(context, e);
        }
        assert false : "Exception expected in " + context;
        return null;
    }

    public static <T> F<T> function(final String name, final Class<T> type) {
        return new F<>(name, type);
    }

    /**
     * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
     */
    public record F<T>(String name, Class<T> type, IFn f) {
        public F(final String name, final Class<T> type) {
            this(name.substring(name.indexOf("/") + 1), type, var(name));
        }

        public Engine.Result<T> call(final Engine.Result<?>... args) {
            return ClojureScript.call(
                    f,
                    type,
                    callToString(args),
                    Arrays.stream(args).map(Engine.Result::value).toArray()
            );
        }

        public String callToString(final Engine.Result<?>[] args) {
            return "(" + name + Arrays.stream(args).map(arg -> " " + arg.context()).collect(Collectors.joining()) + ")";
        }

        public Engine.Result<Throwable> expectException(final Engine.Result<?>... args) {
            return ClojureScript.expectException(
                    f,
                    Arrays.stream(args).map(Engine.Result::value).toArray(),
                    "(" + name + " " + Arrays.stream(args).map(Engine.Result::context).collect(Collectors.joining(" ")) + ")"
            );
        }
    }

    public static IFn var(final String name) {
        final String qualifiedName = (name.contains("/") ? "" : "user/") + name;
        return Clojure.var(qualifiedName);
    }
}
