using System;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Configuration;
using HarmonyLib;

namespace BanquetForCyka {
    public interface IFieldModifier {
        void CaptureOriginal();
        void Apply();
        void Restore();
        void LogValue(string prefix);
    }

    /// <summary>
    /// Represents a field that can be multiplied by a configurable value
    /// </summary>
    /// <typeparam name="TField">The type of the field value</typeparam>
    /// <typeparam name="TMul">The type of the multiplier</typeparam>
    public class FieldConfiguration<TField, TMul> {
        private string _fieldName;
        private ConfigEntry<TMul> _defaultMultiplier;
        private Func<object, ConfigEntry<TMul>> _conditionalMultiplier;
        private Func<object, bool> _applyCondition;

        public string FieldName {
            get { return _fieldName; }
            set { _fieldName = value; }
        }

        public ConfigEntry<TMul> DefaultMultiplier {
            get { return _defaultMultiplier; }
            set { _defaultMultiplier = value; }
        }

        public Func<object, ConfigEntry<TMul>> ConditionalMultiplier {
            get { return _conditionalMultiplier; }
            set { _conditionalMultiplier = value; }
        }

        public Func<object, bool> ApplyCondition {
            get { return _applyCondition; }
            set { _applyCondition = value; }
        }

        public FieldConfiguration(string fieldName, ConfigEntry<TMul> defaultMultiplier) {
            _fieldName = fieldName;
            _defaultMultiplier = defaultMultiplier;
        }

        public FieldConfiguration(string fieldName, ConfigEntry<TMul> defaultMultiplier,
                                  Func<object, ConfigEntry<TMul>> conditionalMultiplier) {
            _fieldName = fieldName;
            _defaultMultiplier = defaultMultiplier;
            _conditionalMultiplier = conditionalMultiplier;
        }

        public FieldConfiguration(string fieldName, ConfigEntry<TMul> defaultMultiplier,
                                  Func<object, bool> applyCondition) {
            _fieldName = fieldName;
            _defaultMultiplier = defaultMultiplier;
            _applyCondition = applyCondition;
        }

        public FieldConfiguration(string fieldName, ConfigEntry<TMul> defaultMultiplier,
                                  Func<object, ConfigEntry<TMul>> conditionalMultiplier,
                                  Func<object, bool> applyCondition) {
            _fieldName = fieldName;
            _defaultMultiplier = defaultMultiplier;
            _conditionalMultiplier = conditionalMultiplier;
            _applyCondition = applyCondition;
        }

        public ConfigEntry<TMul> GetMultiplier(object __instance) {
            if (_conditionalMultiplier == null) {
                return _defaultMultiplier;
            }
            return _conditionalMultiplier(__instance);
        }

        public bool ShouldApply(object __instance) {
            if (_applyCondition == null) {
                return true;
            }
            return _applyCondition(__instance);
        }
    }

    public class MultipliedField<TField, TMul> : IFieldModifier {
        private readonly string _fieldName;
        private readonly ConfigEntry<TMul> _multiplier;
        private readonly Traverse _parentTraverse;
        private readonly Func<object, bool> _applyCondition;
        private TField _originalValue;

        public string FieldName {
            get { return _fieldName; }
        }

        public MultipliedField(string fieldName, ConfigEntry<TMul> multiplier, Traverse parentTraverse,
                               Func<object, bool> applyCondition = null) {
            _fieldName = fieldName;
            _multiplier = multiplier;
            _parentTraverse = parentTraverse;
            _applyCondition = applyCondition;

            if (!parentTraverse.Field(fieldName).FieldExists()) {
                throw new ArgumentException(
                    string.Format("Field {0} does not exist on {1}", fieldName, parentTraverse));
            }

            // Verify TField is a numeric type
            if (!IsNumericType(typeof(TField))) {
                throw new ArgumentException(
                    string.Format("Field type {0} must be a numeric type", typeof(TField).Name));
            }

            // Verify TMul is a numeric type
            if (!IsNumericType(typeof(TMul))) {
                throw new ArgumentException(
                    string.Format("Multiplier type {0} must be a numeric type", typeof(TMul).Name));
            }
        }

        private static bool IsNumericType(Type type) {
            return type == typeof(byte) || type == typeof(sbyte) || type == typeof(short) || type == typeof(ushort) ||
                   type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong) ||
                   type == typeof(float) || type == typeof(double) || type == typeof(decimal);
        }

