//The MIT License(MIT)

//Copyright(c) 2016 Alberto Rodriguez & LiveCharts Contributors

//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.

using System;
using System.Collections.Generic;
using System.Linq;
using LiveCharts.Definitions.Charts;
using LiveCharts.Definitions.Series;
using LiveCharts.Dtos;
using LiveCharts.Helpers;

namespace LiveCharts.Charts
{
    /// <summary>
    /// Chart Model
    /// </summary>
    public class CartesianChartCore : ChartCore
    {
        #region Constructors

        /// <summary>
        /// Initializes Chart model
        /// </summary>
        /// <param name="view">The view.</param>
        /// <param name="updater">The updater.</param>
        public CartesianChartCore(IChartView view, ChartUpdater updater) : base(view, updater)
        {
            updater.Chart = this;
        }

        #endregion

        #region Publics

        /// <summary>
        /// Prepares Chart Axes
        /// </summary>
        public override void PrepareAxes()
        {
            base.PrepareAxes();

            if (View.ActualSeries.Any(x => !(x.Model is ICartesianSeries)))
                throw new LiveChartsException(
                    "There is a invalid series in the series collection, " +
                    "verify that all the series implement ICartesianSeries.");

            var cartesianSeries = View.ActualSeries.Select(x => x.Model).Cast<ICartesianSeries>().ToArray();

            for (var index = 0; index < AxisX.Count; index++)
            {
                var xi = AxisX[index];

                xi.CalculateSeparator(this, AxisOrientation.X);

                // ReSharper disable once AccessToModifiedClosure
                SetAxisLimits(xi, cartesianSeries.Where(x => x.View.ScalesXAt == index).ToArray(), AxisOrientation.X);

                if (Math.Abs(xi.BotLimit - xi.TopLimit) < xi.S * .01)
                {
                    if (Math.Abs(xi.PreviousBot - xi.PreviousTop) < xi.S * .01)
                    {
                        if (double.IsNaN(xi.MinValue)) xi.BotLimit -= xi.S;
                        else xi.BotLimit = xi.MinValue;

                        if (double.IsNaN(xi.MaxValue)) xi.TopLimit += xi.S;
                        else xi.TopLimit = xi.MaxValue;

                        if (Math.Abs(xi.BotLimit - xi.TopLimit) < xi.S * .01 && !View.IsInDesignMode)
                            throw new LiveChartsException("One axis has an invalid range, it is or it is " +
                                                          "tends to zero, please ensure your axis has a valid " +
                                                          "range");
                    }
                    else
                    {
                        xi.BotLimit = xi.PreviousBot;
                        xi.TopLimit = xi.PreviousTop;
                    }
                }
                xi.PreviousBot = xi.BotLimit;
                xi.PreviousTop = xi.TopLimit;
            }

            for (var index = 0; index < AxisY.Count; index++)
            {
                var yi = AxisY[index];

                yi.CalculateSeparator(this, AxisOrientation.Y);

                // ReSharper disable once AccessToModifiedClosure
                SetAxisLimits(yi, cartesianSeries.Where(x => x.View.ScalesYAt == index).ToArray(), AxisOrientation.Y);

                if (Math.Abs(yi.BotLimit - yi.TopLimit) < yi.S * .01)
                {
                    if (Math.Abs(yi.PreviousBot - yi.PreviousTop) < yi.S * .01)
                    {
                        if (double.IsNaN(yi.MinValue)) yi.BotLimit -= yi.S;
                        else yi.BotLimit = yi.MinValue;

                        if (double.IsNaN(yi.MaxValue)) yi.TopLimit += yi.S;
                        else yi.TopLimit = yi.MaxValue;

                        if (Math.Abs(yi.BotLimit - yi.TopLimit) < yi.S * .01)
                            throw new LiveChartsException("One axis has an invalid range, it is or it " +
                                                          "tends to zero, please ensure your axis has a valid " +
                                                          "range");
                    }
                    else
                    {
                        yi.BotLimit = yi.PreviousBot;
                        yi.TopLimit = yi.PreviousTop;
                    }
                }
                yi.PreviousBot = yi.BotLimit;
                yi.PreviousTop = yi.TopLimit;
            }

            PrepareSeries();
            CalculateComponentsAndMargin();
            DrawOrUpdateSections();

            AreComponentsLoaded = true;
        }

        /// <summary>
        /// Runs the specialized chart components.
        /// </summary>
        public override void RunSpecializedChartComponents()
        {
            foreach (var visualElement in ((ICartesianChart) View).VisualElements)
            {
                visualElement.AddOrMove(this);
            }
        }

        /// <summary>
        /// Draws the or update sections.
        /// </summary>
        public void DrawOrUpdateSections()
        {
            for (var index = 0; index < AxisX.Count; index++)
            {
                var xi = AxisX[index];
                foreach (var section in xi.Sections)
                {
                    section.AxisIndex = index;
                    section.Source = AxisOrientation.X;
                    section.View.DrawOrMove(AxisOrientation.X, index);
                }
            }

            for (var index = 0; index < AxisY.Count; index++)
            {
                var yi = AxisY[index];
                foreach (var section in yi.Sections)
                {
                    section.AxisIndex = index;
                    section.Source = AxisOrientation.Y;
                    section.View.DrawOrMove(AxisOrientation.Y, index);
                }
            }
            
        }

        #endregion

        #region Privates

