/*
    Configurable Ores - A Space Engineers mod for managing planetary ores.
    Copyright 2022 Travis Wichert

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

using System;
using System.Collections;
using System.Runtime;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Xml.Serialization;
using Microsoft.VisualBasic;
using Sandbox.Common.ObjectBuilders;
using Sandbox.Common.ObjectBuilders.Definitions;
using Sandbox.Definitions;
using Sandbox.Game;
using Sandbox.Game.Debugging;
using Sandbox.Game.Entities;
using Sandbox.Game.EntityComponents;
using Sandbox.Game.Screens.ViewModels;
using Sandbox.Game.World.Generator;
using Sandbox.ModAPI;
using Sandbox.ModAPI.Interfaces;
using Sandbox.ModAPI.Interfaces.Terminal;
using SpaceEngineers.Game.ModAPI;
using VRage;
using VRage.Game;
using VRage.Game.Components;
using VRage.Game.Entity;
using VRage.Game.GUI.TextPanel;
using VRage.Game.ModAPI;
using VRage.Game.VisualScripting.Utils;
using VRage.ModAPI;
using VRage.ObjectBuilders;
using VRage.Serialization;
using VRage.Serialization.Xml;
using VRage.Utils;
using VRageMath;
using static System.Int32;
using static System.String;
using static ConfigurableOres.Helpers;
using static ConfigurableOres.Strings;

namespace ConfigurableOres
{
    #region MyPlanetConfiguration

    [Serializable]
    public class MyPlanetConfiguration
    {
        #region Not Chat Configurable
        
        private static readonly ConfigurableOres Session = ConfigurableOres.Instance;
        private Configuration Config;

        public int OreSlotCount;
        public int LargestOremappingCount;
        public List<byte> AssignableOresBlueChannelValues = new List<byte>();
        public double MeanFrequency;
        public float MeanDepthStart;
        public float MeanDepthSize;
        public float MeanRarity;
        public string PlanetTargetColor;
        public float PlanetColorInfluence;
        public List<string> MyDefaultOreAssignments = new List<string>();

        #endregion

        #region Chat Configurable

        public MyDepthSettings Depth;
        public MySizeSettings Size;

        // cruise control depth
        /*public float PlanetDepthMin;
        public float PlanetDepthMax;
        public bool PlanetAutoDepth;
        public bool PlanetUseDetector;*/

        /*public bool PlanetAutoSize;
        public float PlanetSizeMin;
        public float PlanetSizeMax;*/

        #endregion

        #region Assignments

        //These are generated by raffling algorithm.
        public List<MyPlanetOreMapping> GeneratedPlanetOreMappings = new List<MyPlanetOreMapping>();

        // Default assigned ores are collected from PlanetGeneratorDefinitions.
        public List<MyPlanetOreAssignment> AssignedOres = new List<MyPlanetOreAssignment>();
        public List<MyPlanetOreAssignment> StaticOres = new List<MyPlanetOreAssignment>();

        #endregion

        // Parameterless Constructor for XML serializer
        public MyPlanetConfiguration()
        {
        }

        public MyPlanetConfiguration(IEnumerable<MyPlanetOreMapping> planetOreMappings)
        {
            LogBegin(LOG_CREATE_PLANET_CONFIG);

            Depth = Config.Depth.Clone();
            Size = Config.Size.Clone();

            // if we are already configured, we probably shouldn't be here.
            if (GeneratedPlanetOreMappings.Count >= 1 || AssignedOres.Count >= 1) return;

            if (Depth.UseDetector) Depth.Max = (int)Session.OreDetectorRanges[Config.Depth.DetectorSize];

            // If there are no ore slots (ex. Overvent) then don't continue.
            if (!planetOreMappings.Any()) return;

            // Filter duplicate blue value bytes
            var cleanPlanetOreMappings = planetOreMappings.GroupBy(pom => pom.Value).Select(v => v.First()).ToList();

            // Filter reserved "static" color values from assignables
            var pomsWithoutStaticVoxels = cleanPlanetOreMappings
                .Where(v => !Session.Config.StaticVoxelMaterials.Contains(v.Type));

            AssignableOresBlueChannelValues.AddRange(pomsWithoutStaticVoxels.Select(c => c.Value)
                );

            OreSlotCount = AssignableOresBlueChannelValues.Count;
            LogVar("OreSlotCount", OreSlotCount);

            GenerateDefaultAssignments(cleanPlanetOreMappings.ToList());

            // Track the default ores of this planet so we can tell the player.
            foreach (var planetOreAssignment in AssignedOres)
            {
                MyDefaultOreAssignments.Add(planetOreAssignment.VoxelMaterialType);
            }

            GenerateMappings();

            LogEnd(LOG_CREATE_PLANET_CONFIG);
        }

        public void ReInit(Configuration config)
        {
            Log($"Assigning Config = config");
            Config = config;
            
            CleanUpRemovedOres();

            Log("MyPlanetConfiguration Depth.SetRelationship()");
            
            LogVar("World Depth Max",Config.Depth.Max);
            LogVar("My Depth.Max", Depth.Max);
            
            Depth.SetRelationship(Config.Depth);
            Log("MyPlanetConfiguration Size.SetRelationship()");
            Size.SetRelationship(Config.Size);

            var i = 1;
            foreach (var ore in AssignedOres)
            {
                Log($"Linking ore assignment {i}/{this.AssignedOres.Count} {ore.VoxelMinedOre} -> {ore.VoxelMaterialType} (Static? {ore.HasStaticVoxelMaterial}) ");
                i++;
                ore.ReInit(this);    
            }
        }

        public void GenerateDefaultAssignments(IReadOnlyCollection<MyPlanetOreMapping> planetOreMappings)
        {
            LogBegin(LOG_PLANET_DEFAULT_ASSIGNMENTS);


            LogBegin(LOG_PLANET_MASS_ASSIGNMENT);
            AssignedOres.AddRange(planetOreMappings
                .GroupBy(p => p.Type)
                .Select(x => new MyPlanetOreAssignment(
                        this,
                        x.Key,
                        x.Count(),
                        x.Average(s => (float)Math.Round(s.Start, 0)),
                        x.Average(d => (float)Math.Round(d.Depth, 0)),
                        x.First().TargetColor,
                        colorInfluence: x.Average(c => c.ColorInfluence),
                        blueValues: x.Select(v => v.Value).ToList()
                    )
                )
            );
            LogEnd(LOG_PLANET_MASS_ASSIGNMENT);

            StaticOres.AddRange(AssignedOres
                .Select(a => a)
                .Where(x => Session.Config.StaticVoxelMaterials
                    .Contains(x.VoxelMaterialType)
                ));

            foreach (var myPlanetOreAssignment in StaticOres)
            {
                Log("VoxelMaterial: " + myPlanetOreAssignment.VoxelMaterialType + ", Mined Ore: " +
                    myPlanetOreAssignment.VoxelMinedOre);
                AssignedOres.Remove(myPlanetOreAssignment);
            }

            // Get base planet stats
            MeanFrequency = (int)Math.Round(AssignedOres.Average(a => a.Count), 0);
            LargestOremappingCount = AssignedOres.Max(a => a.Count);
            MeanDepthStart = (int)Math.Round(AssignedOres.Average(o => o.OriginalMeanDepth), 0);
            MeanDepthSize = (int)Math.Round(AssignedOres.Average(o => o.OriginalMeanSize), 0);
            MeanRarity = (int)Math.Round((1 / (MeanFrequency / OreSlotCount)), 0);

            LogBegin(LOG_PLANET_MASS_INITIALIZE_RARITY);

            LogEnd(LOG_PLANET_MASS_INITIALIZE_RARITY);

            PlanetTargetColor = AssignedOres.FirstOrDefault().TargetColor;
            PlanetColorInfluence = AssignedOres.FirstOrDefault().ColorInfluence;

            LogEnd(LOG_PLANET_DEFAULT_ASSIGNMENTS);
        }

        /// <summary>
        /// Generates planet oremappings where:
        /// Each ore has at least one oremapping, regardless of rarity.
        /// Ores with a lower rarity score have a greater likelihood of getting an oremapping slot.
        /// Ores with a higher rarity score have a lesser likelihood of getting an oremapping slot.
        ///
        /// Note: VRage.Utils ShuffleList() implements Fisher-Yates.
        /// </summary>
        public void GenerateMappings()
        {
            LogBegin(LOG_PLANET_GENERATE_MAPPINGS);
            /*
             * Shuffle the blue channel values to break up vanilla clustering and prevent
             * clusters of rare ores in "easy-start" locations.
             */

            if (OreSlotCount < 1) return;

            // COPY THE LIST or else it gets popped to death in the raffle
            var tmpBlueChannelValues = new List<byte>(AssignableOresBlueChannelValues);
            tmpBlueChannelValues.Sort();

            // Filter out special things like Mars Ice so it retains a persistent color value
            // as the planet designer intended
            tmpBlueChannelValues = RemoveOreMappingColorValuesOfStaticVoxelMaterials(tmpBlueChannelValues);
            LogVar("tmpBlueChannelValues.Count", tmpBlueChannelValues.Count);

            // Reset each assigned ore's count value to 0; it will be increased later according to rarity.
            AssignedOres.ForEach(o => o.Count = 0);
            AssignedOres.ForEach(o => o.BlueValues = new List<byte>());

            var raffleList = new List<MyPlanetOreAssignment>();

            // Pull available ore slot count off tmpBlueChannelValues not PlanetConfig.OreSlots
            var numAvailableOreSlots = tmpBlueChannelValues.Count;

            LogBegin("GenerateMappings() Raffle prep");
            LogVar("numAvailableOreSlots", numAvailableOreSlots);

            while (raffleList.Count < numAvailableOreSlots)
            {
                LogVar("raffleList.Count", raffleList.Count);

                // Give each ore one ore slot
                foreach (var assignedOre in AssignedOres)
                {
                    LogVar("assignedOre.VoxelMinedOre", assignedOre.VoxelMinedOre);
                    assignedOre.Count++;
                }

                numAvailableOreSlots -= AssignedOres.Count;
                LogVar("numAvailableOreSlots", numAvailableOreSlots);

                // Add tickets to the raffle based on rarity
                foreach (var assignedOre in AssignedOres)
                {
                    if (assignedOre.HasStaticVoxelMaterial)
                    {
                        Log("Has static voxel, not raffling.");
                        continue;
                    }

                    LogVar("assignedOre.Rarity", assignedOre.Rarity);
                    var additionalTickets = (int)(OreSlotCount / assignedOre.Rarity);
                    LogVar("additionalTickets", additionalTickets);

                    for (var i = 0; i < additionalTickets; i++)
                    {
                        raffleList.Add(assignedOre);
                    }
                }
            }

            LogEnd("GenerateMappings() Raffle prep");

            raffleList.ShuffleList();

            // Raffle off remaining ore slots
            LogBegin("GenerateMappings() Raffle Raffle actual");
            for (var i = 0; i < numAvailableOreSlots; i++)
            {
                var selectedOre = raffleList.Pop();
                selectedOre.Count++;
                //selectedOre.BlueValues.Add(tmpBlueChannelValues.Pop());
            }

            LogEnd("GenerateMappings() Raffle Raffle actual");

            // Assign the blue values in one lump to keep clusters homogenized
            foreach (var assignedOre in AssignedOres)
            {
                LogVar("assignedOre.VoxelMinedOre", assignedOre.VoxelMinedOre);

                for (var i = 0; i < assignedOre.Count; i++)
                {
                    assignedOre.BlueValues.Add(tmpBlueChannelValues.Pop());
                }
            }

            // Refresh the largest oremapping count
            LargestOremappingCount = AssignedOres.Max(a => a.Count);

            // Collect the new planet ore mappings.
            GeneratedPlanetOreMappings = null;
            GeneratedPlanetOreMappings = new List<MyPlanetOreMapping>();

            // Get mappings for static ores
            foreach (var staticOre in StaticOres) GeneratedPlanetOreMappings.AddRange(staticOre.ToOreMappingMembers());

            // Get mappings for dynamic ores
            foreach (var assignedOre in AssignedOres)
            {
                //assignedOre.UpdateDepthBasis(MeanRarity);
                GeneratedPlanetOreMappings.AddRange(assignedOre.ToOreMappingMembers());
            }

            // Sort for more readable chat and config
            //GeneratedPlanetOreMappings.Sort((x,y) => Compare(x.Value.ToString(), y.Value.ToString()));
            GeneratedPlanetOreMappings.Sort((x, y) => x.Value.CompareTo(y.Value));

            LogEnd(LOG_PLANET_GENERATE_MAPPINGS);
        }

        public bool AddOreAssignment(string oreName, float rarity)
        {
            LogBegin("AddOreAssignment");

            // Do not touch planets with no ore slots
            if (OreSlotCount < 1) return false;

            // Not adding more ore assignments than we have ore slots.
            if (AssignedOres.Count + 1 > OreSlotCount) return false;

            // Not adding an ore we already have.
            if (AssignedOres.Any(a => a.VoxelMinedOre == oreName)) return false;

            var voxelMaterialType = Session.VoxelOres.GetVoxelMaterialByOre(oreName).Item2;

            LogVar("Got voxelMaterialType", voxelMaterialType);
            if (voxelMaterialType.Length < 1) return false;

            var count = (int)Math.Round(MeanFrequency, 0);
            var depthStart = (int)Math.Round(MeanDepthStart, 0);
            var depthSize = (int)Math.Round(MeanDepthSize, 0);

            var newOreAssignment = new MyPlanetOreAssignment(
                this,
                voxelMaterialType,
                count,
                depthStart,
                depthSize,
                PlanetTargetColor,
                PlanetColorInfluence
            );

            // Rarity given on argument
            if (rarity > 0) newOreAssignment.Rarity = rarity;

            AssignedOres.Add(newOreAssignment);

            return true;
        }

        public bool DelOreAssignment(MyPlanetOreAssignment oreAssignment)
        {
            // Do not touch planets with no ore slots
            if (OreSlotCount < 1) return false;

            if (IsLastAssignmentStone()) return false;

            if (AssignedOres.Count < 2) AddOreAssignment("Stone", 1);
            AssignedOres.Remove(oreAssignment);
            return true;
        }

        public bool DelOreAssignment(string oreName)
        {
            // Do not touch planets with no ore slots
            if (OreSlotCount < 1)
            {
                LogFail("Cannot modify ores on planets with no ore slots.");
                return false;
            }

            // Do not touch planets which appear to have been cleared
            if (IsLastAssignmentStone())
            {
                LogFail("Ore map is clear, everything is stone, I cannot remove anything else.");
                return false;
            }

            var voxelMaterial = Session.VoxelOres.GetVoxelMaterialByOre(oreName);

            if (!voxelMaterial.Item1)
            {
                LogFail($"GetVoxelMaterialByOre({oreName}) failed");
                return false;
            }

            LogVar("voxelMaterial.Item1", voxelMaterial.Item1.ToString());
            LogVar("voxelMaterial.Item2", voxelMaterial.Item2);

            var isOk = true;
            foreach (var assignment in new List<MyPlanetOreAssignment>(AssignedOres.Where(a =>
                         voxelMaterial.Item2 == a.VoxelMaterialType)))
            {
                LogVar("Trying to remove: ", assignment.VoxelMaterialType);
                LogVar("Which is Mined Ore: ", assignment.VoxelMinedOre);
                isOk &= DelOreAssignment(assignment);
            }

            return isOk;
        }

        public bool ClearOreAssignments()
        {
            // Do not touch planets with no ore slots
            if (OreSlotCount < 1) return false;

            if (IsLastAssignmentStone()) return false;

            Log("IsLastAssignmentStone = false");
            AssignedOres.RemoveAll(o => !o.HasStaticVoxelMaterial);
            AddOreAssignment("Stone", MeanRarity);
            return true;
        }

        private bool IsLastAssignmentStone()
        {
            var nonStaticVoxelAssignmentCount = AssignedOres.Count(o => !o.HasStaticVoxelMaterial);
            var stoneVoxelAssignmentCount = AssignedOres.Count(a => a.VoxelMinedOre == "Stone");

            LogVar("nonStaticVoxelAssignmentCount", nonStaticVoxelAssignmentCount);
            LogVar("stoneVoxelAssignmentCount", stoneVoxelAssignmentCount);
            LogVar("nonStaticVoxelAssignmentCount < 2", nonStaticVoxelAssignmentCount < 2);
            LogVar("stoneVoxelAssignmentCount > 0", stoneVoxelAssignmentCount > 0);
            LogVar("IsLastAssignmentStone()", nonStaticVoxelAssignmentCount < 2 && stoneVoxelAssignmentCount > 0);

            return nonStaticVoxelAssignmentCount < 2 && stoneVoxelAssignmentCount > 0;
        }

        /*public void UpdateUseOreDetector(bool useOreDetector)
        {
            foreach (var oreAssignment in AssignedOres)
            {
                oreAssignment.Depth.UseDetector = useOreDetector;
            }
        }*/

        private List<byte> RemoveOreMappingColorValuesOfStaticVoxelMaterials(List<byte> blueChannelValues)
        {
            Log("RemoveOreMappingColorValuesOfStaticVoxelMaterials()");
            return blueChannelValues.Except(StaticOres.SelectMany(s => s.BlueValues)).ToList();
        }

        public MyPlanetOreAssignment Get(string oreName)
        {
            return AssignedOres.Find(a => a.VoxelMinedOre == oreName);
        }

        public List<MyPlanetOreAssignment> GetNonStaticVoxelOreAssignments()
        {
            /*
             * Filters out static voxel materials so we don't mess with the weird
             * stuff some Planets do with the oremappings, for example Mars's "Ice" voxelmaterial
             */
            return AssignedOres
                .Select(o => o)
                .Where(o => !o.HasStaticVoxelMaterial)
                .ToList();
        }

        public List<string> GetAssignedOres()
        {
            return GetNonStaticVoxelOreAssignments()
                .Select(o => o.VoxelMinedOre)
                .ToList();
        }

        public Dictionary<string, float> GetAssignedOresWithRarity()
        {
            var ores = GetNonStaticVoxelOreAssignments();
            return ores.ToDictionary(ore => ore.VoxelMinedOre, ore => ore.Rarity);
        }

        public List<string> GetPrettyAssignedOresWithRarity()
        {
            var format = "{0} ({1})";
            return GetAssignedOresWithRarity().Select(ore => Format(format, ore.Key, ore.Value)).ToList();
        }

        public List<string> GetUnassignedOres()
        {
            return Session.VoxelOres.GetUsableOres()
                .Where(o => AssignedOres.All(a => a.VoxelMinedOre != o)).ToList();
        }

        public string DumpOreMappings(MyPlanetOreAssignment oreAssignment = null)
        {
            var menuText = new StringBuilder();
            var mappings = new List<MyPlanetOreMapping>();

            const string template = "Value={0} Type={1} Start={2} Depth={3}";

            switch (oreAssignment != null)
            {
                case true:
                    mappings.AddRange(GeneratedPlanetOreMappings.Where(o => o.Type == oreAssignment.VoxelMaterialType));
                    break;
                case false:
                    mappings.AddRange(GeneratedPlanetOreMappings);
                    break;
            }

            foreach (var mapping in mappings)
            {
                menuText.AppendLine(Format(template, mapping.Value, mapping.Type, mapping.Start, mapping.Depth));
            }

            return menuText.ToString();
        }

        //todo: need a way to raffle off the freed 
        public void CleanUpRemovedOres()
        {
            LogBegin("CleanUpRemovedOres()");
            var assignments = new List<MyPlanetOreAssignment>();

            assignments.AddRange(AssignedOres);

            foreach (var ore in assignments)
            {
                if (Session.VoxelOres.ContainsVoxelMaterial(ore.VoxelMaterialType)) continue;
                DelOreAssignment(ore);
                AddOreAssignment("Stone", MeanRarity);
            }
            LogEnd("CleanUpRemovedOres()");
        }

        #region Logging

        private static void LogBegin<T>(T message, bool isServer = false) =>
            Helpers.LogBegin(LOG_PREFIX_PLANET, message, isServer);

        private static void LogEnd<T>(T message, bool isServer = false) =>
            Helpers.LogEnd(LOG_PREFIX_PLANET, message, isServer);

        private static void LogTry<T>(T message, bool isServer = false) =>
            Helpers.LogTry(LOG_PREFIX_PLANET, message, isServer);

        private static void LogFail<T>(T message, bool isServer = false) =>
            Helpers.LogFail(LOG_PREFIX_PLANET, message, isServer);

        private static void LogWarn<T>(T message, bool isServer = false) =>
            Helpers.LogWarn(LOG_PREFIX_PLANET, message, isServer);

        private static void LogVar<T>(string name, T varT, bool isServer = false) =>
            Helpers.LogVar(LOG_PREFIX_PLANET, name, varT, isServer);

        private static void LogFlag<T>(T message, string flag, bool showHeader = false, bool isServer = false) =>
            Helpers.LogFlag(LOG_PREFIX_PLANET, message, flag, showHeader, isServer);

        private static void Log<T>(T message, bool showHeader = false, bool isServer = false) =>
            Helpers.Log(LOG_PREFIX_PLANET, message, showHeader, isServer);

        #endregion
    }

    #endregion

    #region MyPlanetOreAssignment

    [Serializable]
    public class MyPlanetOreAssignment
    {
        #region Not Chat Configurable

        private static readonly ConfigurableOres Session = ConfigurableOres.Instance;

        //private static readonly Configuration Config = Session.Config;
        private MyPlanetConfiguration MyPlanet;

        public int Count;

        // Original values from initial planet oremapping slot
        public float OriginalMeanDepth;
        public float OriginalMeanSize;

        public string VoxelMaterialType;
        public string VoxelMinedOre;
        public string TargetColor;
        public float ColorInfluence;

        // Blue channel values in the MyPlanetOreMapping.
        public List<byte> BlueValues = new List<byte>();

        // Should be true when this Ore Assignment has a voxel material we don't want to mess with
        // so it will not be involved in the standard oremapping generation process.
        public bool HasStaticVoxelMaterial = false;

        #endregion

        #region Chat Configurable

        public MyDepthSettings Depth;
        public MySizeSettings Size;

        //public float DepthStart;
        //public float DepthSize;

        // Relative rarity, loosely enforced
        public float Rarity;

        #endregion

        // Parameterless Constructor for XML serializer
        public MyPlanetOreAssignment()
        {
        }

        public MyPlanetOreAssignment(
            MyPlanetConfiguration planet,
            string voxelMaterialType,
            int count,
            float depthStart = DEFAULT_OREMAP_DEPTH_START,
            float depthSize = DEFAULT_OREMAP_DEPTH_SIZE,
            string targetColor = DEFAULT_OREMAP_TARGETCOLOR,
            float colorInfluence = DEFAULT_OREMAP_COLORINFLUENCE,
            List<byte> blueValues = null
        )
        {
            // Constructor arguments
            MyPlanet = planet;

            VoxelMaterialType = voxelMaterialType;
            if (Session.Config.StaticVoxelMaterials.Contains(VoxelMaterialType)) HasStaticVoxelMaterial = true;
            Log($"MyPlanetOreAssignment() constructor looking for Session.VoxelMaterialToOre[{voxelMaterialType}]");
            VoxelMinedOre = Session.VoxelOres.GetOreByVoxelMaterial(voxelMaterialType).Item2;

            // Initial values from default planet oremappings
            Count = count;

            Depth = MyPlanet.Depth.Clone();
            Size = MyPlanet.Size.Clone();

            OriginalMeanDepth = (float)Math.Round(depthStart, 0);
            OriginalMeanSize = (float)Math.Round(depthSize, 0);

            TargetColor = targetColor;
            ColorInfluence = colorInfluence;

            if (blueValues != null) BlueValues = blueValues;

            Rarity = (int)Math.Round((float)MyPlanet.OreSlotCount / Count, 0);

            Log(Format(LOG_ORE_ASSIGNMENT_INITIALIZE, VoxelMaterialType, VoxelMinedOre, Count, OriginalMeanDepth,
                OriginalMeanSize));
        }

        public void ReInit(MyPlanetConfiguration myPlanet)
        {
            Log("SetRelationship()");
            MyPlanet = myPlanet;
            Log($"MyPlanet Depth.SetRelationship()");
            Depth.SetRelationship(MyPlanet.Depth);
            Log($"MyPlanet Depth.SetRelationship()");
            Size.SetRelationship(MyPlanet.Size);
        }

        private float GenDepositDepth(int slotNumber)
        {
            /*if (Depth.UseCustomDepth)
            {
                return Depth.CustomDepth * RandomFuzz;	
            }*/

            var depthRange = Depth.Max - Depth.Min;

            //LogVar("OreDepthMax", Depth.Max);
            //LogVar("OreDepthMin", Depth.Min);

            if (Depth.UseDetector)
            {
                depthRange = Depth.DetectorFactor * Session.OreDetectorRanges[Depth.DetectorSize];
            }

            //LogVar("depthRange", depthRange);

            //LogVar("Count", Count);

            //LogVar("MyPlanet.LargestOremappingCount", MyPlanet.LargestOremappingCount);
            var maxDepthRatio = 1 - (Count - slotNumber) * 1f / MyPlanet.LargestOremappingCount;
            //LogVar("maxDepthRatio", maxDepthRatio);

            //LogVar("Depth.Curve",Depth.Curve);
            var maxDepthCurve = (float)Math.Pow(maxDepthRatio, Depth.Curve);
            //LogVar("maxDepthCurve", maxDepthCurve);

            var depthBase = (depthRange * maxDepthCurve);
            //LogVar("depthBase", depthBase);

            var depthFuzz = depthBase * NegaFuzz * Depth.Fuzz;
            //LogVar("depthFuzz", depthFuzz);

            var depth = (float)Math.Round(depthBase - depthFuzz, 0);
            //LogVar("depth", depth);

            if (Depth.UseDetector)
            {
                return depth + DEFAULT_ORE_DEPTH_MIN;
            }

            if (depth > Depth.Max) depth = Depth.Max;

            return depth + Depth.Min;
        }

        private float GenDepositSize(int slotNumber)
        {
            /*switch (Size.UseCustomSize)
            {
                case true:
                {
                    var sizeBase = Size.CustomSize;
                    var sizeFuzz = sizeBase * RandomFuzz * Size.Fuzz;
                    var size = (float) Math.Round(sizeBase - sizeFuzz, 0);
                    return size;
                }
                case false:
                {*/
            // Grabbing a new deposit depth here is both easier and adds variance to deposit sizes.
            var sizeBase = GenDepositDepth(slotNumber) * Size.Factor;
            var sizeFuzz = sizeBase * RandomFuzz * Size.Fuzz;
            var size = (float)Math.Round(sizeBase - sizeFuzz, 0);

            if (size > Size.Max) size = Size.Max;
            if (size < Size.Min) size = Size.Min;
            return size;
            /*}
            default:
                // Should never get here.
                return 0;
        }*/
        }

        public IEnumerable<MyPlanetOreMapping> ToOreMappingMembers()
        {
            /*
             * MyPlanetOreMappings reference.
             * Note: Values cannot be assigned on the constructor
             *
             * <Ore Value="24" Type="Nickel_01" Start="10" Depth="4" TargetColor="#616c83" ColorInfluence="15"/>
             * 
             * Value refers to blue channel value on material texture.
             * Type refers to name of a voxel material from Content\Data\VoxelMaterials.sbc.
             * Start is depth at which the ore deposit starts.
             * Depth is how deep under ground it goes.
             * TargetColor is color that will be used to color surface above the ore.
             * ColorInfluence sets strength of coloring.
             * 
             * source: https://www.medievalengineerswiki.com/index.php@title=Keen:Planet_Modding_-_Full_Guide.html#Ores_definitions
             */

            var tmpOreMappingsMembers = new List<MyPlanetOreMapping>();
            var tmpBlueValues = new List<byte>(BlueValues);

            for (var slotNumber = 0; slotNumber < Count; slotNumber++)
            {
                var tmpBlue = tmpBlueValues.Pop();

                var myPlanetOreMapping = new MyPlanetOreMapping
                {
                    Value = tmpBlue,
                    Type = VoxelMaterialType,
                    TargetColor = TargetColor,
                    ColorInfluence = ColorInfluence
                };

                switch (HasStaticVoxelMaterial)
                {
                    case true:
                    {
                        myPlanetOreMapping.Start = OriginalMeanDepth;
                        myPlanetOreMapping.Depth = OriginalMeanSize;
                        break;
                    }
                    case false:
                    {
                        switch (Depth.UseProgressive)
                        {
                            case true:
                            {
                                myPlanetOreMapping.Start = GenDepositDepth(slotNumber);
                                myPlanetOreMapping.Depth = GenDepositSize(slotNumber);
                                break;
                            }
                            case false:
                            {
                                myPlanetOreMapping.Start = GenDepositDepth(1);
                                myPlanetOreMapping.Depth = GenDepositSize(1);
                                break;
                            }
                        }

                        break;
                    }
                }

                Log(Format(LOG_ORE_ASSIGNMENT_TO_OREMAPPING, myPlanetOreMapping.Value, myPlanetOreMapping.Type,
                    myPlanetOreMapping.Start, myPlanetOreMapping.Depth, myPlanetOreMapping.TargetColor,
                    myPlanetOreMapping.ColorInfluence));

                tmpOreMappingsMembers.Add(myPlanetOreMapping);
            }

            // Sort for more readable chat and config
            //tmpOreMappingsMembers.Sort((x,y) => x.Value.CompareTo(y.Value));

            return tmpOreMappingsMembers;
        }

        public Dictionary<string, string> GetOreVoxelDict()
        {
            return new Dictionary<string, string> { { VoxelMinedOre, VoxelMaterialType } };
        }

        #region Menu Strings Methods

        private static void Error(string topic) => MenuStrings.Error(topic);
        private static void Warning(string topic) => MenuStrings.Warning(topic);
        private static void Item(string topic) => MenuStrings.Item(topic);
        private static void Hint(string topic) => MenuStrings.Hint(topic);
        private static void Help(string topic) => MenuStrings.Help(topic);
        private static void Result(string topic) => MenuStrings.Result(topic);

        #endregion

        #region Logging

        private void LogBegin<T>(T message, bool isServer = false) =>
            Helpers.LogBegin(LOG_PREFIX_ORE_ASSIGNMENT, message, isServer);

        private void LogComplete<T>(T message, bool isServer = false) =>
            Helpers.LogEnd(LOG_PREFIX_ORE_ASSIGNMENT, message, isServer);

        private void LogTry<T>(T message, bool isServer = false) =>
            Helpers.LogTry(LOG_PREFIX_ORE_ASSIGNMENT, message, isServer);

        private void LogFail<T>(T message, bool isServer = false) =>
            Helpers.LogFail(LOG_PREFIX_ORE_ASSIGNMENT, message, isServer);

        private void LogWarn<T>(T message, bool isServer = false) =>
            Helpers.LogWarn(LOG_PREFIX_ORE_ASSIGNMENT, message, isServer);

        private void LogVar<T>(string name, T varT, bool isServer = false) =>
            Helpers.LogVar(LOG_PREFIX_ORE_ASSIGNMENT, name, varT, isServer);

        private void LogFlag<T>(T message, string flag, bool showHeader = false, bool isServer = false) =>
            Helpers.LogFlag(LOG_PREFIX_ORE_ASSIGNMENT, message, flag, showHeader, isServer);

        private void Log<T>(T message, bool showHeader = false, bool isServer = false) =>
            Helpers.Log(LOG_PREFIX_ORE_ASSIGNMENT, message, showHeader, isServer);

        #endregion
    }

    #endregion

    #region MyPlanetConfigurationCollection

    [Serializable]
    public class MyPlanetConfigurationCollection
    {
        public List<MyPlanetConfigurationCollectionMember>
            MemberPlanets = new List<MyPlanetConfigurationCollectionMember>();

        public void Add(MyPlanetConfigurationCollectionMember myPlanet)
        {
            MemberPlanets.Add(myPlanet);
        }

        public void Remove(MyPlanetConfigurationCollectionMember myPlanet)
        {
            MemberPlanets.Remove(myPlanet);
        }

        public void Remove(string planetName)
        {
            MemberPlanets.RemoveAll(s => s.Name == planetName);
        }

        public List<string> GetNames()
        {
            return MemberPlanets.Select(p => p.Name).ToList();
        }

        public int Count()
        {
            return MemberPlanets.Count;
        }

        public bool Contains(string planetName)
        {
            return MemberPlanets.Any(s => s.Name == planetName);
        }

        public MyPlanetConfiguration Get(string name)
        {
            return MemberPlanets
                .Where(s => s.Name == name)
                .Select(p => p.PlanetConfig)
                .First();
        }

        public void GenerateMappings()
        {
            foreach (var planet in MemberPlanets)
            {
                Log(Format("GenerateMappings: Planet={0}", planet.Name));
                planet.PlanetConfig.GenerateMappings();
            }
        }

        public string DumpOreMappings()
        {
            var mappings = new StringBuilder();

            foreach (var planet in MemberPlanets)
            {
                mappings.AppendLine(CHAT_LINE_THICK);
                mappings.AppendLine(planet.Name);
                mappings.AppendLine(CHAT_LINE_THIN);
                mappings.Append(planet.PlanetConfig.DumpOreMappings());
                mappings.AppendLine();
            }

            return mappings.ToString();
        }


        #region Menu Strings Methods

        private static void Error(string topic) => MenuStrings.Error(topic);
        private static void Warning(string topic) => MenuStrings.Warning(topic);
        private static void Item(string topic) => MenuStrings.Item(topic);
        private static void Hint(string topic) => MenuStrings.Hint(topic);
        private static void Help(string topic) => MenuStrings.Help(topic);
        private static void Result(string topic) => MenuStrings.Result(topic);

        #endregion

        #region Logging

        private void LogVar<T>(string name, T varT, bool isServer = false) =>
            Helpers.LogVar("PLANET_SETTINGS", name, varT, isServer);

        private void Log<T>(T message, bool showHeader = false, bool isServer = false) =>
            Helpers.Log("PLANET_SETTINGS", message, showHeader, isServer);

        #endregion
    }

    #endregion

    #region MyPlanetConfigurationCollectionMember

    [Serializable]
    public class MyPlanetConfigurationCollectionMember
    {
        public string Name;
        public MyPlanetConfiguration PlanetConfig;

        public MyPlanetConfigurationCollectionMember(string name, MyPlanetConfiguration planetConfig)
        {
            Name = name;
            PlanetConfig = planetConfig;
        }

        // Parameterless Constructor for XML serializer
        public MyPlanetConfigurationCollectionMember()
        {
        }
    }

    #endregion

    #region MyVoxelOre

    public class MyVoxelOre
    {
        public string Ore { get; }
        public string VoxelMaterial { get; }
        public bool IsStatic { get; }

        public MyVoxelOre(string voxelMaterial, string ore, bool isStatic)
        {
            VoxelMaterial = voxelMaterial;
            Ore = ore;
            IsStatic = isStatic;
        }
    }

    #endregion

    #region MyVoxelOreCollection

    // Next release I want to switch the dict to an actual class that manages and filters all the voxel material types.
    // Doing it with the lookup dictionary has been annoying.
    public class MyVoxelOreCollection
    {
        private List<MyVoxelOre> _myVoxelOres = new List<MyVoxelOre>();

        public void Add(string voxelMaterial, string ore, bool isStatic)
        {
            _myVoxelOres.Add(new MyVoxelOre(voxelMaterial, ore, isStatic));
        }

        public MyTuple<bool, string> GetOreByVoxelMaterial(string voxelMaterial)
        {
            if (!_myVoxelOres.Any(o => o.VoxelMaterial == voxelMaterial)) return new MyTuple<bool, string>(false, null);

            var matches = _myVoxelOres.First(o => o.VoxelMaterial == voxelMaterial);
            return new MyTuple<bool, string>(true, matches.Ore);
        }

        public MyTuple<bool, string> GetVoxelMaterialByOre(string ore)
        {
            LogBegin($"GetVoxelMaterialByOre({ore})");
            if (_myVoxelOres.All(o => o.Ore != ore))
            {
                LogFail($"GetVoxelMaterialByOre({ore})");
                return new MyTuple<bool, string>(false, null);
            }

            // Find the first match.
            Log($"_myVoxelOres.FirstOrDefault(vo => vo.Ore == {ore} && vo.IsStatic == false)");
            var matches = _myVoxelOres.FirstOrDefault(vo => vo.Ore == ore && vo.IsStatic == false);

            // if the above search returns a null then idkwtf, but return false here.
            // todo: this section is probably duct-tape on a bug.
            if (matches == null)
            {
                Log("matches is null, returning false");
                return new MyTuple<bool, string>(false, null);
            } 
            
            // Return the match we found
            LogVar(ore, matches.VoxelMaterial);
            LogComplete($"GetVoxelMaterialByOre({ore})");
            return new MyTuple<bool, string>(true, matches.VoxelMaterial);
        }

        public bool IsStatic(string voxelMaterial)
        {
            return GetMyVoxelOreByVoxelMaterial(voxelMaterial).IsStatic;
        }

        public bool ContainsVoxelMaterial(string voxelMaterial)
        {
            return GetOreByVoxelMaterial(voxelMaterial).Item1;
        }

        public bool ContainsOre(string ore)
        {
			LogBegin($"ContainsOre({ore})");
            var res = GetVoxelMaterialByOre(ore);
            LogVar($"ContainsOre({ore}) res.Item1", res.Item1.ToString());
            LogComplete($"ContainsOre({ore})");
            return res.Item1;
        }

        public MyVoxelOre GetMyVoxelOreByVoxelMaterial(string voxelMaterial)
        {
            return _myVoxelOres.FirstOrDefault(vo => vo.VoxelMaterial == voxelMaterial);
        }

        public MyVoxelOre GetMyVoxelOreByOre(string ore)
        {
            return _myVoxelOres.FirstOrDefault(vo => vo.Ore == ore);
        }

        public List<string> GetStaticVoxelMaterials()
        {
            return _myVoxelOres.Where(vo => vo.IsStatic).Select(vo => vo.VoxelMaterial).ToList();
        }

        public List<string> GetUsableVoxelMaterials()
        {
            return _myVoxelOres.Where(vo => !vo.IsStatic).Select(vo => vo.VoxelMaterial).ToList();
        }

        public List<string> GetUsableOres()
        {
            return _myVoxelOres.Where(vo => !vo.IsStatic).Select(vo => vo.Ore).ToList();
        }

        public List<string> DebugMap()
        {
            List<string> dump = new List<string>();

            foreach (var vo in _myVoxelOres)
            {
                dump.Add($"[static={vo.IsStatic.ToString()}] {vo.VoxelMaterial} -> {vo.Ore}");
            }

            return dump;
        }

        #region Logging

        private void LogBegin<T>(T message, bool isServer = false) =>
            Helpers.LogBegin(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, isServer);

        private void LogComplete<T>(T message, bool isServer = false) =>
            Helpers.LogEnd(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, isServer);

        private void LogTry<T>(T message, bool isServer = false) =>
            Helpers.LogTry(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, isServer);

        private void LogFail<T>(T message, bool isServer = false) =>
            Helpers.LogFail(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, isServer);

        private void LogWarn<T>(T message, bool isServer = false) =>
            Helpers.LogWarn(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, isServer);

        private void LogVar<T>(string name, T varT, bool isServer = false) =>
            Helpers.LogVar(LOG_PREFIX_VOXEL_ORE_COLLECTION, name, varT, isServer);

        private void LogFlag<T>(T message, string flag, bool showHeader = false, bool isServer = false) =>
            Helpers.LogFlag(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, flag, showHeader, isServer);

        private void Log<T>(T message, bool showHeader = false, bool isServer = false) =>
            Helpers.Log(LOG_PREFIX_VOXEL_ORE_COLLECTION, message, showHeader, isServer);

        #endregion
    }

    #endregion
}