        private TField MultiplyValues(TField fieldValue, TMul multiplierValue) {
            // Convert both to double for the multiplication
            double fieldDouble = Convert.ToDouble(fieldValue);
            double multiplierDouble = Convert.ToDouble(multiplierValue);
            double result = fieldDouble * multiplierDouble;
            // Convert back to TField
            return (TField)Convert.ChangeType(result, typeof(TField));
        }

        public TField GetValue() {
            var value = _parentTraverse.Field(_fieldName).GetValue();
            if (value == null)
                throw new InvalidOperationException(string.Format("Field {0} returned null", _fieldName));
            return (TField)value;
        }

        public void SetValue(TField value) {
            _parentTraverse.Field(_fieldName).SetValue(value);
            var verifyValue = GetValue();
            if (!verifyValue.Equals(value))
                throw new InvalidOperationException(
                    string.Format("Field {0} set to {1} but read back as {2}", _fieldName, value, verifyValue));
        }

        public void CaptureOriginal() {
            _originalValue = GetValue();
            if (Main.debug.Value)
                Console.WriteLine("Captured original value for {0}: {1}", _fieldName, _originalValue);
        }

        public void Apply() {
            try {
                if (_applyCondition != null && !_applyCondition(_parentTraverse.GetValue())) {
                    if (Main.debug.Value)
                        Console.WriteLine("Skipping {0}: condition not met", _fieldName);
                    return;
                }

                var newValue = MultiplyValues(_originalValue, _multiplier.Value);

                if (Main.debug.Value)
                    Console.WriteLine("Applying to {0}: {1} * {2} = {3}", _fieldName, _originalValue, _multiplier.Value,
                                      newValue);

                SetValue(newValue);
            } catch (Exception e) {
                throw new InvalidOperationException(string.Format("Failed to apply multiplication to {0}", _fieldName),
                                                    e);
            }
        }

        public void Restore() {
            if (Main.debug.Value)
                Console.WriteLine("Restoring {0} to original value: {1}", _fieldName, _originalValue);
            SetValue(_originalValue);
        }

