///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.Windows.Forms;
using System.IO;
using System.Text;
using System.Windows.Forms.DataVisualization.Charting;
using DocumentFormat.OpenXml.Packaging;
using System.Globalization;

/// <summary>
/// Main application
/// Input: RunParams.xlsx, containing runtime parameters and references to files for agents (see CreateBats.cs) and resources (see CreateFlowers.cs)
/// The output is saved in the Folder "~Exploitation\bin\Debug"
/// </summary>

namespace Exploitation
{
    /// <summary>
    /// Select goal of next action
    /// </summary>
    public static class Exploitation
    {
        static MainForm form;

        static int[][] endOfGenerationInformation;
        // Run Time parameters
        static int[] singleRunTimeSteps;
        static int[] singleRunChoices;
        static int[] sequentialRunGenerations;
        static int[] independentRepeats;
        static string[] subDir, ressourceFile, ressourceTable, agentsFile, agentsTable, resultsEnvFile, resultsAgtFile;
        static int nDatasets;
        
        /// <summary>
        /// The starting point of the application
        /// </summary>
        public static void Start()
        {
            

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            form = new MainForm();
            form.Show();
            SimpleRNG.SetSeedFromSystemTime();
            SimpleRNG rand = new SimpleRNG();
            SimpleRNG.SetSeed(Convert.ToUInt32(rand.GetInt(0, 10000))); 
            ReadRuntimeParameters();
            int z = 0;
            

            for (int x = 0; x < nDatasets; x++) //Main loop, over each line in the "RunParams.xlsx" file
            {
                string filenameAgents = resultsAgtFile[x] + ".csv";
                string filenameEnvironment = resultsEnvFile[x] + ".csv";

                // Filestreams used here to create new files. if they already exist, their contents are deleted
                // The files are defined in the "RunParams.xlsx" file:
                // ResultsAgents - the name of the output file with the individual decisions of each agent. Optional
                // ResultsEnvironment - the name of the output file with the generation-level selection data. Optional 
                FileStream fsOutAgt = new FileStream(filenameAgents, FileMode.Create, FileAccess.Write);
                FileStream fsOutEnv = new FileStream(filenameEnvironment, FileMode.Create, FileAccess.Write);
                
                fsOutAgt.Close();
                fsOutEnv.Close();
            }

            while (form.Created && z < nDatasets) 
            {
            
            // generate the agents and resources from their respective files and tables, as given in the "Runparams.xlsx" file:
            // Subdirectory - the subdirectory where the agent and resource files are located
            // DefRessources - the name of the xlxs file with the resource table to be used
            // ResTable - the name of the table of resources to be used
            // DefAgents - the name of the xlsx file with agent table to be used
            // AgtTable - the name of the table of agents to be used
            CreateRessources createRessources = new CreateFlowers(subDir[z], ressourceFile[z] + ".xlsx", ressourceTable[z]);
            CreateAgents createAgents = new CreateBats(subDir[z], agentsFile[z] + ".xlsx", agentsTable[z]);


                RessourceManager ressources = createRessources.CreateRessources();
                AgentManager agents = createAgents.CreateAgents(ressources.Ressources);

                for (int x = 0; x < independentRepeats[z]; x++)   // loop over the number of "IndependentRepeats" from the input table in RunParams.xlsx
                {
                    Run(agents, ressources, x, z);
                }
                z++;
            }
        }

