#region PDFsharp - A .NET library for processing PDF
//
// Authors:
//   Stefan Lange
//
// Copyright (c) 2005-2017 empira Software GmbH, Cologne Area (Germany)
//
// http://www.pdfsharp.com
// http://sourceforge.net/projects/pdfsharp
//
// 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.
#endregion

using System;
using System.Diagnostics;
using System.Globalization;

namespace PdfSharp.Drawing
{
    /// <summary>
    /// Represents a value and its unit of measure. The structure converts implicitly from and to
    /// double with a value measured in point.
    /// </summary>
    [DebuggerDisplay("{DebuggerDisplay}")]
    public struct XUnit : IFormattable
    {
        internal const double PointFactor = 1;
        internal const double InchFactor = 72;
        internal const double MillimeterFactor = 72 / 25.4;
        internal const double CentimeterFactor = 72 / 2.54;
        internal const double PresentationFactor = 72 / 96.0;

        internal const double PointFactorWpf = 96 / 72.0;
        internal const double InchFactorWpf = 96;
        internal const double MillimeterFactorWpf = 96 / 25.4;
        internal const double CentimeterFactorWpf = 96 / 2.54;
        internal const double PresentationFactorWpf = 1;

        /// <summary>
        /// Initializes a new instance of the XUnit class with type set to point.
        /// </summary>
        public XUnit(double point)
        {
            _value = point;
            _type = XGraphicsUnit.Point;
        }

        /// <summary>
        /// Initializes a new instance of the XUnit class.
        /// </summary>
        public XUnit(double value, XGraphicsUnit type)
        {
            if (!Enum.IsDefined(typeof(XGraphicsUnit), type))
#if !SILVERLIGHT && !NETFX_CORE && !UWP
                throw new System.ComponentModel.InvalidEnumArgumentException(nameof(type), (int)type, typeof(XGraphicsUnit));
#else
                throw new ArgumentException("type");
#endif
            _value = value;
            _type = type;
        }

        /// <summary>
        /// Gets the raw value of the object without any conversion.
        /// To determine the XGraphicsUnit use property <code>Type</code>.
        /// To get the value in point use the implicit conversion to double.
        /// </summary>
        public double Value
        {
            get { return _value; }
        }

        /// <summary>
        /// Gets the unit of measure.
        /// </summary>
        public XGraphicsUnit Type
        {
            get { return _type; }
        }

        /// <summary>
        /// Gets or sets the value in point.
        /// </summary>
        public double Point
        {
            get
            {
                switch (_type)
                {
                    case XGraphicsUnit.Point:
                        return _value;

                    case XGraphicsUnit.Inch:
                        return _value * 72;

                    case XGraphicsUnit.Millimeter:
                        return _value * 72 / 25.4;

                    case XGraphicsUnit.Centimeter:
                        return _value * 72 / 2.54;

                    case XGraphicsUnit.Presentation:
                        return _value * 72 / 96;

                    default:
                        throw new InvalidCastException();
                }
            }
            set
            {
                _value = value;
                _type = XGraphicsUnit.Point;
            }
        }

        /// <summary>
        /// Gets or sets the value in inch.
        /// </summary>
        public double Inch
        {
            get
            {
                switch (_type)
                {
                    case XGraphicsUnit.Point:
                        return _value / 72;

                    case XGraphicsUnit.Inch:
                        return _value;

                    case XGraphicsUnit.Millimeter:
                        return _value / 25.4;

                    case XGraphicsUnit.Centimeter:
                        return _value / 2.54;

                    case XGraphicsUnit.Presentation:
                        return _value / 96;

                    default:
                        throw new InvalidCastException();
                }
            }
            set
            {
                _value = value;
                _type = XGraphicsUnit.Inch;
            }
        }

        /// <summary>
        /// Gets or sets the value in millimeter.
        /// </summary>
        public double Millimeter
        {
            get
            {
                switch (_type)
                {
                    case XGraphicsUnit.Point:
                        return _value * 25.4 / 72;

                    case XGraphicsUnit.Inch:
                        return _value * 25.4;

                    case XGraphicsUnit.Millimeter:
                        return _value;

                    case XGraphicsUnit.Centimeter:
                        return _value * 10;

                    case XGraphicsUnit.Presentation:
                        return _value * 25.4 / 96;

                    default:
                        throw new InvalidCastException();
                }
            }
            set
            {
                _value = value;
                _type = XGraphicsUnit.Millimeter;
            }
        }

