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


namespace Exploitation
{
    /// <summary>
    /// Implements different evaluation methods for rewards, generating "instantaneous values" for the currently obtained reward at a flower.
    /// Reward parameters such as volume and concentration are integrated to give a value between 0 (bad) and 1 (good) of the obtained reward.
    /// The instantaneous value is used to update for each flower the long-term "remembered" value that is used for action selection.
    /// </summary>
    class EvaluationFamilies : Memory
    {
        double[] rValue; // vector of remembered values for each flower
        double[] pVisit; // vector of visit probabilities for each flower
        double[] decValue; //decayed value after memory decay
        int memoryRule; //Different memoryRule values determine how and when remembered value is updated from the instantaneous value and
        // whether the remembered value is subjected to an avoidance function, that prevents immediate revisits to the same flower
        //memoryRule values: 1 = flowers with lower volumes avoided for longer, gamma updating (see Utdate());
        //2 = no flower revisit avoidance, gamma updating;
        //3 = standard revisit avoidance, remembered value is floating average of instantaneous values;
        //4 = standard revisit avoidance, but no gamma updating on empty flowers (vol = 0);
        //5 = standard revisit avoidance, gamma updating.
        Perception perception; // Different perception.Type values determine how instantaneous value is generated. See alternatives A-H below.
        Physiology physiology;
        double[] memoryRaw;
        double gamma; //update weight, similar to a learning rate. See Utdate()
        double inU = 0; //initial Value

        public EvaluationFamilies(int memoryRule, Perception perception, Physiology physiology, Ressource[] ressources)
            : base(ressources)
        {
            rValue = new double[ressources.Count()];
            decValue = new double[ressources.Count()];
            pVisit = new double[ressources.Count()];
            memoryRaw = new double[2];
            this.memoryRule = memoryRule;
            this.physiology = physiology;
            this.perception = perception;
            gamma = perception.Gamma;

            switch( perception.PerceptionType )
            {

                case 6: case 7: case 8:
                inU = 0; // initial Value equals 0
                break;
                default :
                inU = Lapse / ressources.Count(); // set initial Value from the lapse rate, divided by total number of flowers
                break;
            }

            ResetMemoryContent(ressources);
  
        }

        /// <summary>
        /// Evaluate a reward and store the representation of value into memory
        /// </summary>
        /// <param name="memoryElement">the memory slot where the event is stored</param>
        public override void StoreEvent(MemoryStruct memoryElement)
        {
            double instU = inU;
       

            switch (perception.PerceptionType)
            {
                case 6:
                    //Scalar property implementation. Value is a linear function of physical stimuli, but comparison is between
                    //samples from lognormal distributions representing the physical values
                    instU = linU(lognoise(memoryElement.Reward[1])) * linU(lognoise(memoryElement.Reward[0]));
                    break;
                case 7:
                    // Absolute Evaluation via logarithmic Value function
                    instU = expUVol(memoryElement.Reward[1]) * expUConc(memoryElement.Reward[0]);
                    break;
                case 8:
                    // Caloric value calculation scaled linearly to fit the 0-1 range
                    instU = 1.7 * (15.96 * memoryElement.Reward[1] * (0.05298 * memoryElement.Reward[0] * 
                        memoryElement.Reward[0] + 9.56955 * memoryElement.Reward[0] + 3.32727)) / 1000000;                   
                    break;

            }

            if (physiology.DigestionRate > 0) gamma = -0.08*physiology.Satiety + 0.001; // learning rate linearly dependent on satiety
            // has no effect if digestion rate = 0, as in paper


                switch (MemoryRule) // the case 3 omitted below is implemented in DecisionRules.cs
                                    // it refers to dynamic averageValue update, that is, a floating average of all experienced instantaneous values
                                    // if the window size is very large, that amounts to the long-term average 
            {
                case 1: case 2: case 5:
                        Utdate(memoryElement, instU, gamma); //update remembered value from instantaneous value
                    break;
                    case 4:

                // special case of Utdate: Update remembered value in memory only if current volume larger than 0
                if (memoryElement.Reward[1] > 0)
                {
                    Utdate(memoryElement, instU, gamma);
                }
                else
                {
                    instU = 5; //this output value has no meaning but serves as a marker to be easily filtered out during analysis
                }
                    break;
                }
 
            
            memoryElement.Reward = new RewardStruct(memoryElement.Reward[0], memoryElement.Reward[1], instU);
      
            base.StoreEvent(memoryElement);
        }

         
        public double[] RValue { get { return rValue; } }
        public double[] DecayedValue { get { return decValue; } }
        public double[] PVisit { get { return pVisit; } }
        public double[] MRaw { get { return memoryRaw; } }
        public int MemoryRule { get { return memoryRule; } }
        public int PerceptionType { get { return perception.PerceptionType; } }
        public double Lapse { get { return perception.Lapse; } }
        public double Gamma { get { return perception.Gamma; } }
        public double Satiety { get { return physiology.Satiety; } }

