using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using C1.FlexMap;
using C1.Win.Map;
using Point = C1.Win.Interop.Point;
using Rect = C1.Win.Interop.Rect;

namespace C1.Win.FlexReport.CustomFields.Maps
{
    /// <summary>
    /// Provides functionality that allows to use the C1.Win.Map.C1Map control
    /// to generate maps in reports.
    /// </summary>
    /// <remarks>
    /// TODO:
    /// - allow to specify keys for tile sources that require it (CloudMadeMapsSource);
    /// - ?
    /// </remarks>
    public class C1Mapper
    {
        #region private data
        
        private readonly IMapperOwner _owner;
        private readonly C1.Win.Map.C1Map _c1map;
        private readonly List<MapsLayerBase> _layers = new List<MapsLayerBase>();
        
        // client-set bounds for auto-zoom (united with KML bounds by the mapper):
        private C1.Win.Interop.Rect? _boundingRect;

        #endregion

        #region nested types
        /// <summary>
        /// C1Maps limits/constants:
        /// </summary>
        private static class Limits
        {
            public const double MinLat = -90d;
            public const double MaxLat = 90d;
            public const double MinLong = -180d;
            public const double MaxLong = 180d;
            public const double MinZoom = C1Map.MinZoomValue;
            public const double MaxZoom = C1Map.MaxZoomValue;

            public const int MercatorMapWidth = 512;
        }

        private abstract class MapsLayerBase
        {
            protected MapsLayerBase(LayerBase owner)
            {
                MapLayer = owner;
            }

            public LayerBase MapLayer { get; private set; }

            public abstract Win.Map.VectorLayer Layer { get; }

            public abstract IEnumerable<C1.Win.Interop.Rect> GeoBounds { get; }
            public virtual void ClearMarks() { }
            public abstract void UpdateLayerProperties();
        }

        private abstract class VectorLayer : MapsLayerBase
        {
            private readonly C1.Win.Map.VectorLayer _vectorLayer;

            protected VectorLayer(LayerBase owner)
                : base(owner)
            {
                _vectorLayer = new C1.Win.Map.VectorLayer();
                _vectorLayer.Name = MapLayer.Key;
                _vectorLayer.LabelVisibility = FlexMap.LabelVisibility.AutoHide;
            }

            public override Win.Map.VectorLayer Layer
            {
                get { return _vectorLayer; }
            }

            public override IEnumerable<C1.Win.Interop.Rect> GeoBounds
            {
                get
                {
                    return from mark in _vectorLayer.Items
                        where mark.Visible && mark.Track
                        select mark.Geometry.Bounds;
                }
            }

            protected void InternalClearMarks()
            {
                Layer.Items.Clear();
            }

            public override void UpdateLayerProperties()
            {
                Layer.Opacity = MapLayer.Opacity;
            }
        }

        private class PointsMapsLayer : VectorLayer
        {
            public PointsMapsLayer(LayerBase owner)
                : base(owner)
            {
            }

            public void AddMark(C1.Win.Map.VectorPlacemark mark)
            {
                Layer.Items.Add(mark);
            }

            public override void ClearMarks()
            {
                InternalClearMarks();
            }
        }

        private class LinesMapsLayer : VectorLayer
        {
            public LinesMapsLayer(LayerBase owner)
                : base(owner)
            {
            }

            public void AddLine(C1.Win.Map.VectorPolyline line)
            {
                Layer.Items.Add(line);
            }

            public override void ClearMarks()
            {
                InternalClearMarks();
            }
        }

        private class KmlMapsLayer : VectorLayer
        {
            public KmlMapsLayer(LayerBase owner)
                : base(owner)
            {
            }
            
            /// <summary>
            /// Adds a layer created from the specified KML stream.
            /// </summary>
            /// <param name="kml">The source KML stream.</param>
            /// <param name="addingItemProc">The callback to invoke for each KML item as it is being loaded.</param>
            public void Update(Stream kml, AddingKmlItemDelegate addingItemProc)
            {
                InternalClearMarks();
                if (kml != null)
                    C1Mapper.LoadKML(Layer, kml, addingItemProc);
            }
        }
        #endregion

