using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Diagnostics.CodeAnalysis;

namespace Raytheon.Units
{
    [Serializable]
    [DataContract]
    public class Temperature
    {
        public enum Unit
        {
            DegreesC,
            DegreesF,
            DegreesK
        }

        public static IDictionary<Unit, List<string>> UnitAbbreviations = new Dictionary<Unit, List<string>>()
        {
            {Unit.DegreesC, new List<string>(){ "C" } },
            {Unit.DegreesF, new List<string>(){ "F" } },
            {Unit.DegreesK, new List<string>(){ "K" } },
        };

        private const Unit DefaultUnits = Unit.DegreesC;
        private const double CdegreesPerFdegree = 5.0 / 9.0;
        private const double CtoF_Offset = 32.0;
        private const double KtoC_Offset = 273.15;
        
        [DataMember(Name = "Temperature", IsRequired = true)]
        private double _temperature; // degrees C

        // map: unit => abbreviation, conversion method and property
        private static IDictionary<Unit, UnitInfo<Temperature>> _unitInfo = new Dictionary<Unit, UnitInfo<Temperature>>()
        {
            { Unit.DegreesC, new UnitInfo<Temperature>(UnitAbbreviations[Unit.DegreesC], FromDegreesC, typeof(Temperature).GetProperty("DegreesC").GetGetMethod()) },
            { Unit.DegreesF, new UnitInfo<Temperature>(UnitAbbreviations[Unit.DegreesF], FromDegreesF, typeof(Temperature).GetProperty("DegreesF").GetGetMethod()) },
            { Unit.DegreesK, new UnitInfo<Temperature>(UnitAbbreviations[Unit.DegreesK], FromDegreesK, typeof(Temperature).GetProperty("DegreesK").GetGetMethod()) }
        };

        private Temperature()
        {
            throw new InvalidOperationException("No one should be calling this, it is only here to prevent users from creating default object");
        }

        private Temperature(double temperature)
        {
            _temperature = temperature;
        }

        #region output properties

        public double DegreesC
        {
            get
            {
                return _temperature;
            }
        }

        public double DegreesF
        {
            get
            {
                return (_temperature / CdegreesPerFdegree) + CtoF_Offset;
            }
        }

        public double DegreesK
        {
            get
            {
                return DegreesC + KtoC_Offset;
            }
        }

        #endregion

        #region creation methods

        public static Temperature FromDegreesC(double temperature)
        {
            return new Temperature(temperature);
        }

        public static Temperature FromDegreesF(double temperature)
        {
            return new Temperature((temperature - CtoF_Offset) * CdegreesPerFdegree);
        }

        public static Temperature FromDegreesK(double temperature)
        {
            return FromDegreesC(temperature - KtoC_Offset);
        }

        #endregion

        #region add delta methods

        public void Add(TemperatureDelta delta)
        {
            if (delta == null)
            {
                throw new ArgumentNullException("delta");
            }

            _temperature += delta.DegreesC;
        }

        public void AddDegreesC(double delta)
        {
            _temperature += TemperatureDelta.FromDegreesC(delta).DegreesC;
        }

        public void AddDegreesF(double delta)
        {
            _temperature += TemperatureDelta.FromDegreesF(delta).DegreesC;
        }

        public void AddDegreesK(double delta)
        {
            _temperature += TemperatureDelta.FromDegreesK(delta).DegreesC;
        }

        #endregion

        #region binary operators

        
        public static Temperature operator +(Temperature left, TemperatureDelta right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot add a null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot add null TemperatureDelta");

            return new Temperature(left.DegreesC + right.DegreesC);
        }

        
        public static Temperature operator +(TemperatureDelta left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot add a null TemperatureDelta");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot add null Temperature");

            return new Temperature(left.DegreesC + right.DegreesC);
        }

        
        public static TemperatureDelta operator -(Temperature left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot add a null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot add null Temperature");

            return TemperatureDelta.FromDegreesC(left.DegreesC - right.DegreesC);
        }

        
        public static Temperature operator -(Temperature left, TemperatureDelta right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot add a null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot add null TemperatureDelta");

            return Temperature.FromDegreesC(left.DegreesC - right.DegreesC);
        }

        #endregion

        #region comparison operators

        public static bool operator ==(Temperature left, Temperature right)
        {
            bool bReturn = false;

            if (object.ReferenceEquals(left, null))
            {
                if (object.ReferenceEquals(right, null))
                {
                    //both are null, so we are ==
                    bReturn = true;
                }
            }
            else
            {
                if (!object.ReferenceEquals(left, null) &&
                    !object.ReferenceEquals(right, null))
                {
                    bReturn = left._temperature == right._temperature;
                }
            }

            return bReturn;
        }

        public static bool operator !=(Temperature left, Temperature right)
        {
            return !(left == right);
        }

        
        public static bool operator <(Temperature left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot compare null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot compare null Temperature");

            return (left._temperature < right._temperature);
        }

        
        public static bool operator >(Temperature left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot compare null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot compare null Temperature");

            return (left._temperature > right._temperature);
        }

        
        public static bool operator <=(Temperature left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot compare null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot compare null Temperature");

            return (left._temperature <= right._temperature);
        }

        
        public static bool operator >=(Temperature left, Temperature right)
        {
            if (null == left)
                throw new ArgumentNullException("left", "Cannot compare null Temperature");

            if (null == right)
                throw new ArgumentNullException("right", "Cannot compare null Temperature");

            return (left._temperature >= right._temperature);
        }

        #endregion

        #region parsing

        public static Temperature Parse(string s)
        {
            return Common.Parse(s, _unitInfo);
        }

        public static bool TryParse(string s, out Temperature value)
        {
            try
            {
                value = Temperature.Parse(s);
                return true;
            }
            catch (Exception)
            {
                value = null;
                return false;
            }
        }

        #endregion

        #region Object overrides

        public override bool Equals(object obj)
        {
            Temperature t = obj as Temperature;

            return (t == null) ? false : (this._temperature == t._temperature);
        }

        public override int GetHashCode()
        {
            return _temperature.GetHashCode();
        }

        /// <summary>
        /// Returns a string appended with default units (e.g. "1.23 m/sec^2").
        /// </summary>
        /// <returns>
        /// A string that represents this instance
        /// </returns>
        public override string ToString()
        {
            return ToString(DefaultUnits);
        }

        /// <summary>
        /// overload that returns a string appended with specified units (e.g. "1.23 ft/sec^2").
        /// </summary>
        /// <param name="units"></param>
        /// <returns></returns>
        public string ToString(Unit units)
        {
            double value = (double)_unitInfo[units].PropertyGetter.Invoke(this, null);

            return $"{value} {_unitInfo[units].Abbreviation[0]}";
        }

        #endregion
    }
}
