using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using C1.C1Zip;

namespace wijmoSample1.Models.Maps
{
    public class KmlReader
    {
        #region public members
        public static IList<C1VectorItemBase> ReadVectorItems(Stream stream)
        {
            var kmlStream = GetKmlStream(stream);
            if (kmlStream == null) return null;

            using (var reader = new StreamReader(kmlStream))
            {
                var context = new WijJsonReaderContext();
                Read(context, reader.ReadToEnd());
                return context.VectorItems;
            }
        }
        #endregion

        #region implementation

        private static Stream GetKmlStream(Stream stream)
        {
            if (stream == null) return null;

            var kmlStream = stream;
            if (stream.CanSeek && C1ZipFile.IsZipFile(stream))
            {
                var zip = new C1ZipFile(stream);
                foreach (var entry in zip.Entries)
                {
                    if (entry.FileName.EndsWith(".kml", StringComparison.OrdinalIgnoreCase))
                    {
                        kmlStream = entry.OpenReader();
                        break;
                    }
                }
            }

            return kmlStream;
        }

        private static void Read(IReaderContext context, string xmlContent)
        {
            var xdoc = XElement.Parse(xmlContent);
            var ns = xdoc.GetDefaultNamespace();
            var placeNames = ReadPlacemarkNames(ns, xdoc);
            var styles = ReadStyles(ns, xdoc);

            ReadPlacemarks(context, xdoc, placeNames, styles);
        }

        private static IList<XName> ReadPlacemarkNames(XNamespace ns, XElement xdoc)
        {
            const string defPlacemarkName = "Placemark";

            var placeNames = new List<XName> { ns + defPlacemarkName };

            var schemas = from schema in xdoc.Elements(ns + "Document").Descendants(ns + "Schema")
                          where ((string)schema.Attribute("parent") == defPlacemarkName)
                          select (string)schema.Attribute("name");
            placeNames.AddRange(schemas.Select(schema => ns + schema));

            return placeNames;
        }

        private static IDictionary<string, XElement> ReadStyles(XNamespace ns, XElement xdoc)
        {
            var styles = new Dictionary<string, XElement>();
            var styleElements = xdoc.Elements(ns + "Document").Descendants(ns + "Style");

            foreach (var styleElement in styleElements)
            {
                var idElement = styleElement.Attribute("id");
                if (idElement != null)
                {
                    styles.Add(idElement.Value, styleElement);
                }
            }

            var styleMapElements = xdoc.Elements(ns + "Document").Descendants(ns + "StyleMap");
            foreach (var styleMapElement in styleMapElements)
            {
                var idElement = styleMapElement.Attribute("id");
                if (idElement == null || string.IsNullOrEmpty(idElement.Value)) continue;

                foreach (var pair in styleMapElement.Descendants(ns + "Pair"))
                {
                    var alias = pair.Element(ns + "styleUrl");
                    if (alias != null)
                    {
                        XElement mappedStyle;
                        var styleName = alias.Value.TrimStart('#');
                        if (styles.TryGetValue(styleName, out mappedStyle))
                        {
                            styles[idElement.Value] = mappedStyle;
                        }
                        break;
                    }
                }
            }

            return styles;
        }

        private static void ReadPlacemarks(IReaderContext context, XElement xdoc, IList<XName> names,
            IDictionary<string, XElement> styles)
        {
            var ns = xdoc.GetDefaultNamespace();

            var places = from place in xdoc.DescendantsAndSelf()
                         where names.Contains(place.Name)
                         select place;

            foreach (var place in places)
            {
                var featureVector = context.CreateFeature();
                context.Root.AddChild(featureVector);

                var vector = ReadPoint(context, ns, place.Element(ns + "Point")) ??
                    ReadLineString(context, ns, place.Element(ns + "LineString")) ??
                    ReadPolygons(context, ns, place) ??
                    ReadMultiGeometry(context, ns, place.Element(ns + "MultiGeometry"));
                if (vector == null) continue;

                featureVector.AddChild(vector);
                ApplyPlacemarkProerties(ns, featureVector, place, styles);
            }
        }

        private static void ApplyPlacemarkProerties(XNamespace ns, IVectorObject vector, XElement element,
            IDictionary<string, XElement> styles)
        {
            var nameElement = element.Element(ns + "name");
            if (nameElement == null) return;

            vector.ApplyProperty("Name", nameElement.Value);
            vector.ApplyProperty("Label", nameElement.Value);

            XElement styleElement;
            var styleUrlElement = element.Element(ns + "styleUrl");
            if (styleUrlElement != null)
            {
                var styleUrlString = styleUrlElement.Value;
                if (styleUrlString.StartsWith("#"))
                {
                    styleUrlString = styleUrlString.Substring(1);
                    if (styles.ContainsKey(styleUrlString))
                    {
                        styleElement = styles[styleUrlString];
                        if (styleElement != null)
                            ApplyStyle(ns, vector, styleElement);
                    }
                }
            }

            styleElement = element.Element(ns + "Style");
            if (styleElement != null)
                ApplyStyle(ns, vector, styleElement);
        }

