using System;
using System.Collections.Generic;
using System.Linq;

using LabelerCore.Model.Math;

namespace LabelerCore.Model.Classifiers
{
    public class NearestCentroid: IClassifier
    {
        protected double[][] _centroids; //n_classes x n_features
        protected int[] _classes;

        //distance type to use
        protected Metrics.Metric _metric;
        protected Metrics.Distance _distance;

        public int[] Classes { get { return _classes; } }

        public NearestCentroid(Metrics.Metric metric = Metrics.Metric.SquaredEuclidean)
        {
            this._metric = metric;
            this._distance = Metrics.SelectDistance(metric);
        }

        protected void _CheckFitness()
        {
            if (this._centroids == null)
            {
                throw new NullReferenceException("Nearest Centroid Classifier is not fitted yet");
            }
        }

        public void Fit(double[][] data, int[] targets)
        {
            CheckInputs.CheckNumberOfSamples(data, targets);
            //get unique classes
            _classes = new HashSet<int>(targets).OrderBy(x => x).ToArray();
            //cmp centroids
            _centroids = new double[_classes.Length][];
            for(int i = 0; i < _classes.Length; i++)
            {
                int[] classInd = targets.Select((val, idx) => new KeyValuePair<int, int>(val, idx))
                                        .Where(x => x.Key == _classes[i])
                                        .Select(x => x.Value)
                                        .ToArray();
                double[][] X = Utils.Select(data, classInd);
                _centroids[i] = Math.Stats.Mean(X);
            }
        }

        public int[] Predict(double[][] data)
        {
            _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(_centroids, 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(_centroids, data);

            //compute distances between input sample and training samples
            double[] distanceArr = _centroids.Select(x => this._distance(data, x))
                                             .ToArray();
            //argmin
            int idx = Stats.ArgKSmallest(distanceArr, 1)[0];
            return _classes[idx];
        }

        public double[] CentroidsDistance(double[] data)
        {
            _CheckFitness();
            CheckInputs.CheckNumberOfFeatures(_centroids, data);

            //compute distances between input sample and training samples
            double[] distanceArr = _centroids.Select(x => this._distance(data, x))
                                             .ToArray();
            return distanceArr;
        }

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

    }
}
