using System;
using System.Collections.Generic;
using KitsuneCafe.System;
using UnityEngine;
using Wacs.Core.Runtime;
using Wacs.Core.Runtime.Types;

namespace KitsuneCafe.Scripting
{
    public class RuntimeInstance
    {
        public const string DefaultStartFunction = "main";

        public readonly WasmRuntime Runtime;
        public readonly Runtime Asset;
        public readonly IDictionary<BindingPath, IFunctionState> State;

        public RuntimeInstance(WasmRuntime runtime, Runtime asset, IDictionary<BindingPath, IFunctionState> state)
        {
            Runtime = runtime;
            Asset = asset;
            State = state;
        }

        public IResult<Unit, Exception> Invoke(FuncAddr addr, InvokerOptions options = null)
        {
            var caller = Runtime.CreateInvokerAction(addr, options);
            try
            {
                caller();
                return Result.Ok<Unit, Exception>(default);
            }
            catch (Exception e)
            {
                return Result.Err<Unit, Exception>(e);
            }
        }

        public IResult<T, Exception> Invoke<T>(FuncAddr addr, InvokerOptions options = null)
        {
            var caller = Runtime.CreateInvokerFunc<T>(addr, options);
            try
            {
                T value = caller();
                return Result.Ok<T, Exception>(default);
            }
            catch (Exception e)
            {
                return Result.Err<T, Exception>(e);
            }
        }

        public IResult<Unit, Exception> Invoke(BindingPath path, InvokerOptions options = null)
        {
            if (Runtime.TryGetExportedFunction(path, out var addr))
            {
                return Invoke(addr, options);
            }

            return Result.Err<Unit, Exception>(new ArgumentException($"No such function \"{path}\""));
        }

        public IResult<T, Exception> Invoke<T>(BindingPath path, InvokerOptions options = null)
        {
            if (Runtime.TryGetExportedFunction(path, out var addr))
            {
                return Invoke<T>(addr, options);
            }

            return Result.Err<T, Exception>(new ArgumentException($"No such function \"{path}\""));
        }

        public IResult<Unit, Exception> Start(ModuleInstance module, InvokerOptions options = null)
        {
            return Invoke(module.StartFunc, options);
        }

        public IResult<Unit, Exception> Start(string moduleName, InvokerOptions options = null)
        {
            return Start(Runtime.GetModule(moduleName), options);
        }

        public IResult<Unit, Exception> Start(Module module, InvokerOptions options = null)
        {
            return Start(module.Name, options);
        }

        public IResult<Unit, Exception> Start(InvokerOptions options = null)
        {
            if (Asset.Modules.Count != 1)
            {
                return Result.Err<Unit, Exception>(new ArgumentException("Runtime has more than one module registered. Specify the module to start."));
            }

            return Start(Asset.Modules[0], options);
        }
    }

    [CreateAssetMenu(menuName = KitsuneCafeMenu.Module + "Runtime")]
    public class Runtime : ScriptableObject
    {
        [SerializeField]
        private RuntimeAttributes attributes = new();
        public RuntimeAttributes Attributes => attributes;

        [SerializeField]
        private List<Environment> environments = new();
        public List<Environment> Environments => environments;

        [SerializeField]
        private List<Module> modules = new();
        public List<Module> Modules => modules;

        public RuntimeInstance Instantiate()
        {
            var runtime = new WasmRuntime(attributes);
            var state = BindEnvironments(runtime);
            RegisterModules(runtime);

            return new RuntimeInstance(runtime, this, state);
        }

        private RuntimeState BindEnvironments(WasmRuntime runtime)
        {
            var state = new RuntimeState();

            foreach (var env in environments)
            {
                env.Bind(runtime, state);
            }

            return state;
        }

        private void RegisterModules(WasmRuntime runtime)
        {
            foreach (var module in modules)
            {
                var instance = module.Instantiate(runtime);
                runtime.RegisterModule(module.Name, instance);
            }
        }
    }
}