        private static void ApplyStyle(XNamespace ns, IVectorObject vector, XElement style)
        {
            var polyStyleElement = style.Element(ns + "PolyStyle");
            if (polyStyleElement != null)
            {
                var fillColor = GetColor(polyStyleElement);
                if (fillColor.HasValue)
                    vector.ApplyProperty("Fill", fillColor.Value);
            }

            var lineStyleElement = style.Element(ns + "LineStyle");
            if (lineStyleElement != null)
            {
                var strokeColor = GetColor(lineStyleElement);
                if (strokeColor.HasValue)
                    vector.ApplyProperty("Stroke", strokeColor.Value);
            }
        }

        private static IVectorObject ReadMultiGeometry(IReaderContext context, XNamespace ns, XElement container)
        {
            if (container == null || container.Name != ns + "MultiGeometry") return null;

            var geometries = context.CreateMultiGeometry();
            foreach (var element in container.Elements())
            {
                var vector = ReadLineString(context, ns, element) ??
                    ReadPolygon(context, ns, element) ??
                    ReadMultiGeometry(context, ns, element);
                if (vector == null) continue;

                geometries.AddChild(vector);
            }

            return geometries;
        }

        private static IVectorObject ReadPoint(IReaderContext context, XNamespace ns, XElement element)
        {
            if (element == null || element.Name != ns + "Point") return null;

            var point = GetPoint(element);
            if (point == null) return null;

            var placemark = context.CreatePlacemark();
            placemark.Points = new[] { point.Value };
            return placemark;
        }

        private static IVectorObject ReadLineString(IReaderContext context, XNamespace ns, XElement element)
        {
            if (element == null || element.Name != ns + "LineString") return null;

            var points = GetPoints(element);
            if (points == null) return null;

            var polyline = context.CreatePolyline();
            polyline.Points = points;
            return polyline;
        }

        private static IVectorObject ReadPolygon(IReaderContext context, XNamespace ns, XElement element)
        {
            if (element == null || element.Name != ns + "Polygon") return null;

            var boundsElement = element.Element(ns + "outerBoundaryIs");
            if (boundsElement == null) return null;

            var linearRingElement = boundsElement.Element(ns + "LinearRing");
            if (linearRingElement == null) return null;

            var points = GetPoints(linearRingElement);
            if (points == null) return null;

            var polygon = context.CreatePolygon();
            polygon.Points = points;
            return polygon;
        }

        private static IVectorObject ReadPolygons(IReaderContext context, XNamespace ns, XElement element)
        {
            var vectors = context.CreatePolygons();
            var polygonElements = element.Elements(ns + "Polygon");
            bool empty = true;

            foreach (var polygonElement in polygonElements)
            {
                var boundsElement = polygonElement.Element(ns + "outerBoundaryIs");
                if (boundsElement == null) continue;

                var linearRingElement = boundsElement.Element(ns + "LinearRing");
                if (linearRingElement == null) continue;

                var points = GetPoints(linearRingElement);
                if (points == null) continue;

                var polygon = context.CreatePolygon();
                polygon.Points = points;
                vectors.AddChild(polygon);

                empty = false;
            }

            return empty ? null : vectors;
        }

        #endregion

        #region utils

        private static Color? GetColor(XElement element)
        {
            var colorElement = element.Element(element.GetDefaultNamespace() + "color");
            if (colorElement == null) return null;

            var colorString = colorElement.Value;
            if (string.IsNullOrEmpty(colorString)) return null;

            UInt32 argb;
            if (!UInt32.TryParse(colorString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out argb)) return null;

            var a = (byte)((argb & 0xff000000) >> 0x18);
            var b = (byte)((argb & 0xff0000) >> 0x10);
            var g = (byte)((argb & 0xff00) >> 8);
            var r = (byte)(argb & 0xff);
            return Color.FromArgb(a, r, g, b);
        }

        private static PointD? GetPoint(XElement element)
        {
            var coordsElement = element.Element(element.GetDefaultNamespace() + "coordinates");
            if (coordsElement == null) return null;

            PointD? point = null;
            var coordsString = coordsElement.Value;
            if (!string.IsNullOrEmpty(coordsString))
            {
                var coordsValues = coordsString.Split(',');
                if (coordsValues.Length >= 2)
                {
                    double x, y;
                    if (double.TryParse(coordsValues[0], out x) &&
                        double.TryParse(coordsValues[1], out y))
                    {
                        point = new PointD(x, y);
                    }
                }
            }
            return point;
        }