        #region construction
        private C1Mapper()
            : this(null)
        {
            // do not use this ctor.
        }

        public C1Mapper(IMapperOwner owner)
        {
            _owner = owner;
            _owner.LayersChanged += Map_LayersChanged;
            _owner.Layers.CollectionChanged += Layers_CollectionChanged;

            _c1map = new C1.Win.Map.C1Map();
            _c1map.ResetToDefaultStyle();
            _c1map.BackColor = GetMapBackColor();
            InitDistanceScale();
            _c1map.TileLoaded += (s,e)=>OnTileLoaded();

            HideShowTools();

            //
            ResetLayers();
        }

        /// <summary>
        /// Gets the backcolor for the Map.
        /// </summary>
        /// <returns>
        /// The backcolor for the Map.
        /// </returns>
        /// <remarks>
        /// On saving Metafile to Bitmap, the text gets blurred if the backcolor is Transparent.
        /// So using the White as the backcolor. And WPF does not support Metafile.
        /// </remarks>
        private Color GetMapBackColor()
        {
            var backColor = _owner.BackColor;

#if !WPF
            if (_owner.ImageType == Map.ImageTypeEnum.Emf)
                return backColor;
#endif

            if (backColor == Color.Transparent)
            {
                backColor = Color.White;
            }

            return backColor;
        }

        private void InitDistanceScale()
        {
            _c1map.DistanceScale.Alignment = ContentAlignment.BottomRight;

            var barStyle = _c1map.DistanceScale.BarStyle;
            barStyle.BackColor2 = Color.FromArgb(0xf7, 0xf7, 0xf7);
            barStyle.BackColor = Color.FromArgb(0xb7, 0xb7, 0xb7);
            barStyle.GradientStyle = GradientStyle.Horizontal;
            barStyle.Stroke.Color = Color.FromArgb(0x72, 0x72, 0x72);
            barStyle.Stroke.Width = 1;
            barStyle.ForeColor = Color.FromArgb(0x31, 0x42, 0x4E);

            var style = _c1map.DistanceScale.Style;
            style.Border.Color = Color.FromArgb(0x72, 0x72, 0x72);
            style.Border.Width = 1;

            _c1map.DistanceScale.Margin = new Padding(12);
        }

        public int Width
        {
            get { return _c1map.Width; }
            set { _c1map.Width = value; }
        }

        public int Height
        {
            get { return _c1map.Height; }
            set { _c1map.Height = value; }
        }

        private void ResetLayers()
        {
            _layers.Clear();

            _c1map.Layers.Clear();
            
            foreach (LayerBase l in _owner.Layers.FindAllReverse((ll) => ll.Visible))
            {
                MapsLayerBase layer;
                if (l is PointsLayer)
                    layer = new PointsMapsLayer(l);
                else if (l is LinesLayer)
                    layer = new LinesMapsLayer(l);
                else if (l is KmlLayer)
                    layer = new KmlMapsLayer(l);
                else
                {
                    System.Diagnostics.Debug.Assert(false, "unknown map layer type");
                    layer = null;
                }
                _layers.Add(layer);

                layer.Layer.ParentMap = _c1map;
                _c1map.Layers.Add(layer.Layer);
            }
        }

        void Map_LayersChanged(object sender, EventArgs e)
        {
            _owner.Layers.CollectionChanged += Layers_CollectionChanged;
            ResetLayers();
        }

        void Layers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            // TODO: this is too drastic. Analyze e.Action and only update layers rather than recreating all:
            ResetLayers();
        }
        #endregion

        #region private

