﻿/*
MIT License

Copyright(c) 2019 Mitchel Thompson
www.angryarugula.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Arugula.Math
{
    /// <summary>
    /// A container for a predefined configuration of dice to use for a Roll
    /// </summary>
    [CreateAssetMenu(fileName = "Dice", menuName = "Arugula/Dice", order = 0)]
    public class Dice : ScriptableObject
    {

        #region Useful functions
        /// <summary>
        /// Get a random integer.
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        public static int GetInt(int min, int max) { return Random.Range(min, max); }

        /// <summary>
        /// Roll X Y-sided dice.  Return sum.
        /// </summary>
        /// <param name="sides"></param>
        /// <param name="quantity"></param>
        /// <returns></returns>
        public static int Roll(int sides, int quantity = 1)
        {
            int val = 0;
            for (int i = 0; i < quantity; i++)
                val += GetInt(1, sides + 1);

            return val;
        }

        /// <summary>
        /// Roll X Y-sided dice Z times.  Return the lowest roll.
        /// </summary>
        /// <param name="sides"></param>
        /// <param name="quantity"></param>
        /// <param name="iterations"></param>
        /// <returns></returns>
        public static int RollMax(int sides, int quantity, int iterations)
        {
            int[] values = new int[iterations];
            for (int i = 0; i < iterations; i++)
            {
                values[i] = Roll(sides, quantity);
            }

            return values.Max();
        }

        /// <summary>
        /// Roll X Y-sided dice Z times.  Return the highest roll.
        /// </summary>
        /// <param name="sides"></param>
        /// <param name="quantity"></param>
        /// <param name="iterations"></param>
        /// <returns></returns>
        public static int RollMin(int sides, int quantity, int iterations)
        {
            int[] values = new int[iterations];
            for (int i = 0; i < iterations; i++)
            {
                values[i] = Roll(sides, quantity);
            }

            return values.Min();
        }

        /// <summary>
        /// Roll X Y-sided dice Z times. Return the sum after dropping the lowest roll.
        /// </summary>
        /// <param name="sides"></param>
        /// <param name="quantity"></param>
        /// <param name="iterations"></param>
        /// <param name="reroll"></param>
        /// <returns></returns>
        public static int RollDropLowest(int sides, int quantity, int iterations, bool reroll = false)
        {
            int[] values = new int[iterations];
            for (int i = 0; i < iterations; i++)
            {
                values[i] = Roll(sides, quantity);
            }

            if (!reroll)
                return values.Sum() - values.Min();
            else
            {

                int min = values.Min();
                for (int i = 0; i < values.Length; i++)
                {
                    if (values[i] == min)
                    {
                        values[i] = Roll(sides, quantity);
                        break;
                    }
                }

                return values.Sum() - values.Min();
            }

        }

        /// <summary>
        /// Roll X Y-sided dice Z times. Return the sum after dropping the highest roll.
        /// </summary>
        /// <param name="sides"></param>
        /// <param name="quantity"></param>
        /// <param name="iterations"></param>
        /// <param name="reroll"></param>
        /// <returns></returns>
        public static int RollDropHighest(int sides, int quantity, int iterations, bool reroll = false)
        {
            int[] values = new int[iterations];
            for (int i = 0; i < iterations; i++)
            {
                values[i] = Roll(sides, quantity);
            }

            if (!reroll)
                return values.Sum() - values.Max();
            else
            {
                int max = values.Max();
                for (int i = 0; i < values.Length; i++)
                {
                    if (values[i] == max)
                    {
                        values[i] = Roll(sides, quantity);
                        break;
                    }
                }

                return values.Sum() - values.Max();
            }

        }

        /// <summary>
        /// Roll the dice!
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="weightedValues"></param>
        /// <returns>Returns a value from an array of weighted values.</returns>
        public static T Roll<T>(WeightedValue<T>[] weightedValues)
        {
            int sum = weightedValues.Sum(v => !v.ignore ? v.weight : 0);
            if (sum == 0)
                throw new System.Exception("No weights!");

            int x = GetInt(0, sum);
            int arrayIndex;
            return GetWeightedValue(weightedValues, x, out arrayIndex).value;
        }

        /// <summary>
        /// Roll the dice!
        /// Also outputs the actual array index of the result (a bit expensively).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="weightedValues"></param>
        /// <param name="arrayIndex"></param>
        /// <returns></returns>
        public static T Roll<T>(WeightedValue<T>[] weightedValues, out int arrayIndex)
        {

            int sum = weightedValues.Sum(v => !v.ignore ? v.weight : 0);
            if (sum == 0)
                throw new System.Exception("No weights!");

            int x = GetInt(0, sum);

            WeightedValue<T> weightedValue = GetWeightedValue(weightedValues, x, out arrayIndex);

            return weightedValue.value;
        }

        /// <summary>
        /// Simple % chance
        /// </summary>
        /// <param name="chance">Expects 0-100%</param>
        /// <returns></returns>
        public static bool Critical(int chance)
        {
            return GetInt(0, 100) < chance;
        }

        private static WeightedValue<T> GetWeightedValue<T>(WeightedValue<T>[] weightedValues, int x, out int arrayIndex)
        {
            if (x < 0 || x >= weightedValues.Sum(v => !v.ignore ? v.weight : 0))
                throw new System.Exception();

            int cumulativeWeight = 0;
            arrayIndex = 0;
            foreach (var v in weightedValues)
            {
                if (v.ignore)
                {
                    arrayIndex++;
                    continue;
                }

                cumulativeWeight += v.weight;
                if (x < cumulativeWeight)
                    return v;

                arrayIndex++;
            }

            //should NEVER get here
            throw new System.Exception();
        }
        #endregion

        /// <summary>
        /// Weighted Value Base
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class WeightedValue<T>
        {
            public T value;
            [Range(0, 100)]
            public int weight;
            public bool ignore = false;
        }

        //specialized classes for serializing common Unity types
        [System.Serializable]
        public class WeightedString : WeightedValue<string> { }
        [System.Serializable]
        public class WeightedBool : WeightedValue<bool> { }
        [System.Serializable]
        public class WeightedInt : WeightedValue<int> { }
        [System.Serializable]
        public class WeightedFloat : WeightedValue<float> { }
        [System.Serializable]
        public class WeightedGameObject : WeightedValue<GameObject> { }
        [System.Serializable]
        public class WeightedObject : WeightedValue<Object> { }
        [System.Serializable]
        public class WeightedVector2 : WeightedValue<Vector2> { }
        [System.Serializable]
        public class WeightedVector3 : WeightedValue<Vector3> { }
        [System.Serializable]
        public class WeightedQuaternion : WeightedValue<Quaternion> { }
        [System.Serializable]
        public class WeightedTransform : WeightedValue<Transform> { }
        [System.Serializable]
        public class WeightedSprite : WeightedValue<Sprite> { }
        [System.Serializable]
        public class WeightedMaterial : WeightedValue<Material> { }
        [System.Serializable]
        public class WeightedMesh : WeightedValue<Mesh> { }
        [System.Serializable]
        public class WeightedColor : WeightedValue<Color> { }
        /// <summary>
        /// Method used to calculate a roll
        /// </summary>
        public enum Mode
        {
            /// <summary>
            /// Roll X Y-sided dice.  Return sum.
            /// </summary>
            Simple,
            /// <summary>
            /// Roll X Y-sided dice Z times.  Return the lowest roll.
            /// </summary>
            Min,
            /// <summary>
            /// Roll X Y-sided dice Z times.  Return the highest roll.
            /// </summary>
            Max,
            /// <summary>
            /// Roll X Y-sided dice Z times.  Return the sum after dropping the lowest roll.
            /// </summary>
            DropLowest,
            /// <summary>
            /// Roll X Y-sided dice Z times.  Return the sum after dropping the highest roll.
            /// </summary>
            DropHighest,
            /// <summary>
            /// Returns a value from an array of weighted values.
            /// </summary>
            Weighted
        }

        /// <summary>
        /// Math used to apply Critical Bonus
        /// </summary>
        public enum CriticalMode { Add, Multiply }

        /// <summary>
        /// What kind of roll to do
        /// </summary>
        public Mode mode;
        /// <summary>
        /// How many sides on a die
        /// </summary>
        [Range(1, 20)]
        public int sides = 6;
        /// <summary>
        /// How many dice per roll
        /// </summary>
        [Range(1, 20)]
        public int quantity = 2;
        /// <summary>
        /// How many times to perform the roll with quantity dice
        /// </summary>
        [Range(1, 20)]
        public int iterations = 10;
        /// <summary>
        /// Reroll the lowest or highest value depending on mode
        /// </summary>
        public bool rerollOutlier;

        /// <summary>
        /// Array of weighted integers to potentially return when mode is set to Weighted
        /// </summary>
        public WeightedInt[] weightedIntegers;

        /// <summary>
        /// % Chance to add a critical bonus
        /// </summary>
        [Range(0, 100)]
        public int criticalChance = 0;
        /// <summary>
        /// Critical bonus value
        /// </summary>
        public float criticalBonus = 10;
        /// <summary>
        /// How Critical Bonus is applied
        /// </summary>
        public CriticalMode criticalMode;
        /// <summary>
        /// Base value to add to the result of any roll
        /// </summary>
        public int baseValue = 0;
        [TextArea]
        public string description = "";

        /// <summary>
        /// Roll the dice!
        /// </summary>
        /// <returns></returns>
        public int Roll()
        {
            bool crit = false;
            return Roll(baseValue, out crit);
        }

        /// <summary>
        /// Roll the dice!
        /// </summary>
        /// <param name="critical">True when a Critical chance occurred</param>
        /// <returns></returns>
        public int Roll(out bool critical)
        {
            return Roll(baseValue, out critical);
        }

        /// <summary>
        /// Roll the dice!
        /// </summary>
        /// <param name="baseValue">Overrides the baseValue parameter of the Dice object.</param>
        /// <param name="critical">True when a Critical chance occurred</param>
        /// <returns></returns>
        public int Roll(int baseValue, out bool critical)
        {
            int val = 0;

            switch (mode)
            {
                case Mode.Simple:
                    val = Roll(sides, quantity);
                    break;
                case Mode.Min:
                    val = RollMin(sides, quantity, iterations);
                    break;
                case Mode.Max:
                    val = RollMax(sides, quantity, iterations);
                    break;
                case Mode.DropLowest:
                    val = RollDropLowest(sides, quantity, iterations, rerollOutlier);
                    break;
                case Mode.DropHighest:
                    val = RollDropHighest(sides, quantity, iterations, rerollOutlier);
                    break;
                case Mode.Weighted:
                    val = Roll(weightedIntegers);
                    break;
            }

            val += baseValue;

            critical = false;

            if (criticalChance > 0)
            {
                if (Critical(criticalChance))
                {
                    switch (criticalMode)
                    {
                        case CriticalMode.Add:
                            val = Mathf.RoundToInt(val + criticalBonus);
                            break;
                        case CriticalMode.Multiply:
                            val = Mathf.RoundToInt(val * criticalBonus);
                            break;
                    }

                    critical = true;
                }
            }

            return val;
        }
    }

}
