using System;
using System.Linq;
using LabelerCore.Model.Math;

namespace LabelerCore.Model.Classifiers
{
    public class KNNClassifier : IClassifier, INearestNeighbor 
    {
        //stores the training data, shape (number of training samples, number of features)
        //TODO: think about rectangular array, list?
        protected double[][] _data;
        //stores the training targets
        protected int[] _targets;
        //number of neighbors to use
        protected UInt16 _numberOfNeighbors;
        //distance type to use
        protected Metrics.Metric _metric;
        protected Metrics.Distance _distance;


        public KNNClassifier(UInt16 neighbors = 3,
                             Metrics.Metric metric = Metrics.Metric.SquaredEuclidean)
        {
            this._numberOfNeighbors = neighbors;
            this._metric = metric;
            this._distance = Metrics.SelectDistance(metric);
        }

        protected void _CheckFitness()
        {
            if (this._data == null)
            {
                throw new NullReferenceException("KNNClassifier is not fitted yet");
            }
        }

        //Fits the classifier with training data and targets
        public void Fit(double[][] data, int[] targets)
        {
            CheckInputs.CheckNumberOfSamples(data, targets);
            this._data = data;
            this._targets = targets;
        }

        //Predicts class labels for the provided data
        //data shape (number of samples, number of features)
        public int[] Predict(double[][] data)
        {
           _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(data, this._data);

            int[] targetsPred = new int[data.Length];
            for (int i = 0; i < data.Length; i++)
            {
                targetsPred[i] = this.Predict(data[i]);
            }
            return targetsPred;
        }

        public int Predict(double[] data)
        {
           _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(this._data, data);

            //compute distances between input sample and training samples
            double[] distanceArr = this._data.AsParallel()
                                             .AsOrdered()
                                             .Select(x => this._distance(data, x))
                                             .ToArray();
            //find k neighbors
            int[] kNeighbors = Stats.ArgKSmallest(distanceArr, this._numberOfNeighbors)
                                      .Select(x => this._targets[x]).ToArray();
            //voting
            return Stats.Mode(kNeighbors);
        }

        //returns array of tuples, each tuples first item is k indices array of nearest samples
        //and second item is array of k distances
        public Tuple<int[], double[]>[] KNeighbors(double[][] data)
        {
           _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(this._data, data);
            var ret = new Tuple<int[], double[]>[data.Length];
            for (int i = 0; i < data.Length; i++)
                ret[i] = KNeighbors(data[i]);
            return ret;
        }

        public Tuple<int[], double[]> KNeighbors(double[] data)
        {
            return KNeighbors(data, this._numberOfNeighbors);
        }

        public Tuple<int[], double[]> KNeighbors(double[] data, ushort k)
        {
            _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(this._data, data);
            //compute distances between input sample and training samples
            double[] distanceArr = this._data.AsParallel()
                                             .AsOrdered()
                                             .Select(x => this._distance(data, x))
                                             .ToArray();
            //find k neighbors
            int[] kNeighbors = Stats.ArgKSmallest(distanceArr, k).ToArray();
            double[] kDists = new double[k];
            for (int i = 0; i < kNeighbors.Length; i++)
                kDists[i] = distanceArr[kNeighbors[i]];
            return new Tuple<int[], double[]>(kNeighbors, kDists);
        }

        public Tuple<int, double>[] NearestNeighbor(double[][] X)
        {
            Tuple<int, double>[] ret = new Tuple<int, double>[X.Length];
            for (int i = 0; i < X.Length; i++)
            {
                Tuple<int[], double[]> dummy =  KNeighbors(X[i], 1);
                ret[i] = new Tuple<int, double>(dummy.Item1[0], dummy.Item2[0]);
            }
            return ret;
        }

        public Tuple<int, double> NearestNeighbor(double[] x)
        {
            return NearestNeighbor(new double[][] { x })[0];
        }

        public object Clone()
        {
            return this.MemberwiseClone();
        }

    }
}