        /// <summary>
        /// Sets visibility of the standard C1Maps tools (scale, pan & zoom) based on owner's settings.
        /// </summary>
        public void HideShowTools()
        {
            if (_owner == null)
                return;

            _c1map.DistanceScale.Visible = _owner.ShowScale;
            SetBackground(_c1map.DistanceScale.Style, _owner.ToolsColor);

            _c1map.PanTool.Visible = _owner.ShowPanTool;
            SetBackground(_c1map.PanTool.PanStyle, _owner.ToolsColor);

            _c1map.ZoomTool.Visible = _owner.ShowZoomTool;
            SetBackground(_c1map.ZoomTool.ButtonStyle, _owner.ToolsColor);
            SetBackground(_c1map.ZoomTool.TrackStyle, _owner.ToolsColor);
            SetBackground(_c1map.ZoomTool.ThumbStyle, _owner.ToolsColor);
        }

        private void SetBackground(Style style, Color color)
        {
            style.BackColor = color;
            style.GradientStyle = GradientStyle.None;
        }

        /// <summary>
        /// Refreshes the tile source if needed, based on owner setting.
        /// </summary>
        private void RefreshTileSource()
        {
            if (_owner == null)
                return;

            _c1map.TileLayer.TileSource = _owner.ActualTileSource;
        }

        /// <summary>
        /// Ensures a longitude value is valid.
        /// </summary>
        /// <param name="longitude">The value to validate.</param>
        /// <returns>Value within MinLong/MaxLong limits.</returns>
        private double ValidateLongitude(double longitude)
        {
            return Math.Min(Limits.MaxLong, Math.Max(Limits.MinLong, longitude));
        }

        /// <summary>
        /// Ensures a latitude value is valid.
        /// </summary>
        /// <param name="latitude">The value to validate.</param>
        /// <returns>Value within MinLat/MaxLat limits.</returns>
        private double ValidateLatitude(double latitude)
        {
            return Math.Min(Limits.MaxLat, Math.Max(Limits.MinLat, latitude));
        }

        /// <summary>
        /// Centers and zooms the map based on owner settings (including auto zoom/center).
        /// </summary>
        public void DoCenterAndZoom()
        {
            if (_owner == null)
            {
                _c1map.Viewport.Center = Point.Empty;
                _c1map.Viewport.Zoom = 0;
                return;
            }

            if (_owner.AutoCenter)
            {
                Rect bounds = Rect.Empty;
                bool first = true;

                Action<Rect> processGeoPoint = (rect) =>
                {
                    if (first)
                    {
                        first = false;
                        bounds = rect;
                    }
                    else
                    {
                        bounds = Rect.Union(bounds, rect);
                    }
                };

                foreach (MapsLayerBase lb in _layers.FindAll((l) => l.MapLayer.Track))
                    foreach (var rect in lb.GeoBounds)
                        processGeoPoint(rect);
                if (_boundingRect.HasValue)
                {
                    processGeoPoint(_boundingRect.Value);
                }

                // apply padding:
                if (bounds.IsEmpty)
                {
                    _c1map.Viewport.Center = new Point(_owner.CenterLongitude, _owner.CenterLatitude);
                    _c1map.Viewport.Zoom = _owner.ZoomLevel;
                }
                else
                {
                    var padLon = Math.Abs(bounds.Width) * _owner.AutoZoomPadLon / 100;
                    var padLat = Math.Abs(bounds.Height) * _owner.AutoZoomPadLat / 100;
                    bounds.X -= padLon;
                    bounds.Width += padLon * 2;
                    bounds.Y -= padLat;
                    bounds.Height += padLat * 2;

                    SetExtent(bounds.TopLeft, bounds.BottomRight, _owner.AutoZoom, _owner.MaxAutoZoom > 0 ? _owner.MaxAutoZoom : Limits.MaxZoom, _owner.RoundAutoZoom);
                    if (!_owner.AutoZoom)
                        _c1map.Viewport.Zoom = _owner.ZoomLevel;
                }
            }
            else // if not AutoCenter, AutoZoom is ignored:
            {
                _c1map.Viewport.Center = new Point(_owner.CenterLongitude, _owner.CenterLatitude);
                _c1map.Viewport.Zoom = _owner.ZoomLevel;
            }
        }