        /// <summary>
        /// Gets or sets the value in centimeter.
        /// </summary>
        public double Centimeter
        {
            get
            {
                switch (_type)
                {
                    case XGraphicsUnit.Point:
                        return _value * 2.54 / 72;

                    case XGraphicsUnit.Inch:
                        return _value * 2.54;

                    case XGraphicsUnit.Millimeter:
                        return _value / 10;

                    case XGraphicsUnit.Centimeter:
                        return _value;

                    case XGraphicsUnit.Presentation:
                        return _value * 2.54 / 96;

                    default:
                        throw new InvalidCastException();
                }
            }
            set
            {
                _value = value;
                _type = XGraphicsUnit.Centimeter;
            }
        }

        /// <summary>
        /// Gets or sets the value in presentation units (1/96 inch).
        /// </summary>
        public double Presentation
        {
            get
            {
                switch (_type)
                {
                    case XGraphicsUnit.Point:
                        return _value * 96 / 72;

                    case XGraphicsUnit.Inch:
                        return _value * 96;

                    case XGraphicsUnit.Millimeter:
                        return _value * 96 / 25.4;

                    case XGraphicsUnit.Centimeter:
                        return _value * 96 / 2.54;

                    case XGraphicsUnit.Presentation:
                        return _value;

                    default:
                        throw new InvalidCastException();
                }
            }
            set
            {
                _value = value;
                _type = XGraphicsUnit.Point;
            }
        }

        /// <summary>
        /// Returns the object as string using the format information.
        /// The unit of measure is appended to the end of the string.
        /// </summary>
        public string ToString(IFormatProvider formatProvider)
        {
            string valuestring = _value.ToString(formatProvider) + GetSuffix();
            return valuestring;
        }

        /// <summary>
        /// Returns the object as string using the specified format and format information.
        /// The unit of measure is appended to the end of the string.
        /// </summary>
        string IFormattable.ToString(string format, IFormatProvider formatProvider)
        {
            string valuestring = _value.ToString(format, formatProvider) + GetSuffix();
            return valuestring;
        }

        /// <summary>
        /// Returns the object as string. The unit of measure is appended to the end of the string.
        /// </summary>
        public override string ToString()
        {
            string valuestring = _value.ToString(CultureInfo.InvariantCulture) + GetSuffix();
            return valuestring;
        }

        /// <summary>
        /// Returns the unit of measure of the object as a string like 'pt', 'cm', or 'in'.
        /// </summary>
        string GetSuffix()
        {
            switch (_type)
            {
                case XGraphicsUnit.Point:
                    return "pt";

                case XGraphicsUnit.Inch:
                    return "in";

                case XGraphicsUnit.Millimeter:
                    return "mm";

                case XGraphicsUnit.Centimeter:
                    return "cm";

                case XGraphicsUnit.Presentation:
                    return "pu";

                //case XGraphicsUnit.Pica:
                //  return "pc";

                //case XGraphicsUnit.Line:
                //  return "li";

                default:
                    throw new InvalidCastException();
            }
        }

        /// <summary>
        /// Returns an XUnit object. Sets type to point.
        /// </summary>
        public static XUnit FromPoint(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Point;
            return unit;
        }

        /// <summary>
        /// Returns an XUnit object. Sets type to inch.
        /// </summary>
        public static XUnit FromInch(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Inch;
            return unit;
        }

        /// <summary>
        /// Returns an XUnit object. Sets type to millimeters.
        /// </summary>
        public static XUnit FromMillimeter(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Millimeter;
            return unit;
        }

        /// <summary>
        /// Returns an XUnit object. Sets type to centimeters.
        /// </summary>
        public static XUnit FromCentimeter(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Centimeter;
            return unit;
        }

        /// <summary>
        /// Returns an XUnit object. Sets type to Presentation.
        /// </summary>
        public static XUnit FromPresentation(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Presentation;
            return unit;
        }