        private static IList<PointD> GetPoints(XElement element)
        {
            var coordsElement = element.Element(element.GetDefaultNamespace() + "coordinates");
            if (coordsElement == null) return null;

            var coordsString = coordsElement.Value;
            if (string.IsNullOrEmpty(coordsString)) return null;

            var coordsValues = coordsString.Split(new char[] { }, StringSplitOptions.RemoveEmptyEntries);
            var len = coordsValues.Length;
            if (len <= 0) return null;

            var points = new List<PointD>();
            for (var i = 0; i < len; i++)
            {
                var pointValues = coordsValues[i].Split(',');
                if (pointValues.Length < 2) continue;

                double x, y;
                if (double.TryParse(pointValues[0], out x) &&
                    double.TryParse(pointValues[1], out y))
                {
                    points.Add(new PointD(x, y));
                }
            }
            return points;
        }

        #endregion

        #region context/wrapper interfaces
        private interface IReaderContext
        {
            IVectorObject CreateFeature();
            IVectorObject CreatePlacemark();
            IVectorObject CreatePolyline();
            IVectorObject CreatePolygon();
            IVectorObject CreatePolygons();
            IVectorObject CreateMultiGeometry();
            IVectorObject Root { get; }
        }

        private interface IVectorObject
        {
            IList<IVectorObject> Children { get; }
            void AddChild(IVectorObject child);
            IList<PointD> Points { set; }
            void ApplyProperty(string name, object value);
        }
        #endregion

        #region WijJson context/wrapper

        class C1VectorItemCollectionWrapper : IVectorObject
        {
            private readonly IList<IVectorObject> _children = new List<IVectorObject>();

            public IList<IVectorObject> Children
            {
                get
                {
                    return _children;
                }
            }

            public void AddChild(IVectorObject child)
            {
                _children.Add(child);
            }

            public IList<PointD> Points
            {
                set
                {
                    // do nothing
                }
            }

            public void ApplyProperty(string name, object value)
            {
                foreach (var child in _children)
                {
                    child.ApplyProperty(name, value);
                }
            }
        }

        private class C1VectorItemWrapper : IVectorObject
        {
            private readonly C1VectorItemBase _item;

            public C1VectorItemWrapper(C1VectorItemBase item)
            {
                _item = item;
            }

            public C1VectorItemBase Item
            {
                get { return _item; }
            }

            public IList<IVectorObject> Children
            {
                get { return null; }
            }

            public void AddChild(IVectorObject child)
            {
                // do nothing
            }

            public IList<PointD> Points
            {
                set
                {
                    if (_item == null) return;
                    _item.Points = value;
                }
            }
            public void ApplyProperty(string name, object value)
            {
                if (_item == null) return;

                switch (name)
                {
                    case "Fill":
                        _item.Fill = (Color)value == null ? "" : (Color)value == Color.Transparent ? "transparent" : string.Format("#{0}{1}{2}", ((Color)value).R.ToString("X2"), ((Color)value).G.ToString("X2"), ((Color)value).B.ToString("X2")).ToLower();
                        break;
                    case "Stroke":
                        _item.Stroke = (Color)value == null ? "" : (Color)value == Color.Transparent ? "transparent" : string.Format("#{0}{1}{2}", ((Color)value).R.ToString("X2"), ((Color)value).G.ToString("X2"), ((Color)value).B.ToString("X2")).ToLower();
                        break;
                    case "Name":
                        _item.Name = (string)value;
                        break;
                    case "Label":
                        if (_item.VectorItemType == VectorType.placemark.ToString())
                        {
                            ((C1VectorPlacemark)_item).Label = (string)value;
                        }
                        break;
                }
            }
        }

        private class WijJsonReaderContext : IReaderContext
        {
            private readonly C1VectorItemCollectionWrapper _root;

            public WijJsonReaderContext()
            {
                _root = new C1VectorItemCollectionWrapper();
            }

            public IList<C1VectorItemBase> VectorItems
            {
                get
                {
                    var items = new List<C1VectorItemBase>();
                    PrepareVectorItems(items, _root);
                    return items;
                }
            }

            private void PrepareVectorItems(IList<C1VectorItemBase> items, IVectorObject vector)
            {
                var itemWrapper = vector as C1VectorItemWrapper;
                if (itemWrapper != null)
                {
                    var item = itemWrapper.Item;
                    if (item != null)
                    {
                        items.Add(item);
                    }
                }

                if (vector.Children != null)
                {
                    foreach (var child in vector.Children)
                    {
                        PrepareVectorItems(items, child);
                    }
                }
            }

            public IVectorObject Root
            {
                get { return _root; }
            }

            public IVectorObject CreateFeature()
            {
                return new C1VectorItemCollectionWrapper();
            }

            public IVectorObject CreatePlacemark()
            {
                return new C1VectorItemWrapper(new C1VectorPlacemark());
            }

            public IVectorObject CreatePolyline()
            {
                return new C1VectorItemWrapper(new C1VectorPolyline());
            }

            public IVectorObject CreatePolygon()
            {
                return new C1VectorItemWrapper(new C1VectorPolygon());
            }

            public IVectorObject CreatePolygons()
            {
                return new C1VectorItemCollectionWrapper();
            }

            public IVectorObject CreateMultiGeometry()
            {
                return new C1VectorItemCollectionWrapper();
            }
        }

        #endregion
    }
}