        /// <summary>
        /// Sets the map extent and optionally auto-zooms the map.
        /// </summary>
        /// <param name="extMin">Minimum point of the new extent.</param>
        /// <param name="extMax">Maximum point of the new extent.</param>
        /// <param name="autoZoom">True to auto zoom.</param>
        /// <param name="maxZoom">Maximum auto zoom value.</param>
        /// <param name="roundAutoZoom">True to round down the calculated auto zoom value.</param>
        private void SetExtent(Point extMin, Point extMax, bool autoZoom, double maxZoom, bool roundAutoZoom)
        {
            // sanity:
            extMin.X = ValidateLongitude(extMin.X);
            extMin.Y = ValidateLatitude(extMin.Y);
            extMax.X = ValidateLongitude(extMax.X);
            extMax.Y = ValidateLatitude(extMax.Y);

#if DEBUG && only_if_you_really_need_it
            DrawPoint(extMin.X, extMin.Y, "", 5, System.Drawing.Color.DarkMagenta, System.Drawing.Color.Magenta, MarkerShape.Star);
            DrawPoint(extMin.X, extMax.Y, "", 5, System.Drawing.Color.DarkMagenta, System.Drawing.Color.Magenta, MarkerShape.Star);
            DrawPoint(extMax.X, extMin.Y, "", 5, System.Drawing.Color.DarkMagenta, System.Drawing.Color.Magenta, MarkerShape.Star);
            DrawPoint(extMax.X, extMax.Y, "", 5, System.Drawing.Color.DarkMagenta, System.Drawing.Color.Magenta, MarkerShape.Star);
            DrawPoint((extMax.X + extMin.X) / 2, (extMax.Y + extMin.Y) / 2, "", 5, System.Drawing.Color.DarkMagenta, System.Drawing.Color.Magenta, MarkerShape.Star);
#endif
            Point min = _c1map.CoordinateSystem.Project(extMin);
            Point max = _c1map.CoordinateSystem.Project(extMax);
            if (autoZoom)
            {
                Point absMin = new Point(0, 0);
                // this is based on C1Maps.ViewportWidth code:
                Point absMax = new Point((double)Width / Limits.MercatorMapWidth, (double)Height / Limits.MercatorMapWidth);

                double xZoom = Math.Abs(absMax.X - absMin.X) / Math.Abs(max.X - min.X);
                double yZoom = Math.Abs(absMax.Y - absMin.Y) / Math.Abs(max.Y - min.Y);
                double zoom = Math.Min(xZoom, yZoom);

                zoom = Math.Max(Limits.MinZoom, Math.Log(zoom, 2));
                if (roundAutoZoom)
                    zoom = Math.Truncate(zoom);

                _c1map.Viewport.Zoom = Math.Min(zoom, maxZoom);
            }
            // center:
            Point cc = new Point((min.X + max.X) / 2, (min.Y + max.Y) / 2);
            var center = _c1map.CoordinateSystem.Unproject(cc);
            _c1map.Viewport.Center = center;
        }

        /// <summary>
        /// Loads a KML or KMZ file from the specified stream into the layer.
        /// </summary>
        /// <param name="vl">The layer to load the stream into.</param>
        /// <param name="stream">The source stream.</param>
        /// <param name="addingItemProc">The callback to call for each KML item.</param>
        /// <param name="maxBounds">OUT: The union of all loaded items' bounds.</param>
        private static void LoadKML(Win.Map.VectorLayer vl, Stream stream, AddingKmlItemDelegate addingItemProc)
        {
            var vects = FlexMap.KmlReader.Read(stream);

            vl.BeginUpdate();
            foreach (var vect in vects)
            {
                if (addingItemProc != null)
                {
                    var vector = addingItemProc(vect);
                    if (vector != null) vl.Items.Add(vector);
                }

            }
            vl.EndUpdate();
        }
        #endregion

        #region events

        /// <summary>
        /// Occurs when the tile images is downloaded.
        /// </summary>
        public event EventHandler TileLoaded;