        public override void ResetMemoryContent(Ressource[] ressources)
        {
            base.ResetMemoryContent(ressources);
            foreach (Ressource feeder in ressources)
            {
                //initializing all remembered values at initial neutral or lower value
                rValue[feeder.ID] = inU;
                decValue[feeder.ID] = inU;
                pVisit[feeder.ID] = 1 / ressources.Count();
                memoryRaw[0] = 0;
                memoryRaw[1] = 0;
            }
        }


        /// <summary>
        /// Add noise to a perceived/stored value, according to the scalar property (noise proportional to magnitude of stimulus) 
        /// </summary>
        /// <param name="val">physical value (either a concentration or volume)</param>
        /// <returns>a sample from a lognormal distribution centered at the input value and with a fixed coefficient of variation</returns>
        public double lognoise(double val)
        {
            double CVfactor = 0.2; // the fixed coefficient of variation
            double lognoise = 0;

            if (Math.Log(val) >= 0) //values under 1 are rounded down to 0
            {
                SimpleRNG normal = new SimpleRNG();
                lognoise = Math.Exp(normal.GetNormal(Math.Log(val), Math.Log(val) * CVfactor)); //obtain a lognormal value, reverse transform to original scale
            }
            return lognoise;
        }

        /// <summary>
        /// Update remembered value according to gamma rule (only applied in some perception types) 
        /// </summary>
        /// <param name="memoryElement">a memory slot for an event instance with corresponding perceived reward parameters</param>
        /// <param name="instValue">the instantaneous value</param>
        /// <param name="gamma">the weight put on the current experience</param>
        public void Utdate(MemoryStruct memoryElement, double instValue, double gamma)
        {
            // gamma is the weight put on the current experience, the weaker it is, the stronger the reliance on memory
            // remembered values are updated by averaging with different weights (gamma) the current instantaneous value with the value for 
            // that feeder in the memory slot (remembered value)
            // when gamma is .5, the weights are equal and the result is simple averaging
            //for instValue values between 0 and 1 gamma is equivalent to the alpha learning rate in
            //TD learning

            rValue[memoryElement.RessourceObj.ID] = rValue[memoryElement.RessourceObj.ID] * (1 - gamma) + gamma * instValue;
            // the remembered value vector is updated with the instantaneous value
        }

        /// <summary>
        /// Convert a physical value to a perceived value, with logarithmic scaling
        /// </summary>
        /// <param name="Vol">physical volume in microliters</param>
        /// <returns>a logarithmically scaled value on [0,1]</returns>
        public double expUVol(double Vol)
        {
            // calculate the Value based on the theoretical Value function for volume
            return (1 - Math.Exp(-0.06 * Vol));
        }

        /// <summary>
        /// Convert a physical value to a perceived value, with logarithmic scaling
        /// </summary>
        /// <param name="Conc">physical concentration in % weight/ total weight</param>
        /// <returns>a logarithmically scaled value on [0,1]</returns>
        public double expUConc(double Conc)
        {
            // calculate the Value based on the theoretical Value function for concentration
            return (1 - Math.Exp(-0.06 * Conc));
        }

        /// <summary>
        /// Convert a physical value to a perceived value, with linear scaling so that linU(60)~ 1
        /// </summary>
        /// <param name="val">physical value, concentration or volume [0,60]</param>
        /// <returns>a linearly scaled value on [0,1]</returns>
        public double linU(double val)
        {
            // calculate the Value based on the theoretical Value function for volume with scalar property of noise
            double linvalue;
            if (0.0167 * val > 1) // if value exceeds 1, set it to 1
                linvalue = 1;
            else
                linvalue = 0.0167 * val;

            return linvalue;
        }

        
    }
}

