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

namespace Raytheon.Units
{
    /// <summary>
    /// This class represents energy.  
    /// 
    /// It has no constructors.  Instead, it provides multiple methods for creating objects of this 
    /// type.  Those methods are of the form FromXXX, where XXX is a unit (e.g. FromWatts).
    /// 
    /// Properties (e.g. Watts) are provided to convert the value to other units and return 
    /// as type double.
    /// </summary>
    [Serializable]
    [DataContract]
    public class Power
    {
        public enum Unit
        {
            Watts,
            Milliwatts,
            DecibelMilliWatt
        }

        public static IDictionary<Unit, List<string>> UnitAbbreviations = new Dictionary<Unit, List<string>>()
        {
            {Unit.Watts, new List<string>(){ "W", "Watts", "Watt" } },
            {Unit.Milliwatts, new List<string>(){ "mW", "MilliWatt", "MilliWatts" } },
            {Unit.DecibelMilliWatt, new List<string>(){ "dBm", "DecibelMilliWatt", "DecibelMilliWatts" } }
        };

        private const Unit DefaultUnits = Unit.Watts;

        [DataMember(Name = "Power", IsRequired = true)]
        private readonly double _power; // watts

        // map: unit => abbreviation, conversion method and property
        private static readonly IDictionary<Unit, UnitInfo<Power>> _unitInfo = new Dictionary<Unit, UnitInfo<Power>>()
        {
            { Unit.Watts, new UnitInfo<Power>(UnitAbbreviations[Unit.Watts], FromWatts, typeof(Power).GetProperty("Watts").GetGetMethod()) },
            { Unit.Milliwatts, new UnitInfo<Power>(UnitAbbreviations[Unit.Milliwatts], FromMilliwatts, typeof(Power).GetProperty("Milliwatts").GetGetMethod()) },
            { Unit.DecibelMilliWatt, new UnitInfo<Power>(UnitAbbreviations[Unit.DecibelMilliWatt], FromDBM, typeof(Power).GetProperty("DBM").GetGetMethod()) }
        };

        /// <summary>
        /// Prevents a default instance of the <see cref="Power"/> class from being created.  
        /// Use FromXXX methods to create objects of this type.
        /// </summary>
        /// <exception cref="System.Exception">No one should be calling this, it is only here to prevent users from creating default object</exception>
        private Power()
        {
            throw new InvalidOperationException("No one should be calling this, it is only here to prevent users from creating default object");
        }

        /// <summary>
        /// ctor used by this class to create objects in FromXXX methods 
        /// </summary>
        /// <param name="energy">The energy - watts/cm^2.</param>
        private Power(double energy)
        {
            _power = energy;
        }

        #region conversion properties
        public double Milliwatts
        {
            get { return _power * 1000; }
        }

        public double Watts
        {
            get { return _power; }
        }

        public double DBM
        {
            get { return (10 * Math.Log10(Math.Abs(_power))) + 30; }
        }

        /// <summary>
        /// Returns watts/cm^2 as type double
        /// </summary>
        /// <value>
        /// watts/cm^2
        /// </value>
        
        [Obsolete("WattsPerCmSqrd is deprecated, please use Watts instead. This will be removed in a future version.", false)]
        public double WattsPerCmSqrd
        {
            get
            {
                return Double.NaN;
            }
        }
        #endregion

        #region creation methods

        /// </summary>
        /// <param name="wattsPerCmSqrd">watts/cm^2</param>
        /// <returns></returns>
        
        [Obsolete("FromWattsPerCmSqrd is deprecated, please use FromWatts instead. This will be removed in a future version.", false)]
        public static Power FromWattsPerCmSqrd(double wattsPerCmSqrd)
        {
            return new Power(Double.NaN);
        }

        /// <summary>
        /// Returns Power object interpreting passed value as milliwatts
        /// </summary>
        /// <param name="milliwatts">milliwatts</param>
        /// <returns></returns>
        public static Power FromMilliwatts(double milliwatts)
        {
            return new Power(milliwatts / 1000);
        }

        /// <summary>
        /// Returns Power object interpreting passed value as watts
        /// </summary>
        /// <param name="watts">watts</param>
        /// <returns></returns>
        public static Power FromWatts(double watts)
        {
            return new Power(watts);
        }

        public static Power FromDBM(double dbm)
        {
            return new Power(Math.Pow(10, (dbm - 30) / 10));
        }
        #endregion

        public override bool Equals(object obj)
        {
            Power e = obj as Power;

            return (e == null) ? false : (this._power == e._power);
        }

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

        #region binary operators

        // not implementing %, &, |, ^, <<, >>

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

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

            return new Power(left._power + right._power);
        }

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

            if (null == right)
                throw new ArgumentNullException("right", "Cannot subtract a null Power");

            return new Power(left._power - right._power);
        }

        public static Power operator *(Power power, double scalar)
        {
            if (null == power)
                throw new ArgumentNullException("power", "Cannot multiply a null Power");

            return new Power(power._power * scalar);
        }

        public static Power operator *(double scalar, Power power)
        {
            if (null == power)
                throw new ArgumentNullException("power", "Cannot multiply a null Power");

            return new Power(scalar * power._power);
        }

        public static Power operator /(Power power, double scalar)
        {
            if (null == power)
                throw new ArgumentNullException("power", "Cannot divide a null Power");

            if (0.0 == scalar)
                throw new DivideByZeroException("Cannot divide Power by 0");

            return new Power(power._power / scalar);
        }

        #endregion

        #region comparison operators

        public static bool operator ==(Power left, Power 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._power == right._power;
                }
            }
            return bReturn;
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

        #endregion

        #region operators with other classes

        // no operators right now

        #endregion

        #region parsing

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

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

        #endregion

        /// <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]}";
        }
    }
}