        private void OnTileLoaded()
        {
            if (TileLoaded != null)
                TileLoaded(this, EventArgs.Empty);
        }

        #endregion

        #region publics

        /// <summary>
        /// Gets a System.Drawing.Image representing the current map.
        /// </summary>
        /// <param name="designTime">Specifies whether we are in design time or runtime.</param>
        /// <param name="dpiX">The horizontal resolution of the image.</param>
        /// <param name="dpiY">The vertical resolution of the image.</param>
        /// <returns>The image (metafile).</returns>
        public System.Drawing.Image GetImage(bool designTime, double dpiX, double dpiY)
        {
            _c1map.BackColor = GetMapBackColor();
            _c1map.UseAntiAliasedGraphics = _owner.UseAntiAliasedGraphics;
            _c1map.RightToLeft = _owner.ActualRightToLeft ? RightToLeft.Yes : RightToLeft.No;
            RefreshTileSource();

            // apply layer-wide properties:
            foreach (MapsLayerBase lb in _layers)
                lb.UpdateLayerProperties();

            // clear old legends:
            _c1map.Legends.Clear();

            // add new ones:
            if (_owner != null)
            {
                foreach (Legend ml in _owner.Legends)
                {
                    if (ml.Visible)
                    {
                        var legend = new Win.Map.MapLegend();
                        _c1map.Legends.Add(legend);

                        var l = new MapperLegend(legend);
                        l.UpdateFromOwner(_owner, ml);
                    }
                }
            }
            try
            {
                return _c1map.GetMetafile(designTime);
            }
            catch
            {
                return _c1map.GetBitmap(designTime);
            }
        }

        /// <summary>
        /// Gets image representing the current map, with the previous data.
        /// </summary>
        /// <param name="designTime">Specifies whether we are in design time or runtime.</param>
        /// <returns>The image (metafile).</returns>
        public Image RefreshImage(bool designTime)
        {
            try
            {
                return _c1map.GetMetafile(designTime);
            }
            catch
            {
                return _c1map.GetBitmap(designTime);
            }
        }

        public void ResetBoundingPoints()
        {
            _boundingRect = null;
        }

        public void AddBoundingPoint(double longitude, double latitude)
        {
            if (!double.IsNaN(longitude) && !double.IsNaN(latitude))
            {
                _boundingRect = _boundingRect.HasValue
                    ? Rect.Union(_boundingRect.Value, new Rect(longitude, latitude, 0, 0))
                    : new Rect(longitude, latitude, 0, 0);
            }
        }

        /// <summary>
        /// Adds a layer created from the specified KML stream.
        /// </summary>
        /// <param name="kml">The source KML stream.</param>
        /// <param name="name">The new layer name (must be a valid alphanumeric name).</param>
        /// <param name="addingItemProc">The callback to invoke for each KML item as it is being loaded.</param>
        public void UpdateKmlLayer(string layerKey, Stream kml, AddingKmlItemDelegate addingItemProc)
        {
            KmlMapsLayer kl = _layers.Find((l) => l.MapLayer.Key == layerKey) as KmlMapsLayer;
            System.Diagnostics.Debug.Assert(kl != null);
            kl.Update(kml, addingItemProc);
        }

        /// <summary>
        /// Clears all previously added marks and lines. (Does not affect KML layers.)
        /// </summary>
        public void ClearMarks()
        {
            _layers.ForEach((l) => l.ClearMarks());
        }

        /// <summary>
        /// Calculates screen distance (in 96dpi pixels) between two points.
        /// </summary>
        /// <param name="longitude1"></param>
        /// <param name="latitude1"></param>
        /// <param name="longitude2"></param>
        /// <param name="latitude2"></param>
        /// <returns></returns>
        public double DistancePix(double longitude1, double latitude1, double longitude2, double latitude2)
        {
            var p1 = _c1map.CoordinateSystem.Project(new Point(longitude1, latitude1));
            var p2 = _c1map.CoordinateSystem.Project(new Point(longitude2, latitude2));
            double scale = Math.Pow(2, _c1map.Viewport.Zoom) * Limits.MercatorMapWidth;
            return Math.Sqrt(Math.Pow((p1.X - p2.X) * scale, 2) + Math.Pow((p1.Y - p2.Y) * scale, 2));
        }

