///Licensed under Creative Commons Attribution 4.0 License, Written by Clemens Winter and Vladislav Nachev
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Exploitation
{

    public interface Physiology
    {
        /// <summary>
        /// determines the time point when next foraging event will start
        /// </summary>
        /// <returns>time point of next foraging event</returns>
        int IntervalToNextEvent();

        /// <summary>
        /// returns the outcome of a feeding event to the physiology module
        /// </summary>
        /// <param name="gain">gross energy intake</param>
        /// <param name="timeCost">time cost of foraging event</param>
        /// <returns>time point of next foraging event</returns>
        void ReturnActionOutcome(RewardStruct reward, long intervalLastVisit);
        void ResetSatiety();

        int MealMax { get; }
        double Satiety { get; }
        double DigestionRate { get; }
    }


    /// <summary>
    /// Call module to decide about the goal of the next action
    /// </summary>
    public abstract class Decision
    {
        protected Memory memory;
        protected int decisionType;

        public Decision(int decisionType, Memory memory)
        {
            this.decisionType = decisionType;
            this.memory = memory;

        }




        /// <summary>
        /// Select goal of next action
        /// </summary>
        /// <returns>Returns the resource object that is visited next</returns>
        public abstract Ressource RessourceGoal(long time);

        /// <summary>
        /// Select goal of next action under strict alternation rules
        /// </summary>
        /// <returns>Returns the resource object that is visited next depending on strict alternation rule</returns>
        public abstract Ressource RessourceAltGoal(long time, int altvisit, int[] altSequence);

    }

    public class Perception
    {
        /// <summary>
        /// Psychophysical parameters for reward evaluation
        /// </summary>

        protected int perceptionType;
        protected double gamma;
        protected double lapse;
        protected double thresholdV;
        protected double slopeV;
        protected double thresholdC;
        protected double slopeC;

        public Perception(int perceptionType, double gamma, double lapse, double thresholdV, double slopeV, double thresholdC, double slopeC)
        {
            this.perceptionType = perceptionType;
            this.gamma = gamma;
            this.lapse = lapse;
            this.thresholdV = thresholdV;
            this.slopeV = slopeV;
            this.thresholdC = thresholdC;
            this.slopeC = slopeC;
        }
        public int PerceptionType { get { return perceptionType; } }
        public double Gamma { get { return gamma; } }
        public double Lapse { get { return lapse; } }
        public double ThresholdV { get { return thresholdV; } }
        public double SlopeV { get { return slopeV; } }
        public double ThresholdC { get { return thresholdC; } }
        public double SlopeC { get { return slopeC; } }
    }

    /// <summary>
    /// Interaction with the memory module representing the memory of a subject's experience 
    /// with the environment
    /// This module is used by decision making and is updated after each ressource encounter
    /// </summary>
    public abstract class Memory
    {
        Ressource[] ressources;
        protected Dictionary<Position, List<MemoryStruct>> memories;


        public Memory(Ressource[] ressources)
        {
            this.ressources = ressources;
            memories = new Dictionary<Position, List<MemoryStruct>>();
        }


        /// <summary>
        /// Stores the outcome of a ressource visitation event in memory 
        /// </summary>
        /// <param name="memoryElement">the memory component of a ressource visit</param>
        public virtual void StoreEvent(MemoryStruct memoryElement)
        {
            Position key = new Position() { X = memoryElement.Pos.XPos, Y = memoryElement.Pos.YPos, Z = memoryElement.Pos.ZPos };
            if (!memories.ContainsKey(key)) memories[key] = new List<MemoryStruct>();
            memories[key].Add(memoryElement);

        }

        /// <summary>
        /// complete list of all memory items
        /// </summary>
        /// <returns>complete memory list</returns>
        public virtual Dictionary<Position, List<MemoryStruct>> CompleteMemory() { return memories; }

        /// <summary>
        /// Returns the total number of events stored in memory 
        /// </summary>
        /// <returns>complete number of memories</returns>
        public int CountMemories()
        {
            int totalEvents = 0;
            foreach (Position key in memories.Keys)
            {
                totalEvents += memories[key].Count;
            }
            return totalEvents;
        }
        /// <summary>
        /// Find the last visited resource in order to check (elsewhere) if the current visit is a revisit or not
        /// </summary>
        /// <returns>ID of last visited resource</returns>
        public int LastRessource()
        {

            long time = 0;
            Position lastKey = new Position();
            foreach (Position key in memories.Keys)
            {
                int timeLastVisit = memories[key].Count - 1;
                long tmp = memories[key][timeLastVisit].Time;
                if (tmp > time)
                {
                    time = tmp;
                    lastKey = key;
                }
            }

            return memories[lastKey].First<MemoryStruct>().RessourceObj.ID;
        }

        /// <summary>
        /// Find the last visited resource
        /// </summary>
        /// <returns>Key of last visited resource</returns>
        public Position LastRessourceKey()
        {

            long time = 0;
            Position lastKey = new Position();
            foreach (Position key in memories.Keys)
            {
                int timeLastVisit = memories[key].Count - 1;
                long tmp = memories[key][timeLastVisit].Time;
                if (tmp > time)
                {
                    time = tmp;
                    lastKey = key;
                }
            }

            return lastKey;
        }

        /// <summary>
        /// Find the time elapsed since the previous visit (useful to control bout structure)
        /// </summary>
        /// <returns>the time elapsed since any resource was visited before the current event (0 if never)</returns>
        public long LastVisitTime(long time)
        {

            long LastTime = 0;

            if (memories.Values.Count() > 0)
            {
                foreach (Position key in memories.Keys)
                {
                    long tmp = memories[key][memories[key].Count() - 1].Time;
                    if (tmp == time)
                    {
                        if (memories[key].Count() > 1)
                        {
                            tmp = memories[key][memories[key].Count() - 2].Time;
                        }
                        else
                        {
                            tmp = 0;
                        }
                    }
                    if (tmp > LastTime)
                    {
                        LastTime = tmp;
                    }
                }

                if (LastTime != 0) LastTime = time - LastTime;

            }
            return LastTime;
        }

        /// <summary>
        /// Find the time elapsed since the previous visit to the same resource (useful to control bout structure)
        /// </summary>
        /// <returns>the time elapsed since the current resource was visited before the current event (0 if never)</returns>
        public long LastRVisitTime(long time, Position key)
        {

            long LastTime = 0;

            if (memories[key].Count() > 1)
            {

                LastTime = memories[key][memories[key].Count() - 2].Time;
            }
            else
            {
                LastTime = 0;
            }

            if (LastTime != 0) LastTime = time - LastTime;


            return LastTime;
        }

        /// <summary>
        /// Average the instantaneous values over the last n events at a particular resource to obtain its remembered value
        /// </summary>
        /// <param name="key">the key of the resource</param>
        /// <param name="windowsize">the number of events over which to average</param>
        /// <returns>remembred value on [0,1]</returns>
        public double DynAverageUtDate(Position key, int windowsize)
        // average the remembered values over the last n events at that location. n = windowsize

        {
            double sum = 0;
            int memoriescount = memories[key].Count();
            if (memoriescount > 0)
            {
                for (int i = memoriescount - 1; i > -1; i--)
                {
                    if (i < memoriescount - windowsize) break;
                    sum += memories[key][i].Reward[2];
                }
            }
            return sum / Math.Min(memoriescount, windowsize);
        }


        public Ressource[] Ressources { get { return ressources; } }

        public virtual void ResetMemoryContent(Ressource[] ressources)
        {
            memories = new Dictionary<Position, List<MemoryStruct>>();
            this.ressources = ressources;
        }
    }



    public struct Position
    {
        public int X;
        public int Y;
        public int Z;
    }

    /// <summary>
    /// The personality class could be used to implement special individual behavior rules. Currently its only use is to make a
    /// bat a "phantom" whose visits are not recorded in the Agents output (by setting personalityType = 10). This can be useful
    /// if the behavior of bats in different groups with different parameters is to be analyzed, so that only the data of the focal
    /// bats is saved, to prevent unnecessarily large output files.
    /// </summary>
    public class Personality
    {
        protected int personalityType;

        public Personality(int personalityType)
        {
            this.personalityType = personalityType;

        }
        public int PersonalityType { get { return personalityType; } }
    }

    /// <summary>
    /// Pollen particles are transferred from the bats to the flowers and/or from the flowers to the bats.
    /// </summary>
    class TransferParticles
    {
        double[] values;

        public TransferParticles(params double[] values)
        {
            this.values = values;
        }

        public int NumberOfValues { get { return values.Length; } }

        public double this[int i]
        {
            get { return values[i]; }
        }
    }


    /// <summary>
    /// rewards are obtained from resources and such events are stored in memory
    /// </summary>
    public struct RewardStruct
    {
        public double[] Reward;

        public RewardStruct(params double[] rewValue)
        {
            Reward = rewValue;
        }


        public double this[int i]
        {
            get { return Reward[i]; }
            set { Reward[i] = value; }
        }
    }
    /// <summary>
    /// A memory consists in the when (time, which (ressourceObj), what (reward), and where (pos) components
    /// </summary>
    public class MemoryStruct
    {
        public long Time;
        public Ressource RessourceObj;
        public RewardStruct Reward;
        public Pos Pos;


        public MemoryStruct(long time, Ressource ressourceObj, RewardStruct reward, Pos pos)
        {
            Time = time;
            RessourceObj = ressourceObj;
            Reward = reward;
            Pos = pos;
        }

    }

    /// <summary>
    /// Selection algorithms implemented in this class. PPO stands for pool of potential offspring.
    /// </summary>
    public class PPO
    {
        public Dictionary<string, int> Seeds;
        private static PPO instance;

        private PPO()
        {
            Seeds = new Dictionary<string, int>();
        }

        public static PPO Instance { get {if(instance == null)
            instance = new PPO();
        return instance;
        }
        }

        /// <summary>
        /// Initialize the Poof of Potential Offspring by generating all possible genotypes and set frequencies to zero
        /// internally, genotypes are encoded as 4-digit strings, one digit for each gene.
        /// 1 = heterozygous, 2 = "dominant" homozygous (A,B,C,D), 0 = "recessive" homozygous (a,b,c,d). E.g. AaBBccDd = 1201
        /// </summary>
        public void InitializePPO()
        {
            Seeds.Clear();
            int gencount = 0;
            for (int a = 0; a < 3; a++)
            {
                for (int b = 0; b < 3; b++)
                {
                    for (int c = 0; c < 3; c++)
                    {
                        for (int d = 0; d < 3; d++)
                        {
                            Seeds.Add(a.ToString() + b.ToString() + c.ToString() + d.ToString(), 0);
                            gencount++;
                        }
                    }
                }
            }
        }

        /// <summary>
        ///  Sort by frequency so that Stochastic Universal Sampling may be applied
        /// </summary>
        public void SortbyFreq()
        {
                Seeds = Seeds.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
        }

        /// <summary>
        /// generate a new seed of the given genotype
        /// </summary>
        /// <param name="genotype"></param>
        public void AddSeed(string genotype)
        {
            Seeds[genotype]++;
        }

        /// <summary>
        /// Calculate the mean concentration of the pool of potential offspring before SUS
        /// </summary>
        /// <returns>mean concentration of pool of potential offspring</returns>
        public double MeanPotential()
        {
            double sumConcentrations = 0;
            double sumFrequencies = 0;
            foreach (string genotype in Seeds.Keys)
            {
                sumConcentrations += NumtoConc(genotype) * Seeds[genotype];
                sumFrequencies += Seeds[genotype];
            }
            return sumConcentrations / sumFrequencies;
        }

        /// <summary>
        /// calculate the mean concentration of an array of genotypes
        /// </summary>
        /// <param name="Genotypes">genotype array</param>
        /// <returns>mean concentration of genotype array</returns>
        public double MeanConc(string[] Genotypes) 
        {

           double ConcentrationSum = 0;

            for (int i = 0; i < Genotypes.Length; i++)
            {
               ConcentrationSum += PPO.Instance.NumtoConc(Genotypes[i]);
            }

            return ConcentrationSum / Genotypes.Length;

        }

        /// <summary>
        /// Calculate the standard deviation of an array of genotypes after they are converted to concentrations
        /// </summary>
        /// <param name="Genotypes">genotype array</param>
        /// <returns>standard deviation of genotype array's concentrations</returns>
        public double SDConc(string[] Genotypes) 
        {
            double ConcentrationSS = 0;
            double average = 0;

            average = PPO.Instance.MeanConc(Genotypes);


            for (int i = 0; i < Genotypes.Length; i++)
            {
                ConcentrationSS += (PPO.Instance.NumtoConc(Genotypes[i]) - average) * (PPO.Instance.NumtoConc(Genotypes[i]) - average);
            }

            return Math.Sqrt(ConcentrationSS / Genotypes.Length);
        }

        /// <summary>
        /// Calculate the allele frequency for a given gene
        /// </summary>
        /// <param name="Genotypes">genotype array</param>
        /// <param name="gene">gene code (A=0,B=1,C=2,D=3)</param>
        /// <returns>Allele frequency of the allele with the higher phenotypic value (e.g. A, B, C, D, and NOT a, b, c, d)</returns>
        public double AlleleFreq(string[] Genotypes,int gene) // 
            //for a given allele in an array of genotypes
        {
            double Freq = 0;

            for (int i = 0; i < Genotypes.Length; i++)
            {
                Freq += int.Parse(Genotypes[i].Substring(gene, 1));
            }

            return Freq / (2 * Genotypes.Length);
        }

        /// <summary>
        /// Calculate the Heterozygosity for a given gene in an array of genotypes
        /// </summary>
        /// <param name="Genotypes">genotype array</param>
        /// <param name="gene">gene code (A=0,B=1,C=2,D=3)</param>
        /// <returns>Heterozygosity of the gene</returns>
        public double Heterozygosity(string[] Genotypes, int gene) 
        {
            double het = 0;
            
            for (int i = 0; i < Genotypes.Length; i++)
            {
                if (int.Parse(Genotypes[i].Substring(gene, 1)) == 1) het += 1;
            }

            return het / (Genotypes.Length);
        }


        
        /// <summary>
        /// Calculate the concentration (phenotypic value) from the genotype
        /// </summary>
        /// <param name="genotype">genotype code</param>
        /// <returns>concentration in % w/w as deterministically encoded by the genome</returns>
        public double NumtoConc(string genotype)
        {
            double concDA = 7.5; //fixed concentration values for the different alleles
            double concra = 1.5;
            double concDD = 2.5;
            double concrd = 0.5;
            int num_genes = 4;

            double units = int.Parse(genotype.Substring(3, 1));
            double tens = int.Parse(genotype.Substring(2, 1));
            double hundreds = int.Parse(genotype.Substring(1, 1));
            double thousands = int.Parse(genotype.Substring(0, 1));
            return (thousands + hundreds + tens) * concDA + (2 * (num_genes - 1) - (thousands + hundreds + tens)) *concra +
                units * concDD + (2 - units) * concrd;
        }

        /// <summary>
        /// Stochastic Universal Sampling implementation
        /// </summary>
        /// <param name="num_selected">number of individuals to be selected</param>
        /// <returns>A number of individuals from a population of individuals (pool of potential offspring), so that
        /// the selected individuals stochastically represent the genotype frequencies of the whole population (pool of potential offspring)</returns>
        public string[] SUS(int num_selected)
        {
            double[] arrSUS = new double[num_selected];
            string[] arrSelected = new string[num_selected];
            double total_instances;
            int num_categories;


            total_instances = PPO.Instance.Seeds.Sum(x => x.Value);
            num_categories = PPO.Instance.Seeds.Where(x => x.Value > 0).Count();
            double[] arrFreq = new double[num_categories+1];
            string[] arrGen = new string[num_categories];

            int k = 1; // calculate the relative frequencies of the different genotypes and store in a list of cumulative frequencies (adding up to 1)
            arrFreq[0] = 0;

            foreach (string genome in PPO.Instance.Seeds.Keys)
            {
                if (k > num_categories) break;
                    arrFreq[k] = PPO.Instance.Seeds[genome] / total_instances;
                    arrGen[k-1] = genome;
                    if (k > 0) arrFreq[k] = arrFreq[k - 1] + arrFreq[k];
                    k++;
            }
            arrFreq[num_categories] = 1; //make sure that rounding error does not cause the sum to be larger than 1

            SimpleRNG rnd = new SimpleRNG();
            arrSUS[0] =  (1 / System.Convert.ToDouble(num_selected) ) * rnd.GetUniform(); //randomly position the first tooth of the SUS comb


            for (k = 1; k < num_selected; k++)

            {
                arrSUS[k] = arrSUS[k - 1] + (1 / System.Convert.ToDouble(num_selected)); // calculate the positions of the rest of the teeth 
                //of the SUS comb
            }
           
            if (arrSUS[num_selected -1] >= 1)   arrSUS[num_selected-1] = 0.99999999; //make sure that roudning error does not cause the result to 
                // be larger than 1

            //return the genotypes the SUS comb teeth are pointing to

            int lowbound = 0;
            int hibound = 1;

            for (k = 0; k < num_selected; k++)
            {
            test:
                if (((arrSUS[k] >= arrFreq[lowbound]) && (arrSUS[k] < arrFreq[hibound])))
                {
                    arrSelected[k] = arrGen[lowbound];
                }
                else
                {
                    lowbound++;
                    hibound++;
                    goto test;
                }
                    
            }
            
            //reshuffle arrSelected in a random order
            try
            {
                arrSelected = arrSelected.OrderBy(x => rnd.GetInt(1, 100)).ToArray();
                return arrSelected;
            }
            catch (Exception ex)
            {
                Console.WriteLine("HelpLink = {0}", ex.HelpLink);
                Console.WriteLine("Message = {0}", ex.Message);
                Console.WriteLine("Source = {0}", ex.Source);
                Console.WriteLine("StackTrace = {0}", ex.StackTrace);
                Console.WriteLine("TargetSite = {0}", ex.TargetSite);
                return arrSelected;
            }

        }
    }

}