import ceylon.test.event {
    ...
}
import ceylon.test {
    TestRunner,
    TestDescription,
    TestRunResult,
    TestListener
}
import ceylon.test.engine.internal {
    TestEventEmitter
}
import ceylon.collection {
    ArrayList,
    MutableList
}
import java.util.concurrent {
    Executors
}
import java.lang {
    Runnable
}

"Represents a context in which a test is executed, it's used by [[TestExecutor]]."
shared class TestExecutionContext {
    
    "The current test runner."
    shared TestRunner runner;
    
    "The summary result of the test run."
    shared TestRunResult result;
    
    "The current test description."
    shared TestDescription description;
    
    "The parent context."
    shared TestExecutionContext? parent;
    
    MutableList<TestExtension> extensionList;
    TestExtensionResolver extensionResolver;
    TaskExecutor taskExecutor;
    
    "Constructor for root context."
    shared new root(TestRunner runner, TestRunResult result, TestExtensionResolver extensionResolver, Boolean async = false) {
        this.runner = runner;
        this.result = result;
        this.description = runner.description;
        this.parent = null;
        this.extensionList = ArrayList<TestExtension>();
        this.extensionResolver = extensionResolver;
        this.taskExecutor = async then AsyncTaskExecutor() else TaskExecutor();
    }
    
    "Constructor for child context."
    new child(TestExecutionContext parent, TestDescription description) {
        this.runner = parent.runner;
        this.result = parent.result;
        this.parent = parent;
        this.description = description;
        this.extensionResolver = parent.extensionResolver;
        this.extensionList = initExtensions(description, parent.extensions<TestExtension>(), extensionResolver);
        this.taskExecutor = parent.taskExecutor;
    }
    
    "Create child context for given test."
    shared TestExecutionContext childContext(TestDescription description)
            => child(this, description);
    
    "Schedule test tasks for execution."
    shared void execute(<Anything()|{Anything()*}>* tasks)
            => taskExecutor.execute(*tasks);
    
    "Returns implementation of test listener, which is firing registered listeners."
    shared TestListener fire()
            => TestEventEmitter(extensions<TestListener>());
    
    "Register given test extension."
    shared void registerExtension(TestExtension* extensions)
            => extensionList.addAll(extensions);
    
    "Returns all registered instances of test extensions with given type."
    shared TestExtensionType[] extensions<TestExtensionType>() given TestExtensionType satisfies TestExtension {
        return collect<TestExtensionType>().sort(increasing).sequence();
    }
    
    "Returns last registered instance of test extension with given type."
    shared TestExtensionType extension<TestExtensionType>() given TestExtensionType satisfies TestExtension {
        assert(exists e = collect<TestExtensionType>().last);
        return e;
    }
    
    {TestExtensionType*} collect<TestExtensionType>() given TestExtensionType satisfies TestExtension
            => (parent?.collect<TestExtensionType>() else {}).chain(extensionList.narrow<TestExtensionType>());
    
}


MutableList<TestExtension> initExtensions(TestDescription description, {TestExtension*} existingExtensions, TestExtensionResolver extensionResolver) {
    value extensions = ArrayList<TestExtension>();
    extensions.addAll(extensionResolver.resolveExtensions<TestExtension>(description));
    extensions.removeAll(existingExtensions);
    return extensions;
}


class TaskExecutor() {
    
    shared ArrayList<Anything()> taskStack = ArrayList<Anything()>();
    
    shared void execute(<Anything()|{Anything()*}>* tasks) {
        value startExecutionLoop = taskStack.empty;
        for (task in tasks.sequence().reversed) {
            if (is Anything() task) {
                taskStack.add(task);
            }
            if (is {Anything()*} task) {
                taskStack.addAll(task.sequence().reversed);
            }
        }
        if (startExecutionLoop) {
            executionLoop();
        }
    }
    
    shared default void executionLoop() {
        while(exists task = taskStack.pop()) {
            task();
        }
    }
    
}

native
class AsyncTaskExecutor() extends TaskExecutor() {
    
    shared actual void executionLoop() {
        if (exists task = taskStack.pop()) {
            executeTask(() {
                try {
                    task();
                } finally {
                    executionLoop();
                }
            });
        } else {
            shutdown();
        }
    }
    
    native
    void executeTask(Anything() task);
    
    native
    void shutdown();

}


native("jvm")
class AsyncTaskExecutor() extends TaskExecutor() {
    
    value jexecutor = Executors.newSingleThreadExecutor();
    
    native("jvm")
    void executeTask(Anything() task) {
        object jrunnable satisfies Runnable {
            shared actual void run() {
                task();
            }
        }
        jexecutor.submit(jrunnable);
    }
    
    native("jvm")
    void shutdown() {
        jexecutor.shutdown();
    }
    
}


native("js")
class AsyncTaskExecutor() extends TaskExecutor() {
    
    native("js")
    void executeTask(Anything() task) {
        dynamic {
            setTimeout(task, 1);
        }
    }
    
    native("js")
    void shutdown() {
        // noop
    }
    
}