using System;
using System.Collections.Generic;

namespace ECOT.Engine.Statistics
{
    #region Calculateurs

    public enum VariableComputeMode
    {
        SimpleSum,
        IncrementalSum
    }

    /// <summary>
    /// 
    /// </summary>
    public interface IVariableComputer
    {
        double Prepare(double value);
        double Compute(double value, List<Variable.Dependency> dependencies);
    }

    public abstract class VariableComputerBase : IVariableComputer
    {
        protected VariableComputeMode mode = VariableComputeMode.SimpleSum;

        public VariableComputerBase()
        {
        }

        public VariableComputerBase(VariableComputeMode mode)
        {
            this.mode = mode;
        }

        public abstract double Prepare(double value);
        public abstract double Compute(double value, List<Variable.Dependency> dependencies);
    }

    /// <summary>
    /// 
    /// </summary>
    public class MedianVariableComputer : VariableComputerBase
    {
        public MedianVariableComputer()
        {
        }

        public MedianVariableComputer(VariableComputeMode mode)
            : base(mode)
        {
        }

        public override double Prepare(double value)
        {
            return (this.mode == VariableComputeMode.IncrementalSum ? value : 0.0);
        }

        public override double Compute(double value, List<Variable.Dependency> dependencies)
        {
            // On fait une moyenne pondérée de toutes les dépendances
            double weight = 0.0;
            value = this.Prepare(value);
            foreach (Variable.Dependency var in dependencies)
            {
                value += var.Variable.Compute() * var.Weight * var.Coefficient;
                weight += var.Weight;
            }
            value /= weight;

            return value;
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public class SumVariableComputer : VariableComputerBase
    {
        public SumVariableComputer()
        {
        }

        public SumVariableComputer(VariableComputeMode mode)
            : base(mode)
        {
        }

        public override double Prepare(double value)
        {
            return (this.mode == VariableComputeMode.IncrementalSum ? value : 0.0);
        }

        public override double Compute(double value, List<Variable.Dependency> dependencies)
        {
            // On fait une somme pondérée de toutes les dépendances
            value = this.Prepare(value);
            foreach (Variable.Dependency var in dependencies)
                value += var.Variable.Compute() * var.Weight * var.Coefficient;

            return value;
        }
    }

    #endregion

    #region Variable

    public delegate void VariableEventHandler(Variable var);

    /// <summary>
    /// Cette classe représente une variable gérée par la simulation. Une variable est calculée en fonction
    /// de ses dépendances. Lorsque plusieurs dépendances sont présentes, elle effectue simplement une moyenne,
    /// pondérée ou non, de celles-ci.
    /// </summary>
    public class Variable
    {
        #region Internal

        /// <summary>
        /// Structure interne qui stocke une variable et son coefficient associé pour calculer la moyenne.
        /// Cette structure est utilisée pour les stocker les dépendances.
        /// </summary>
        public class Dependency
        {
            private Variable variable;
            private double weight;
            private double coeff; // poids des stats et coefficient d'ajustement pour certains calculs comme la réputation

            public double Coefficient
            {
                get { return this.coeff; }
                set { this.coeff = value; }
            }

            public double Weight
            {
                get { return this.weight; }
                set { this.weight = value; }
            }

            public Dependency(Variable var, double weight, double coeff)
            {
                this.Variable = var;
                this.Weight = weight;
                this.Coefficient = coeff;
            }

            public Variable Variable
            {
                get { return this.variable; }
                set { this.variable = value; }
            }
        }

        #endregion

        #region Membres

        protected string name;
        protected double value;
        protected bool upToDate = false;
        protected bool computing = false;
        protected IVariableComputer computer = null;
        protected List<Dependency> dependencies = null;
        protected bool active = true;

        #endregion

        #region Initialisation

        /// <summary>
        /// Constructeur par défaut.
        /// </summary>
        public Variable()
            : this("", 0.0)
        {
        }

        /// <summary>
        /// Constructeur qui permet de doner un nom à la variable.
        /// </summary>
        /// <param name="name">Nom de la variable</param>
        public Variable(string name)
            : this(name, 0.0)
        {
        }

        /// <summary>
        /// Constructeur qui permet de donner une valeur à la variable.
        /// </summary>
        /// <param name="value">Valeur de départ de la variable</param>
        public Variable(double value)
            : this("", value)
        {
        }

        /// <summary>
        /// Constructeur qui permet de donner un nom et une valeur à la variable.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public Variable(string name, double value)
            : this(name, value, new MedianVariableComputer())
        {
        }

        public Variable(double value, IVariableComputer computer)
            : this("", value, computer)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <param name="computer"></param>
        public Variable(string name, double value, IVariableComputer computer)
        {
            if (name.Length > 0)
                this.name = name;
            else
                this.name = Utilities.RandomName();
            this.value = value;
            this.computer = computer;
        }

        #endregion

        #region Propriétés

        /// <summary>
        /// Récupère le nom de la variable.
        /// </summary>
        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }

        /// <summary>
        /// Récupère ou assigne la valeur actuelle de la variable.
        /// </summary>
        public double Value
        {
            get { return this.value; }
            set
            {
                this.value = value;
                if (this.ValueChanged != null)
                    this.ValueChanged(this);
            }
        }

        public int ToInt()
        {
            return (int)this.value;
        }

        /// <summary>
        /// Récupère la liste des variables dépendentes.
        /// </summary>
        public List<Dependency> Dependencies
        {
            get { return this.dependencies; }
        }

        /// <summary>
        /// Récupère ou assigne si la valeur actuelle de la variable est à jour ou bien obsolète.
        /// </summary>
        public bool UpToDate
        {
            get { return this.upToDate; }
            set
            {
                if (this.upToDate == value) return;

                // Met à jour la propriété pour la variable et toutes ses dépendances
                this.upToDate = value;
                if (this.dependencies != null)
                    foreach (Dependency dependency in this.dependencies)
                        dependency.Variable.UpToDate = value;
            }
        }

        public bool Computing
        {
            get { return this.computing; }
        }

        #endregion

        #region Events

        public event VariableEventHandler BeforeCompute;
        public event VariableEventHandler AfterCompute;
        public event VariableEventHandler ValueChanged;

        #endregion

        #region Methodes

        /// <summary>
        /// Ajoute une variable, ou une formule, en dépendance. Son coefficient et son poids associés sont automatiquement défini à 1.
        /// </summary>
        /// <param name="var">Variable dépendante</param>
        public void Depend(Variable var)
        {
            this.Depend(var, 1.0, 1.0);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="var"></param>
        public void Depend(string name, Variable var)
        {
            this.Depend(name, var, 1.0, 1.0);
        }

        /// <summary>
        /// Ajoute une variable, ou une formule, en dépendance et permet de spécifier son coefficient associé.
        /// </summary>
        /// <param name="var">Variable dépendante</param>
        /// <param name="weight">Poids associé</param>
        public void Depend(Variable var, double weight)
        {
            if (this.dependencies == null)
                this.dependencies = new List<Dependency>();
            this.dependencies.Add(new Dependency(var, weight, 1.0));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="var"></param>
        /// <param name="weight"></param>
        /// <param name="coeff"></param>
        public void Depend(Variable var, double weight, double coeff)
        {
            if (this.dependencies == null)
                this.dependencies = new List<Dependency>();
            this.dependencies.Add(new Dependency(var, weight, coeff));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="var"></param>
        /// <param name="weight"></param>
        public void Depend(string name, Variable var, double weight, double coeff)
        {
            //Debug.Assert(name.Length > 0);

            var.name = name;
            if (this.dependencies == null)
                this.dependencies = new List<Dependency>();
            this.dependencies.Add(new Dependency(var, weight, coeff));
        }

        public void Undepend(string name)
        {
            if (this.dependencies != null)
            {
                int i = 0;
                for (; i < this.dependencies.Count && this.dependencies[i].Variable.name != name; i++) ;
                if (i < this.dependencies.Count)
                {
                    this.dependencies.RemoveAt(i);
                    if (this.dependencies.Count == 0)
                    {
                        this.value = this.computer.Prepare(this.value);
                        this.dependencies = null;
                    }
                }
            }
        }

        public void Undepend(Variable var)
        {
            this.Undepend(var.name);
        }

        /// <summary>
        /// Récupère une variable dépendante.
        /// </summary>
        /// <param name="name">Nom de la variable à trouver</param>
        /// <returns>La dépendance trouvée ou null</returns>
        public Dependency GetDependency(string name)
        {
            if (this.dependencies != null)
                foreach (Dependency dep in this.dependencies)
                    if (dep.Variable.name.Equals(name))
                        return dep;
            return null;
        }

        /// <summary>
        /// Calcule la variable en parcourant toutes les dépendances. Elle fait une moyenne pondérée du résultat de toutes
        /// les dépendances. Elle appelle également la méthode Compute de chaque variable dépendante.
        /// Afin de s'assurer que l'on ne recalcule pas 2 fois la variable, elle met à jour la propriété UpToDate et
        /// renvoie directement la valeur actuelle si UpToDate est à true.
        /// </summary>
        /// <returns></returns>
        public virtual double Compute()
        {
            if (!this.active) return this.value;

            // Evénement pre calcul
            if (this.BeforeCompute != null)
                this.BeforeCompute(this);

            // La variable a déjà été calculée, on retourne donc sa valeur actuelle
            if (this.upToDate || this.dependencies == null) return this.value;

            // Si la variable est déjà en train d'être calculée et que nous la recalculons à cause d'une de ces dépendances
            // par exemple, on ne fait rien. Cela évite un joli stack overflow dans le cas présent ^^
            if (this.computing) return this.value;
            this.computing = true;

            // Assigne par l'intermédiaire de la propriété pour implicitement appeler l'événement ValueChanged
            this.Value = this.computer.Compute(this.value, this.dependencies);

            // On marque la variable comme calculée
            this.upToDate = true;
            // On marque la variable comme n'étant plus en cours de calcul
            this.computing = false;

            // Evénement post calcul
            if (this.AfterCompute != null)
                this.AfterCompute(this);

            return this.value;
        }

        public override bool Equals(object obj)
        {
            Variable var = (Variable)obj;
            if (object.ReferenceEquals(var, null))
                return false;
            return (this.value == var.value);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public override string ToString()
        {
            return this.value.ToString("F2");
        }

        public static Variable Clamp(Variable var, double min, double max)
        {
            var.value = Math.Max(Math.Min(var.value, max), min);
            return var;
        }

        public static Variable Round(Variable var)
        {
            var.value = Math.Round(var.value);
            return var;
        }

        protected void FireBeforeEvent()
        {
            if (this.BeforeCompute != null)
                this.BeforeCompute(this);
        }

        protected void FireAfterEvent()
        {
            if (this.AfterCompute != null)
                this.AfterCompute(this);
        }

        /// <summary>
        /// Active or disable Variable update
        /// </summary>
        public void Active()
        {
            this.active = true;
        }

        public void Disable()
        {
            this.active = false;
        }
        #endregion

        #region Operators

        public static Variable Exp(double d)
        {
            return new ExpOperation(d);
        }

        public static Variable Exp(Variable var)
        {
            return new ExpOperation(var);
        }

        public static Variable operator +(Variable var1, Variable var2)
        {
            return new AddOperation(var1, var2);
        }

        public static Variable operator +(double var1, Variable var2)
        {
            return new AddOperation(var1, var2);
        }

        public static Variable operator +(Variable var1, double var2)
        {
            return new AddOperation(var1, var2);
        }

        public static Variable operator -(double d, Variable var)
        {
            return new SubOperation(d, var);
        }

        public static Variable operator -(Variable var, double d)
        {
            return new SubOperation(var, d);
        }

        public static Variable operator -(Variable var1, Variable var2)
        {
            return new SubOperation(var1, var2);
        }

        public static Variable operator -(Variable var)
        {
            return new NegateOperation(var);
        }

        public static Variable operator *(Variable var, double d)
        {
            return new MultOperation(var, d);
        }

        public static Variable operator *(double d, Variable var)
        {
            return new MultOperation(d, var);
        }

        public static Variable operator *(Variable var1, Variable var2)
        {
            return new MultOperation(var1, var2);
        }

        public static Variable operator /(Variable var, double d)
        {
            return new DivOperation(var, d);
        }

        public static Variable operator /(Variable var1, Variable var2)
        {
            return new DivOperation(var1, var2);
        }

        public static bool operator ==(Variable var1, Variable var2)
        {
            return var1.Equals(var2);
        }

        public static bool operator !=(Variable var1, Variable var2)
        {
            if (object.ReferenceEquals(var1, null) || object.ReferenceEquals(var2, null))
                return (!(object.ReferenceEquals(var1, null) && object.ReferenceEquals(var2, null)));
            return (var1.value != var2.value);
        }

        public static bool operator <(Variable var1, Variable var2)
        {
            return (var1.value < var2.value);
        }

        public static bool operator <(int i, Variable var)
        {
            return (i < var.value);
        }

        public static bool operator <(Variable var, int i)
        {
            return (var.value < i);
        }

        public static bool operator >(Variable var1, Variable var2)
        {
            return (var1.value > var2.value);
        }

        public static bool operator >(int i, Variable var)
        {
            return (i > var.value);
        }

        public static bool operator >(Variable var, int i)
        {
            return (var.value > i);
        }

        public static Variable operator ++(Variable var)
        {
            var.value++;
            return var;
        }

        public static Variable operator --(Variable var)
        {
            var.value++;
            return var;
        }

        #endregion
    }

    #endregion
}