        private static void SetAxisLimits(AxisCore ax, IList<ICartesianSeries> series, AxisOrientation orientation)
        {
            var first = new CoreLimit();
            var firstR = 0d;

            if (series.Count > 0)
            {
                first = orientation == AxisOrientation.X
                    ? new CoreLimit(series[0].GetMinX(ax), series[0].GetMaxX(ax))
                    : new CoreLimit(series[0].GetMinY(ax), series[0].GetMaxY(ax));
                var view = series[0].View as IAreaPoint;
                firstR = view != null ? view.GetPointDiameter() : 0;
            }

            //                     [ max, min, pointRadius ]
            var boundries = new[] { first.Max, first.Min, firstR };

            for (var index = 1; index < series.Count; index++)
            {
                var cartesianSeries = series[index];
                var limit = orientation == AxisOrientation.X
                    ? new CoreLimit(cartesianSeries.GetMinX(ax), cartesianSeries.GetMaxX(ax))
                    : new CoreLimit(cartesianSeries.GetMinY(ax), cartesianSeries.GetMaxY(ax));
                var view = cartesianSeries.View as IAreaPoint;
                var radius = view != null ? view.GetPointDiameter() : 0;

                if (limit.Max > boundries[0]) boundries[0] = limit.Max;
                if (limit.Min < boundries[1]) boundries[1] = limit.Min;
                if (radius > boundries[2]) boundries[2] = radius;
            }

            ax.TopSeriesLimit = boundries[0];
            ax.BotSeriesLimit = boundries[1];

            ax.TopLimit = double.IsNaN(ax.MaxValue) ? boundries[0] : ax.MaxValue;
            ax.BotLimit = double.IsNaN(ax.MinValue) ? boundries[1] : ax.MinValue;

            ax.MaxPointRadius = boundries[2];
        }

        private void PrepareSeries()
        {
            PrepareUnitWidth();
            PrepareWeight();
            PrepareStackedColumns();
            PrepareStackedRows();
            PrepareStackedAreas();
            PrepareVerticalStackedAreas();
        }

        private void PrepareWeight()
        {
            if (!View.ActualSeries.Any(x => x is IScatterSeriesView || x is IHeatSeriesView)) return;

            var vs = View.ActualSeries
                .Select(x => x.ActualValues.GetTracker(x).WLimit)
                .DefaultIfEmpty(new CoreLimit()).ToArray();
            WLimit = new CoreLimit(vs.Select(x => x.Min).DefaultIfEmpty(0).Min(),
                vs.Select(x => x.Max).DefaultIfEmpty(0).Max());
        }

        private void PrepareUnitWidth()
        {
            foreach (var series in View.ActualSeries)
            {
                if (series is IStackedColumnSeriesView || series is IColumnSeriesView || 
                    series is IFinancialSeriesView || series is IHeatSeriesView)
                {
                    AxisX[series.ScalesXAt].EvaluatesUnitWidth = true;
                }
                if (series is IStackedRowSeriesView || series is IRowSeriesView || series is IHeatSeriesView)
                {
                    AxisY[series.ScalesYAt].EvaluatesUnitWidth = true;
                }
            }
        }

        private void PrepareStackedColumns()
        {
            if (!View.ActualSeries.Any(x => x is IStackedColumnSeriesView)) return;

            var isPercentage =
                View.ActualSeries.Any(x => x is IStackModelableSeriesView && x is IStackedColumnSeriesView &&
                                           ((IStackModelableSeriesView) x).StackMode == StackMode.Percentage);

            foreach (var group in View.ActualSeries.OfType<IStackedColumnSeriesView>().GroupBy(x => x.ScalesYAt))
            {
                StackPoints(group, AxisOrientation.Y, group.Key, isPercentage
                    ? StackMode.Percentage : StackMode.Values);
            }
        }

        private void PrepareStackedRows()
        {
            if (!View.ActualSeries.Any(x => x is IStackedRowSeriesView)) return;

            var isPercentage =
                View.ActualSeries.Any(x => x is IStackModelableSeriesView && x is IStackedRowSeriesView &&
                                     ((IStackModelableSeriesView) x).StackMode == StackMode.Percentage);

            foreach (var group in View.ActualSeries.OfType<IStackedRowSeriesView>().GroupBy(x => x.ScalesXAt))
            {
                StackPoints(group, AxisOrientation.X, group.Key, isPercentage ? StackMode.Percentage : StackMode.Values);
            }
        }

        private void PrepareStackedAreas()
        {
            if (!View.ActualSeries.Any(x => x is IStackedAreaSeriesView)) return;

            var isPercentage =
                View.ActualSeries.Any(x => x is IStackModelableSeriesView && x is IStackedAreaSeriesView &&
                                     ((IStackModelableSeriesView) x).StackMode == StackMode.Percentage);

            foreach (var group in View.ActualSeries.OfType<IStackedAreaSeriesView>().GroupBy(x => x.ScalesYAt))
            {
                StackPoints(group, AxisOrientation.Y, group.Key, isPercentage ? StackMode.Percentage : StackMode.Values);
            }
        }

        private void PrepareVerticalStackedAreas()
        {
            if (!View.ActualSeries.Any(x => x is IVerticalStackedAreaSeriesView)) return;

            var isPercentage =
                View.ActualSeries.Any(x => x is IStackModelableSeriesView && x is IVerticalStackedAreaSeriesView &&
                                     ((IStackModelableSeriesView) x).StackMode == StackMode.Percentage);

            foreach (var group in View.ActualSeries.OfType<IVerticalStackedAreaSeriesView>().GroupBy(x => x.ScalesXAt))
            {
                StackPoints(group, AxisOrientation.X, group.Key, isPercentage ? StackMode.Percentage : StackMode.Values);
            }
        }

        #endregion
    }
}