        public void LogValue(string prefix) {
            if (!Main.debug.Value)
                return;
            var currentValue = GetValue();
            Console.WriteLine("{0} {1}; {2}: {3} (original: {4}, multiplier: {5})", prefix, _parentTraverse, _fieldName,
                              currentValue, _originalValue, _multiplier.Value);
        }
    }

    public class BooleanFieldConfiguration {
        private string _fieldName;
        private ConfigEntry<bool> _value;
        private Func<object, ConfigEntry<bool>> _conditionalValue;
        private Func<object, bool> _applyCondition;

        public string FieldName {
            get { return _fieldName; }
            set { _fieldName = value; }
        }

        public ConfigEntry<bool> Value {
            get { return _value; }
            set { _value = value; }
        }

        public Func<object, ConfigEntry<bool>> ConditionalValue {
            get { return _conditionalValue; }
            set { _conditionalValue = value; }
        }

        public Func<object, bool> ApplyCondition {
            get { return _applyCondition; }
            set { _applyCondition = value; }
        }

        public BooleanFieldConfiguration(string fieldName, ConfigEntry<bool> value) {
            _fieldName = fieldName;
            _value = value;
        }

        public BooleanFieldConfiguration(string fieldName, ConfigEntry<bool> value,
                                         Func<object, ConfigEntry<bool>> conditionalValue) {
            _fieldName = fieldName;
            _value = value;
            _conditionalValue = conditionalValue;
        }

        public BooleanFieldConfiguration(string fieldName, ConfigEntry<bool> value, Func<object, bool> applyCondition) {
            _fieldName = fieldName;
            _value = value;
            _applyCondition = applyCondition;
        }

        public BooleanFieldConfiguration(string fieldName, ConfigEntry<bool> value,
                                         Func<object, ConfigEntry<bool>> conditionalValue,
                                         Func<object, bool> applyCondition) {
            _fieldName = fieldName;
            _value = value;
            _conditionalValue = conditionalValue;
            _applyCondition = applyCondition;
        }

        public ConfigEntry<bool> GetValue(object __instance) {
            if (_conditionalValue == null) {
                return _value;
            }
            return _conditionalValue(__instance);
        }

        public bool ShouldApply(object __instance) {
            if (_applyCondition == null) {
                return true;
            }
            return _applyCondition(__instance);
        }
    }

    public class BooleanField : IFieldModifier {
        private readonly string _fieldName;
        private readonly ConfigEntry<bool> _value;
        private readonly Traverse _parentTraverse;
        private readonly Func<object, bool> _applyCondition;
        private bool _originalValue;

        public string FieldName {
            get { return _fieldName; }
        }

        public BooleanField(string fieldName, ConfigEntry<bool> value, Traverse parentTraverse,
                            Func<object, bool> applyCondition = null) {
            _fieldName = fieldName;
            _value = value;
            _parentTraverse = parentTraverse;
            _applyCondition = applyCondition;

            if (!parentTraverse.Field(fieldName).FieldExists()) {
                throw new ArgumentException(
                    string.Format("Field {0} does not exist on {1}", fieldName, parentTraverse));
            }
        }

        public bool GetValue() {
            var value = _parentTraverse.Field(_fieldName).GetValue();
            if (value == null)
                throw new InvalidOperationException(string.Format("Field {0} returned null", _fieldName));
            return (bool)value;
        }

        public void SetValue(bool value) {
            _parentTraverse.Field(_fieldName).SetValue(value);
            var verifyValue = GetValue();
            if (verifyValue != value)
                throw new InvalidOperationException(
                    string.Format("Field {0} set to {1} but read back as {2}", _fieldName, value, verifyValue));
        }

        public void CaptureOriginal() {
            _originalValue = GetValue();
            if (Main.debug.Value)
                Console.WriteLine("Captured original value for {0}: {1}", _fieldName, _originalValue);
        }

        public void Apply() {
            try {
                if (_applyCondition != null && !_applyCondition(_parentTraverse.GetValue())) {
                    if (Main.debug.Value)
                        Console.WriteLine("Skipping {0}: condition not met", _fieldName);
                    return;
                }

                if (_value.Value) {
                    if (Main.debug.Value)
                        Console.WriteLine("Applying to {0}: forcing to true", _fieldName);

                    SetValue(true);
                } else {
                    if (Main.debug.Value)
                        Console.WriteLine("Applying to {0}: leaving as {1} (config is false)", _fieldName, GetValue());
                }
            } catch (Exception e) {
                throw new InvalidOperationException(string.Format("Failed to apply value to {0}", _fieldName), e);
            }
        }

        public void Restore() {
            if (Main.debug.Value)
                Console.WriteLine("Restoring {0} to original value: {1}", _fieldName, _originalValue);
            SetValue(_originalValue);
        }

        public void LogValue(string prefix) {
            if (!Main.debug.Value)
                return;
            var currentValue = GetValue();
            Console.WriteLine("{0} {1}; {2}: {3} (original: {4}, config: {5})", prefix, _parentTraverse, _fieldName,
                              currentValue, _originalValue, _value.Value);
        }
    }

    /// <summary>
    /// Represents an object with multiple fields that can be multiplied
    /// </summary>
    /// <typeparam name="T">The type of the object being managed</typeparam>
    public class MultipliedObject<T> {
        private readonly T _instance;
        private readonly Traverse _objectTraverse;
        private readonly Dictionary<string, IFieldModifier> _fields;

        public MultipliedObject(T __instance) {
            _instance = __instance;
            _objectTraverse = Traverse.Create(__instance);
            _fields = new Dictionary<string, IFieldModifier>();
        }

        public void AddField<TField, TMul>(FieldConfiguration<TField, TMul> config) {
            var multiplier = config.GetMultiplier(_instance);
            _fields[config.FieldName] =
                new MultipliedField<TField, TMul>(config.FieldName, multiplier, _objectTraverse, config.ShouldApply);
        }

        public void AddBooleanField(BooleanFieldConfiguration config) {
            var value = config.GetValue(_instance);
            _fields[config.FieldName] = new BooleanField(config.FieldName, value, _objectTraverse, config.ShouldApply);
        }

        public void CaptureFrom() {
            foreach (var field in _fields.Values) {
                field.CaptureOriginal();
            }
        }

        public void ApplyTo(IEnumerable<string> fieldNames = null) {
            IEnumerable<string> fieldsToApply = fieldNames ?? _fields.Keys;
            foreach (var fieldName in fieldsToApply.Where(name => _fields.ContainsKey(name)))
                _fields[fieldName].Apply();
        }

        public void RestoreTo(IEnumerable<string> fieldNames = null) {
            IEnumerable<string> fieldsToRestore = fieldNames ?? _fields.Keys;
            foreach (var fieldName in fieldsToRestore.Where(name => _fields.ContainsKey(name)))
                _fields[fieldName].Restore();
        }

        public void LogValues(string prefix) {
            if (!Main.debug.Value)
                return;
            foreach (var field in _fields.Values) {
                field.LogValue(prefix);
            }
        }
    }

    /// <summary>
    /// Manages a collection of objects whose fields can be multiplied
    /// </summary>
    /// <typeparam name="T">The type of objects being managed</typeparam>
    public class MultipliedObjectManager<T> {
        private readonly Dictionary<T, MultipliedObject<T>> _managedObjects;
        private readonly Action<MultipliedObject<T>> _configureObject;

        public MultipliedObjectManager(Action<MultipliedObject<T>> configureObject) {
            if (configureObject == null)
                throw new ArgumentNullException("configureObject");

            _configureObject = configureObject;
            _managedObjects = new Dictionary<T, MultipliedObject<T>>();
        }

        private void SafeRemove(T __instance) {
            if (__instance == null)
                return;

            try {
                _managedObjects.Remove(__instance);
            } catch (Exception e) {
                Console.WriteLine("Error removing __instance from _managedObjects: {0}", e);
            }
        }

        public void OnObjectAttached(T __instance) {
            if (Main.debug.Value)
                Console.WriteLine("{0}.OnAttached", typeof(T).Name);

            if (__instance == null) {
                Console.WriteLine("Attempted to attach null __instance");
                return;
            }

            try {
                if (_managedObjects.ContainsKey(__instance)) {
                    if (Main.debug.Value)
                        Console.WriteLine("{0} already managed, skipping", typeof(T).Name);
                    return;
                }

                var multipliedObject = new MultipliedObject<T>(__instance);
                _configureObject(multipliedObject);
                multipliedObject.CaptureFrom();
                _managedObjects.Add(__instance, multipliedObject);
                multipliedObject.LogValues("Patching");

                ApplyTo(__instance);
                multipliedObject.LogValues("Patched");
            } catch (Exception e) {
                Console.WriteLine("Error in OnObjectAttached: {0}", e);
            }
        }

        public void OnObjectDetached(T __instance) {
            if (Main.debug.Value)
                Console.WriteLine("{0}.OnDetaching", typeof(T).Name);
            if (__instance == null) {
                Console.WriteLine("Attempted to detach null __instance");
                return;
            }

            try {
                MultipliedObject<T> multipliedObject;
                if (_managedObjects.TryGetValue(__instance, out multipliedObject)) {
                    if (Main.debug.Value)
                        multipliedObject.LogValues("Restoring");

                    try {
                        RestoreTo(__instance);
                        multipliedObject.LogValues("Restored");
                    } catch (Exception e) {
                        Console.WriteLine("Error restoring values: {0}", e);
                    }

                    SafeRemove(__instance);
                }
            } catch (Exception e) {
                Console.WriteLine("Error in OnObjectDetached: {0}", e);
            }
        }

        public void ApplyAll(IEnumerable<string> fieldNames = null) {
            if (Main.debug.Value)
                Console.WriteLine("Modifying {0} {1}", _managedObjects.Count, typeof(T).Name);

            // Make a copy of the keys to avoid modification during enumeration
            var instances = _managedObjects.Keys.ToList();

            foreach (var __instance in instances) {
                try {
                    RestoreTo(__instance, fieldNames);
                    ApplyTo(__instance, fieldNames);
                } catch (Exception e) {
                    Console.WriteLine("Error applying to __instance: {0}", e);
                }
            }
        }

        public void ApplyTo(T __instance, IEnumerable<string> fieldNames = null) {
            if (Main.debug.Value)
                Console.WriteLine("Applying {0}", typeof(T).Name);
            if (__instance == null)
                return;

            try {
                MultipliedObject<T> obj;
                if (_managedObjects.TryGetValue(__instance, out obj))
                    obj.ApplyTo(fieldNames);
            } catch (Exception e) {
                Console.WriteLine("Error in ApplyTo: {0}", e);
            }
        }

        public void RestoreTo(T __instance, IEnumerable<string> fieldNames = null) {
            if (Main.debug.Value)
                Console.WriteLine("Restoring {0}", typeof(T).Name);
            if (__instance == null)
                return;

            try {
                MultipliedObject<T> obj;
                if (_managedObjects.TryGetValue(__instance, out obj))
                    obj.RestoreTo(fieldNames);
            } catch (Exception e) {
                Console.WriteLine("Error in RestoreTo: {0}", e);
            }
        }
    }
}