        /// <summary>
        /// Converts a string to an XUnit object.
        /// If the string contains a suffix like 'cm' or 'in' the object will be converted
        /// to the appropriate type, otherwise point is assumed.
        /// </summary>
        public static implicit operator XUnit(string value)
        {
            XUnit unit;
            value = value.Trim();

            // HACK for Germans...
            value = value.Replace(',', '.');

            int count = value.Length;
            int valLen = 0;
            for (; valLen < count;)
            {
                char ch = value[valLen];
                if (ch == '.' || ch == '-' || ch == '+' || char.IsNumber(ch))
                    valLen++;
                else
                    break;
            }

            try
            {
                unit._value = Double.Parse(value.Substring(0, valLen).Trim(), CultureInfo.InvariantCulture);
            }
            catch (Exception ex)
            {
                unit._value = 1;
                string message = String.Format("String '{0}' is not a valid value for structure 'XUnit'.", value);
                throw new ArgumentException(message, ex);
            }

            string typeStr = value.Substring(valLen).Trim().ToLower();
            unit._type = XGraphicsUnit.Point;
            switch (typeStr)
            {
                case "cm":
                    unit._type = XGraphicsUnit.Centimeter;
                    break;

                case "in":
                    unit._type = XGraphicsUnit.Inch;
                    break;

                case "mm":
                    unit._type = XGraphicsUnit.Millimeter;
                    break;

                case "":
                case "pt":
                    unit._type = XGraphicsUnit.Point;
                    break;

                case "pu":  // presentation units
                    unit._type = XGraphicsUnit.Presentation;
                    break;

                default:
                    throw new ArgumentException("Unknown unit type: '" + typeStr + "'");
            }
            return unit;
        }

        /// <summary>
        /// Converts an int to an XUnit object with type set to point.
        /// </summary>
        public static implicit operator XUnit(int value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Point;
            return unit;
        }

        /// <summary>
        /// Converts a double to an XUnit object with type set to point.
        /// </summary>
        public static implicit operator XUnit(double value)
        {
            XUnit unit;
            unit._value = value;
            unit._type = XGraphicsUnit.Point;
            return unit;
        }

        /// <summary>
        /// Returns a double value as point.
        /// </summary>
        public static implicit operator double(XUnit value)
        {
            return value.Point;
        }

        /// <summary>
        /// Memberwise comparison. To compare by value, 
        /// use code like Math.Abs(a.Pt - b.Pt) &lt; 1e-5.
        /// </summary>
        public static bool operator ==(XUnit value1, XUnit value2)
        {
            // ReSharper disable CompareOfFloatsByEqualityOperator
            return value1._type == value2._type && value1._value == value2._value;
            // ReSharper restore CompareOfFloatsByEqualityOperator
        }

        /// <summary>
        /// Memberwise comparison. To compare by value, 
        /// use code like Math.Abs(a.Pt - b.Pt) &lt; 1e-5.
        /// </summary>
        public static bool operator !=(XUnit value1, XUnit value2)
        {
            return !(value1 == value2);
        }

        /// <summary>
        /// Calls base class Equals.
        /// </summary>
        public override bool Equals(Object obj)
        {
            if (obj is XUnit)
                return this == (XUnit)obj;
            return false;
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        public override int GetHashCode()
        {
            // ReSharper disable NonReadonlyFieldInGetHashCode
            return _value.GetHashCode() ^ _type.GetHashCode();
            // ReSharper restore NonReadonlyFieldInGetHashCode
        }

        /// <summary>
        /// This member is intended to be used by XmlDomainObjectReader only.
        /// </summary>
        public static XUnit Parse(string value)
        {
            XUnit unit = value;
            return unit;
        }

        /// <summary>
        /// Converts an existing object from one unit into another unit type.
        /// </summary>
        public void ConvertType(XGraphicsUnit type)
        {
            if (_type == type)
                return;

            switch (type)
            {
                case XGraphicsUnit.Point:
                    _value = Point;
                    _type = XGraphicsUnit.Point;
                    break;

                case XGraphicsUnit.Inch:
                    _value = Inch;
                    _type = XGraphicsUnit.Inch;
                    break;

                case XGraphicsUnit.Centimeter:
                    _value = Centimeter;
                    _type = XGraphicsUnit.Centimeter;
                    break;

                case XGraphicsUnit.Millimeter:
                    _value = Millimeter;
                    _type = XGraphicsUnit.Millimeter;
                    break;

                case XGraphicsUnit.Presentation:
                    _value = Presentation;
                    _type = XGraphicsUnit.Presentation;
                    break;

                default:
                    throw new ArgumentException("Unknown unit type: '" + type + "'");
            }
        }

        /// <summary>
        /// Represents a unit with all values zero.
        /// </summary>
        public static readonly XUnit Zero = new XUnit();

        double _value;
        XGraphicsUnit _type;

        /// <summary>
        /// Gets the DebuggerDisplayAttribute text.
        /// </summary>
        /// <value>The debugger display.</value>
        // ReSharper disable UnusedMember.Local
        string DebuggerDisplay
        // ReSharper restore UnusedMember.Local
        {
            get
            {
                const string format = Config.SignificantFigures10;
                return String.Format(CultureInfo.InvariantCulture, "unit=({0:" + format + "} {1})", _value, GetSuffix());
            }
        }
    }
}