// Created with MonoDevelop
//
//    Copyright (C) 2008 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

using Hiperscan.Unix;


namespace Hiperscan.Spectroscopy
{

    public enum DataType
    {
        Intensity,
        Absorbance,
        Reference,
        Correction,
        Preview,
        Reflectance
    }
    
    public enum InterpolationType
    {
        Linear,
        NaturalCubic
    }

    [DataContract(Name = "DataSet", Namespace = "http://www.hiperscan.com/spectroscopy")] [Serializable()]
    public partial class DataSet : System.Runtime.Serialization.IExtensibleDataObject
    {
        // default values are defined in Initialize() since DataContracts ignore default constructors
        [DataMember] public List<double> X    { get; private set; }
        [DataMember] public List<double> Y    { get; private set; }
        [DataMember] public List<int> Gaps    { get; set; }
        [DataMember] public string Id         { get; set; }
        [DataMember] public string Label      { get; set; } = string.Empty;
        [DataMember] public DataType DataType { get; set; }

        public bool IsContinuous       { get; set; }
        public double RegionFillLimit  { get; set; }
        public bool IsVisible          { get; set; }
        public bool IsBackground       { get; set; }
        public Color Color             { get; set; }
        public float LineWidth         { get; set; }
        public LineStyle LineStyle     { get; set; }
        public MarkerStyle MarkerStyle { get; set; }
        public int ZOrder              { get; set; }
        public string XLabel           { get; set; }
        public string YLabel           { get; set; }
        public Guid Guid               { get; private set; }

        [NonSerialized] private ExtensionDataObject extension_data;
        [NonSerialized] private bool is_drop_point;
        [NonSerialized] private bool is_region;
        [NonSerialized] private bool? is_1nm_equidistant;
        [NonSerialized] private double y_min;
        [NonSerialized] private double y_max;
        [NonSerialized] private double __ymin;
        [NonSerialized] private double __xmin;
        [NonSerialized] private double __xmax;
        [NonSerialized] private double __ymax;

        [NonSerialized] private const double EPSILON = 1e-50;

        internal DataSet()
        {
            this.Initialize();
        }

        internal DataSet(int capacity)
        {
            this.Initialize(capacity);
        }

        public DataSet(IEnumerable<double> x, IEnumerable<double> y) : this(x.Count())
        {
            if (x.Count() != y.Count())
                throw new System.ArgumentException(Catalog.GetString("Lists in a data sets must have the same element number."));

            if (x.Count() < 2)
                throw new System.ArgumentException(Catalog.GetString("Lists in a data sets must have an element number of at least 2."));

            this.X.AddRange(x);
            this.Y.AddRange(y);
        }

        public DataSet(IEnumerable<float> x, IEnumerable<float> y) : this(x.Select(v => (double)v), y.Select(v => (double)v))
        {
        }

        public DataSet(IEnumerable<double> x, double y) : this(x.Count())
        {
            if (x.Count() < 2)
                throw new System.ArgumentException(Catalog.GetString("Lists in a data sets must have an element number of at least 2."));

            this.X.AddRange(x);
  
            for (int ix = 0; ix < x.Count(); ++ix)
                this.Y.Add(y);
        }

        public DataSet(DataSet ds) : this(ds.Count)
        {
            this.X.AddRange(ds.X);
            this.Y.AddRange(ds.Y);
            this.Gaps.AddRange(ds.Gaps);
        }
        
        [OnDeserializing]
        private void SetValuesOnDeserializing(StreamingContext context)
        {
            this.Initialize();
        }

        private void Initialize(int capacity = 0)
        {
            this.Label = string.Empty;
            this.IsContinuous = true;
            this.RegionFillLimit = 0.0;
            this.IsVisible = true;
            this.IsBackground = false;
            this.Color = Color.Empty;
            this.LineWidth = 2f;
            this.LineStyle = LineStyle.Solid;
            this.MarkerStyle = MarkerStyle.Cross1;
            this.ZOrder = int.MaxValue;
            this.XLabel = string.Empty;
            this.YLabel = string.Empty;

            this.is_drop_point = false;
            this.is_region = false;
            this.is_1nm_equidistant = null;
            this.y_min = double.NaN;
            this.y_max = double.NaN;
            this.__xmin = double.NaN;
            this.__xmax = double.NaN;
            this.__ymin = double.NaN;
            this.__ymax = double.NaN;

            this.Guid = Guid.NewGuid();

            this.X = new List<double>(capacity);
            this.Y = new List<double>(capacity);

            this.Gaps = new List<int>();

            this.DataType = DataType.Intensity;

            this.Id = "data_set_" + Guid.NewGuid().ToString();
        }

        public void SetYMin(double y_min)
        {
            this.y_min = y_min;
        }
        
        public void SetYMax(double y_max)
        {
            this.y_max = y_max;
        }

        public void Add(double x, double y)
        {
            this.X.Add(x);
            this.Y.Add(y);
        }
        
        public void AddRange(DataSet ds)
        {
            this.X.AddRange(ds.X);
            this.Y.AddRange(ds.Y);
        }

        public void RemoveAt(int ix)
        {
            if (this.Count < 3)
                throw new Exception(Catalog.GetString("Cannot remove element if the resulting element number is less than 2."));
            
            this.X.RemoveAt(ix);
            this.Y.RemoveAt(ix);
        }
        
        public void RemoveAt(List<int> indices)
        {
            indices.Sort();
            indices.Reverse();
            foreach (int ix in indices)
            {
                this.RemoveAt(ix);
            }
        }
        
        public void RemoveRange(int ix, int count)
        {
            if ((this.Count - count) < 2)
                throw new System.Exception(Catalog.GetString("Cannot remove field if the resulting element number is less than 2."));
                
            this.X.RemoveRange(ix, count);
            this.Y.RemoveRange(ix, count);
        }
        