        /// <summary>
        /// For given agents and resource input tables, generate agent-resource interaction events until break criteria are met
        /// (e.g. time, number of visits, number of generations)
        /// </summary>
        /// <param name="agentManager">the selection agents (bats), as defined in the input file</param>
        /// <param name="ressourceManager">the ressources, as defined in the input file</param>
        /// <param name="runID">the run number</param>
        /// <param name="nDataset">the data set number</param>
        internal static void Run(AgentManager agentManager, RessourceManager ressourceManager, int runID, int nDataset)
        {

            int generation = 0;
            endOfGenerationInformation = new int[sequentialRunGenerations[nDataset]][];
            string separator = ";";
            string filenameEnvironment = resultsEnvFile[nDataset] + ".csv";
            string filenameAgents = resultsAgtFile[nDataset] + ".csv";
            double meanC = 0;
            
            //Filestreams for the output data, where runs are appended to the end of the same output files
            FileStream fsOutEnv = new FileStream(filenameEnvironment, FileMode.Append, FileAccess.Write);
            using (StreamWriter swOutEnv = new StreamWriter(fsOutEnv))
            {
            
                FileStream fsOutAgt = new FileStream(filenameAgents, FileMode.Append, FileAccess.Write);
                using (StreamWriter swOutAgt = new StreamWriter(fsOutAgt))
                {

            //Set the column titles in the agent output during the first run, if different files used for output
            if (runID == 0)
            {
                if ((nDataset == 0) || ((nDataset > 0) && (resultsAgtFile[nDataset] != resultsAgtFile[nDataset - 1])))
                {
                    swOutAgt.WriteLine("Time;AgentID;ResourceID;ResourceX;ResourceY;ResourceZ;Concentration;Volume;"+
                        "kJ;Generation;Run;Visit;iValue;rValue;pValue;Revisit;PerceptionType;Gamma;Lapse;ResTable;" +
                        "TimeSinceLast;TimeSinceLastR;Genotype;Seed");
                }
            //Set the column titles in the environment output during the first generation, if different files used for output    
                if ((nDataset == 0) || ((nDataset > 0) && (resultsEnvFile[nDataset] != resultsEnvFile[nDataset - 1])))
                {
                    swOutEnv.WriteLine("Agents;ResTable;NPR;Run;Generation;Mean;MeanPPO;Response;StDev;A;B;C;D;HetA;HetB;HetC;HetD");
                }
            }
            
            EventQueue eventQueue = new EventQueue();
            long time = 0;
            Position location = new Position(); 

            Agent[] agents = agentManager.Agents;

            Boolean revisit = false;

            CreateRessources createRessources = new CreateFlowers(subDir[nDataset], ressourceFile[nDataset] + ".xlsx", ressourceTable[nDataset]);
            PPO.Instance.InitializePPO();
            ressourceManager = createRessources.CreateRessources();
            Ressource[] ressources = ressourceManager.Ressources;

            foreach (Agent agent in agents)
            {
                agent.ResetMemoryContent(ressources); //memory is reset after every independent repeat
                agent.Physiology.ResetSatiety(); // physiology is reset after every independent repeat
             
                if (agent.altvisits > 0) // checks whether there are forced alternation choices
                {
                    eventQueue.AddEventRequest(agent.AltEventRequest(time, 0, agent.AltSequence)); // first decisions under the alternation scheme
                }
                else
                {
                    eventQueue.AddEventRequest(agent.EventRequest(time)); 
                            // the first decisions are given using the decision algorithm (amounts to random for the first choice)
                }
            
            }

            //loop over number of individual events
            while (eventQueue.EventsLeft)
            {

                EventRequest currentEvent = eventQueue.Dequeue();

                time = currentEvent.Time;
                location = new Position() { X = currentEvent.Ressource.XPos, Y = currentEvent.Ressource.YPos, Z = currentEvent.Ressource.ZPos };

                Dictionary<Position, List<MemoryStruct>> memories = agents[currentEvent.Agent.ID].memoryrecord.CompleteMemory();
                        //dump memory of an agent to a variable to make it accessible

           
                revisit = false;
                if (memories.Count() > 0)
                {
                    revisit = agents[currentEvent.Agent.ID].memoryrecord.LastRessource() == currentEvent.Ressource.ID;
                }


                if (singleRunTimeSteps[nDataset] > 0 && time > singleRunTimeSteps[nDataset]) // end run: time reached
                {
                    if (sequentialRunGenerations[nDataset] > 0)
                    {
                        double meanpotential = PPO.Instance.MeanPotential(); // calculate the mean concentration of the PPO
                        
                        string[] genotypes = new string[ressources.Length];

                        foreach (Flower ressource in ressources)
                        {
                            genotypes[ressource.ID] = ressource.Genotype; //create an array of coded genomes for further calculations
                        }

                        meanC = PPO.Instance.MeanConc(genotypes);


                        //return the values for the EnvResults file:
                        swOutEnv.WriteLine(agentsTable[nDataset] + separator + ressourceTable[nDataset] + separator +
                            (ressources[0] as Flower).nectarSugarProduction + separator + (runID + 1) + separator +
                            generation + separator + meanC + separator + meanpotential + separator +
                            (meanpotential - meanC) + separator +
                            PPO.Instance.SDConc(genotypes) + separator + PPO.Instance.AlleleFreq(genotypes,0) + separator +
                            PPO.Instance.AlleleFreq(genotypes,1) + separator + PPO.Instance.AlleleFreq(genotypes,2) + separator +
                            PPO.Instance.AlleleFreq(genotypes,3) + separator + PPO.Instance.Heterozygosity(genotypes,0) + separator +
                            PPO.Instance.Heterozygosity(genotypes,1) + separator + PPO.Instance.Heterozygosity(genotypes,2) + separator +
                            PPO.Instance.Heterozygosity(genotypes, 3)
                            );
                        
                        ressources = ressourceManager.CreateNewGeneration(); //apply the SUS to select new offspring individuals
                        agents = agentManager.CreateNewGeneration(ressources);
                        PPO.Instance.InitializePPO();
                        time = 0;
                        eventQueue = new EventQueue();
                        foreach (Agent agent in agents)
                        {
                            //agent.ResetMemoryContent(ressources); //memory is reset after every generation

                            if (agent.altvisits > 0) // checks whether there are forced alternation choices and makes first choices for next generation
                            {
                                eventQueue.AddEventRequest(agent.AltEventRequest(time, 0, agent.AltSequence)); 
                                        // first decisions under the alternation scheme
                            }
                            else
                            {
                                eventQueue.AddEventRequest(agent.EventRequest(time)); 
                                        // the first decisions are given using the decision algorithm (amounts to random for the first choice)
                            } 
                        }

                    }
                        generation++;
                        if (sequentialRunGenerations[nDataset] == 0 || generation == sequentialRunGenerations[nDataset]) break;
                        continue;
                }
                int numVisit = 0;
                
                if (memories.ContainsKey(location)) {
                    numVisit =  memories[location].Count + 1;
                }
                    else 
                {
                    numVisit = 1;
                }
                RewardStruct reward = currentEvent.Ressource.DeliverReward(currentEvent.MealMax, time, numVisit);    
                string seedling = "X";
                if ((revisit == false) && (agents[currentEvent.Agent.ID].memoryrecord.CountMemories() > 0)) // this line prevents selfing
                //if ((agents[currentEvent.Agent.ID].memoryrecord.CountMemories() > 0)) // this line does not prevent selfing
                {
                    currentEvent.Ressource.TransferToRessource(currentEvent.TransferToRessource);
                    seedling = ressources[currentEvent.Ressource.ID].getSeed();
                }

               
                currentEvent.Agent.EventFeedback(reward, new Pos(location.X, location.Y, location.Z), currentEvent.Ressource, currentEvent.Ressource.CheckForTransferToAgent(), time);

                double prob = (agents[currentEvent.Agent.ID].memoryrecord as EvaluationFamilies).PVisit[currentEvent.Ressource.ID];


                //check if an individual has reached the maximum number of choices, if yes, no further choices made
                if ((singleRunChoices[nDataset] == 0) || (agents[currentEvent.Agent.ID].memoryrecord.CountMemories() -
                            agents[currentEvent.Agent.ID].altvisits < singleRunChoices[nDataset]))
                {
                    if ((agents[currentEvent.Agent.ID].altvisits > 0) && (agents[currentEvent.Agent.ID].memoryrecord.CountMemories() <
                                agents[currentEvent.Agent.ID].altvisits))
                        
                        // checks whether there are forced alternation choices left and makes the appropriate decision
                    {
                        eventQueue.AddEventRequest(agents[currentEvent.Agent.ID].AltEventRequest(time, agents[currentEvent.Agent.ID].
                            memoryrecord.CountMemories(), agents[currentEvent.Agent.ID].AltSequence)); // decision under the alternation scheme
                    }
                    else
                    {
                        eventQueue.AddEventRequest(agents[currentEvent.Agent.ID].EventRequest(time)); // decisions are given using the decision algorithm
                    }
                }

                if ((currentEvent.Agent.Personality.PersonalityType != 10) && (filenameAgents != ".csv")) 
                            // personality type 10 means the bat is a phantom bat whose visits are not recorded to save memory
                {   
                    swOutAgt.WriteLine(time + separator +
                                       currentEvent.Agent.ID + separator +
                                       (currentEvent.Ressource.ID + 1) + separator +
                                       location.X + separator +
                                       location.Y + separator +
                                       location.Z + separator +
                                       reward.Reward.ToString(separator) +
                                       generation + separator +
                                       (runID + 1) + separator +
                                       agents[currentEvent.Agent.ID].memoryrecord.CountMemories() + separator +
                                       memories[location].Last<MemoryStruct>().Reward[2] + separator +
                                       (agents[currentEvent.Agent.ID].memoryrecord as EvaluationFamilies).RValue[currentEvent.Ressource.ID] + separator +
                                       prob + separator + 
                                       Convert.ToInt32(revisit) + separator +
                                       agents[currentEvent.Agent.ID].Perception.PerceptionType + separator +
                                       agents[currentEvent.Agent.ID].Perception.Gamma + separator + 
                                       agents[currentEvent.Agent.ID].Perception.Lapse + separator + ressourceTable[nDataset] + separator +
                                       agents[currentEvent.Agent.ID].memoryrecord.LastVisitTime(time)  + separator +
                                       agents[currentEvent.Agent.ID].memoryrecord.LastRVisitTime(time, location)  + separator +
                                       currentEvent.Ressource.Genotype + separator + 
                                       seedling
                                       );
                }
                form.labelStep.Text = "Step: " + time + "\nChoices: " + agents[currentEvent.Agent.ID].memoryrecord.CountMemories() +
                            "\nRepeat:" + runID + "\nGeneration: " + generation +"\nData Set: " + nDataset + "\nMean Concentration: " +
                            meanC + "\nBats: " + agentsFile[nDataset] + "\nBatTable: " + agentsTable[nDataset] ;
                Application.DoEvents();
                if (!form.Created) break;

                System.Threading.Thread.Sleep(0);
            }

        }
                fsOutAgt.Close();
    }
                
                fsOutEnv.Close();
                
         }