        /// <summary>
        /// Adds a point.
        /// </summary>
        /// <param name="layerKey"></param>
        /// <param name="longitude"></param>
        /// <param name="latitude"></param>
        /// <param name="caption"></param>
        /// <param name="font"></param>
        /// <param name="markShape"></param>
        /// <param name="size"></param>
        /// <param name="stroke"></param>
        /// <param name="fill"></param>
        public void DrawPoint(string layerKey,
            double longitude, double latitude,
            string caption, System.Drawing.Font font, System.Drawing.Color textColor,
            MarkerShape markShape, double size, System.Drawing.Color stroke, System.Drawing.Color fill)
        {
            var mark = new C1.Win.Map.VectorPlacemark();
            mark.Lod = new FlexMap.LOD(1, 300, 0, 10); // tbd: allow to control this

            mark.Geometry = new FlexMap.GeoPoint(longitude, latitude);

            if (!string.IsNullOrEmpty(caption))
            {
                mark.Marker.Caption = caption;
                if (font != null) mark.LabelStyle.Font = font;
                if (!Util.IsColorEmpty(textColor))
                    mark.LabelStyle.ForeColor=textColor;
            }

            mark.Marker.Shape = (FlexMap.MarkerShape)markShape;
            mark.Marker.Size = Util.GetMarkerSize(markShape, (float)size);
            mark.Style.Stroke.Color = stroke;
            mark.Style.BackColor = fill;

            PointsMapsLayer ml = _layers.Find((l) => l.MapLayer.Key == layerKey) as PointsMapsLayer;
            System.Diagnostics.Debug.Assert(ml != null);
            ml.AddMark(mark);
        }

        /// <summary>
        /// Adds a line to the lines layer.
        /// </summary>
        /// <param name="longitude1"></param>
        /// <param name="latitude1"></param>
        /// <param name="longitude2"></param>
        /// <param name="latitude2"></param>
        /// <param name="stroke"></param>
        /// <param name="dashStyle"></param>
        public void DrawLine(
            string layerKey,
            double longitude1, double latitude1, double longitude2, double latitude2,
            System.Drawing.Color stroke, DashStyle dashStyle, double thickness)
        {
            DrawLine(layerKey, longitude1, latitude1, longitude2, latitude2, stroke, Color.Transparent, dashStyle, thickness);
        }

        /// <summary>
        /// Adds a line to the lines layer.
        /// </summary>
        /// <param name="layerKey"></param>
        /// <param name="longitude1"></param>
        /// <param name="latitude1"></param>
        /// <param name="longitude2"></param>
        /// <param name="latitude2"></param>
        /// <param name="stroke"></param>
        /// <param name="fill"></param>
        /// <param name="dashStyle"></param>
        /// <param name="thickness"></param>
        public void DrawLine(
            string layerKey,
            double longitude1, double latitude1, double longitude2, double latitude2,
            Color stroke, Color fill, DashStyle dashStyle, double thickness)
        {
            var line = new C1.Win.Map.VectorPolyline();
            line.Geometry = new GeoLineString(new[]
            {
                new GeoPoint(longitude1, latitude1),
                new GeoPoint(longitude2, latitude2),
            });
            line.Style.Stroke.Style = (C1.Win.Map.DashStyle)dashStyle;
            line.Style.Stroke.Color = stroke;
            line.Style.BackColor = fill;
            line.Style.Stroke.Width = (float)thickness;

            LinesMapsLayer ll = _layers.Find((l) => l.MapLayer.Key == layerKey) as LinesMapsLayer;
            System.Diagnostics.Debug.Assert(ll != null);
            ll.AddLine(line);
        }
        #endregion
    }
}
