//
// Copyright (c) 2012-2021 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in the LICENSE file.

using System;
using Antmicro.Migrant.Customization;
using System.IO;
using System.Reflection;
using Antmicro.Migrant.VersionTolerance;
using System.Linq;

namespace Antmicro.Migrant.Tests
{
    public class TwoDomainsDriver
    {
        public TwoDomainsDriver(bool useGeneratedSerializer, bool useGeneratedDeserializer)
        {
            settings = new DriverSettings(useGeneratedSerializer, useGeneratedDeserializer, true);

            // this is just a little hack that allows us to debug TwoDomainTests on one domain
            // when `PrepareDomains` method is not called; when `PrepareDomains` is called
            // then these fields are overwritten with proper values
            testsOnDomain1 = new InnerDriver { Settings = settings };
            testsOnDomain2 = testsOnDomain1;
        }

        public void PrepareDomains()
        {
            var pathBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            foreach (var domain in new[] { "domain1", "domain2" }.Select(x => Path.Combine(pathBase, x)))
            {
                Directory.CreateDirectory(domain);
                foreach(var file in new[] { "Tests.dll", "Migrant.dll" })
                {
                    File.Copy(Path.Combine(pathBase, file), Path.Combine(domain, file), true);
                }
            }

            domain1 = AppDomain.CreateDomain("domain1", null, Path.Combine(pathBase, "domain1"), string.Empty, false);
            domain2 = AppDomain.CreateDomain("domain2", null, Path.Combine(pathBase, "domain2"), string.Empty, false);

            testsOnDomain1 = (InnerDriver)domain1.CreateInstanceAndUnwrap(typeof(InnerDriver).Assembly.FullName, typeof(InnerDriver).FullName);
            testsOnDomain2 = (InnerDriver)domain2.CreateInstanceAndUnwrap(typeof(InnerDriver).Assembly.FullName, typeof(InnerDriver).FullName);

            testsOnDomain1.Settings = settings;
            testsOnDomain2.Settings = settings;
        }

        public void DisposeDomains()
        {
            testsOnDomain1 = null;
            testsOnDomain2 = null;
            AppDomain.Unload(domain1);
            AppDomain.Unload(domain2);
            domain1 = null;
            domain2 = null;

            var pathBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            Directory.Delete(Path.Combine(pathBase, "domain1"), true);
            Directory.Delete(Path.Combine(pathBase, "domain2"), true);
        }
        
        public bool SerializeAndDeserializeOnTwoAppDomains(DynamicType domainOneType, DynamicType domainTwoType, VersionToleranceLevel vtl, bool allowGuidChange = true)
        {
            testsOnDomain1.CreateInstanceOnAppDomain(domainOneType);
            testsOnDomain2.CreateInstanceOnAppDomain(domainTwoType);

            var bytes = testsOnDomain1.SerializeOnAppDomain();
            try 
            {
                testsOnDomain2.DeserializeOnAppDomain(bytes, settings.GetSettings(allowGuidChange ? vtl | VersionToleranceLevel.AllowGuidChange : vtl));
                return true;
            } 
            catch (VersionToleranceException)
            {
                return false;
            }
        }

        protected DriverSettings settings;

        protected InnerDriver testsOnDomain1;
        protected InnerDriver testsOnDomain2;
        
        private AppDomain domain1;
        private AppDomain domain2;
    }

    public class DriverSettings : MarshalByRefObject
    {
        public DriverSettings(bool useGeneratedSerializer, bool useGeneratedDeserializer, bool useStamping)
        {
            this.useGeneratedDeserializer = useGeneratedDeserializer;
            this.useGeneratedSerializer = useGeneratedSerializer;
            this.useStamping = useStamping;
        }

        public Settings GetSettings(VersionToleranceLevel level = 0)
        {
            return new Settings(useGeneratedSerializer ? Method.Generated : Method.Reflection,
                                useGeneratedDeserializer ? Method.Generated : Method.Reflection,
                                level,
                                disableTypeStamping: !useStamping);
        }

        public Settings GetSettingsAllowingGuidChange(VersionToleranceLevel level = 0)
        {
            return GetSettings(level | VersionToleranceLevel.AllowGuidChange);
        }

        private bool useGeneratedSerializer;
        private bool useGeneratedDeserializer;
        private bool useStamping;
    }
    
    public class InnerDriver : MarshalByRefObject
    {
        public InnerDriver()
        {
            DynamicType.prefix = AppDomain.CurrentDomain.FriendlyName;
        }

        public void CreateInstanceOnAppDomain(DynamicType type, Version version = null)
        {
            obj = type.Instantiate(version);
        }

        public void DeserializeOnAppDomain(byte[] data, Settings settings)
        {
            var stream = new MemoryStream(data);
            var deserializer = new Serializer(settings);
            obj = deserializer.Deserialize<object>(stream);
        }

        public void SetValueOnAppDomain(string className, string fieldName, object value)
        {
            var field = FindClass(className).GetField(fieldName);
            field.SetValue(obj, value);
        }

        public void SetValueOnAppDomain(string fieldName, object value)
        {
            SetValueOnAppDomainInner(obj, fieldName, value);
        }

        private object SetValueOnAppDomainInner(object o, string fieldName, object value)
        {
            var elements = fieldName.Split(new[] { '.' }, 2);
            if (elements.Length == 1)
            {
                var field = o.GetType().GetField(fieldName);
                field.SetValue(o, value);
                return o;
            }
            else
            {
                var field = o.GetType().GetField(elements[0]);
                var local = field.GetValue(o);
                local = SetValueOnAppDomainInner(local, elements[1], value);
                field.SetValue(o, local);
                return o;
            }
        }

        public object GetValueOnAppDomain(string className, string fieldName)
        {
            var field = FindClass(className).GetField(fieldName);
            return field.GetValue(obj);
        }

        public object GetValueOnAppDomain(string fieldName)
        {
            var elements = fieldName.Split('.');
            FieldInfo field = null;
            object currentObject = null;
            foreach (var element in elements)
            {
                currentObject = (currentObject == null) ? obj : field.GetValue(currentObject);
                field = currentObject.GetType().GetField(element);
            }

            return field.GetValue(currentObject);
        }

        public byte[] SerializeOnAppDomain()
        {
            var stream = new MemoryStream();
            var serializer = new Serializer(Settings.GetSettings());
            serializer.Serialize(obj, stream);
            return stream.ToArray();
        }
            
        private Type FindClass(string className)
        {
            var currentClass = obj.GetType();
            while (currentClass != null && currentClass.Name != className)
            {
                currentClass = currentClass.BaseType;
            }
            if (currentClass == null)
            {
                throw new ArgumentException(className);
            }

            return currentClass;
        }

        public string Location { get; set; }
        public DriverSettings Settings { get; set; }

        protected object obj;
    }
}