        /// <summary>
        /// Read the runtime parameters from RunParams.xlsx file, containing references to files for agents and resources
        /// </summary>
        internal static void ReadRuntimeParameters()
        { 
            NumberFormatInfo info = new CultureInfo("en-US", false).NumberFormat;  // -> suppress US Format
            SpreadsheetDocument spreadsheet = SpreadsheetDocument.Open("RunParams.xlsx", false);
            Table runParameter = spreadsheet.Table("RunParams");
            nDatasets = runParameter.TableRows().Count();
            subDir = new string[nDatasets];
            singleRunTimeSteps = new int[nDatasets];
            singleRunChoices = new int[nDatasets];
            sequentialRunGenerations = new int[nDatasets];
            independentRepeats = new int[nDatasets];
            ressourceFile = new string[nDatasets];
            ressourceTable = new string[nDatasets];
            agentsFile = new string[nDatasets];
            agentsTable = new string[nDatasets];
            resultsAgtFile = new string[nDatasets];
            resultsEnvFile = new string[nDatasets];
            int i = 0;

            foreach (var r in runParameter.TableRows())
            {
                subDir[i] = r["SubDirectory"].Value;
                singleRunTimeSteps[i] = int.Parse(r["SingleRunTimeSteps"].Value);
                singleRunChoices[i] = int.Parse(r["SingleRunChoices"].Value);
                sequentialRunGenerations[i] = int.Parse(r["SequentialRunGenerations"].Value);
                independentRepeats[i] = int.Parse(r["IndependentRepeats"].Value);
                ressourceFile[i] = r["DefResources"].Value;
                ressourceTable[i] = r["ResTable"].Value;
                agentsFile[i] = r["DefAgents"].Value;
                agentsTable[i] = r["AgtTable"].Value;
                resultsAgtFile[i] = r["ResultsAgents"].Value;
                resultsEnvFile[i] = r["ResultsEnvironment"].Value;
                i++;
            }
        }

        /// <summary>
        /// Return true if input is an odd integer
        /// </summary>
        /// <param name="value">integer, whose oddity is checked</param>
        /// <returns></returns>
        public static bool IsOdd(int value)
        {
            return value % 2 != 0;
        }


        public static string ToString(this double[] values, string separator)
        {
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < values.Length; i++)
            {
                sb.Append(values[i] + (i == values.Length ? "" : separator));
            }

            return sb.ToString();
        }

     
    }
}