        public DataSet GetRange(int index, int count)
        {
            return new DataSet(this.X.GetRange(index, count), this.Y.GetRange(index, count));
        }

        public DataSet GetRange(double lower, double upper)
        {
            return this.GetRange(this.FindNearestIndex(lower), this.FindNearestIndex(upper)-this.FindNearestIndex(lower)+1);
        }

        public void Sort()
        {
            List<double> x = new List<double>(this.X);
            List<double> y = new List<double>(this.X.Count);
            
            x.Sort();
            foreach (double val in x)
            {
                int ix = this.X.IndexOf(val);
                y.Add(this.Y[ix]);
            }
            this.X = x;
            this.Y = y;            
        }

        public IEnumerable<Tuple<double,double>> AsEnumerable()
        {
            for (int ix = 0; ix < this.Count; ++ix)
            {
                yield return new Tuple<double,double>(this.X[ix], this.Y[ix]);
            }
        }

        public void CopyPropertiesFrom(Spectrum spectrum)
        {
            this.Label       = spectrum.Label;
            this.Id          = spectrum.Id;
            this.Color       = spectrum.Color;
            this.LineStyle   = spectrum.LineStyle;
            this.MarkerStyle = spectrum.MarkerStyle;
            this.LineWidth   = spectrum.LineWidth;
            this.IsVisible   = spectrum.IsVisible;
            this.ZOrder      = spectrum.ZOrder;
        }
        
        public void CopyPropertiesFrom(DataSet ds)
        {
            this.Label       = ds.Label;
            this.Id          = ds.Id;
            this.Color       = ds.Color;
            this.LineStyle   = ds.LineStyle;
            this.MarkerStyle = ds.MarkerStyle;
            this.LineWidth   = ds.LineWidth;
            this.IsVisible   = ds.IsVisible;
            this.ZOrder      = ds.ZOrder;
        }

        public override int GetHashCode()
        {
            int hash = 0;
            foreach (double val in this.X)
            {
                hash ^= val.GetHashCode();
            }
            foreach (double val in this.Y)
            {
                hash ^= val.GetHashCode();
            }
            return hash;
        }

        public override bool Equals(object obj)
        {
            if (!(obj is DataSet ds))
                return false;

            return this.X.SequenceEqual(ds.X) && this.Y.SequenceEqual(ds.Y);
        }

        public int Count
        {
            get { return this.X.Count; }
        }

        public bool IsDropPoint
        {
            get { return this.IsContinuous == false && this.is_drop_point; }
            set { this.is_drop_point = value; }
        }

        public bool IsRegion
        {
            get { return this.IsContinuous && this.is_region; }
            set { this.is_region = value; }
        }

        public bool IsPreview
        {
            get { return this.Id == "9_preview"; }
        }

        public bool IsValid
        {
            get { return this.Count >= 2; }
        }

        public bool Is1nmEquidistant
        {
            get
            {
                if (this.is_1nm_equidistant == null)
                {
                    this.is_1nm_equidistant = true;
                    for (int ix = 0; ix < this.Count-1; ++ix)
                    {
                        if (System.Math.Abs((this.X[ix + 1] - this.X[ix]) - 1.0) > EPSILON)
                        {
                            Console.WriteLine($"WARNING: DataSet is not 1nm-equidistant: {this.Id} ({(this.X[ix + 1]-this.X[ix])} != 1.0");
                            this.is_1nm_equidistant = false;
                            break;
                        }
                    }
                }
                
                return (bool)this.is_1nm_equidistant;
            }
        }

        // indexer for collections
        public DataSet this[List<int> indices]
        {
            get
            {
                DataSet ds = new DataSet(indices.Count);
                
                for (int ix=0; ix < indices.Count; ++ix)
                {
                    ds.Add( this.X[indices[ix]], this.Y[indices[ix]] );
                }
                return ds;
            }
            set
            {
                for (int ix=0; ix < indices.Count; ++ix)
                {
                    this.Y[indices[ix]] = value.Y[ix];
                }
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("[DataSet: X=[");
            foreach (double val in this.X)
            {
                sb.AppendFormat(" {0,8}", val.ToString("F3"));
            }

            sb.Append("]\n          Y=[");
            foreach (double val in this.Y)
            {
                sb.AppendFormat(" {0,8}", val.ToString("F3"));
            }

            sb.Append("]]");
            return sb.ToString();
        }
        
        public string ToCsvString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("X:");
            foreach (double val in this.X)
            {
                sb.AppendFormat(CultureInfo.InvariantCulture, ",{0}", val);
            }

            sb.Append("\nY:");
            foreach (double val in this.Y)
            {
                sb.AppendFormat(CultureInfo.InvariantCulture, ",{0}", val);
            }

            return sb.ToString();
        }

        public bool ContainsNaN
        {
            get
            {
                bool contains = false;
                Parallel.For(0, this.Count, (index, state) =>
                {
                    if (double.IsNaN(this.Y[index]) || double.IsNaN(this.X[index]))
                    {
                        contains = true;
                        state.Break();
                    }
                });

                return contains;
            }
        }

        public static double[] GetDefaultSystemFunctionCorrectionCoefficients()
        {
            // FIXME: implement real system function correction
            return Get11nmFirLowpassCoefficients();
        }

        public bool IsIncreasingX
        {
            get { return this.X.First() < this.X.Last(); }
        }

        public bool IsIncreasingY
        {
            get { return this.Y.First() < this.Y.Last(); }
        }

        public ExtensionDataObject ExtensionData
        {
            get { return this.extension_data;  }
            set { this.extension_data = value; }
        }
    }
}
