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

namespace LabelerCore.Model.CrossValidation
{
    public class Report
    {
        //key - class name
        private Dictionary<string, double> _precision;
        private Dictionary<string, double> _recall;
        private Dictionary<string, double> _f1Score;

        //1 dimension is true label, and 2 dimention is predicted label 
        public readonly int[,] ConfusionMatrix;
        //classes shoud have same order as confusion matrix rows and cols 
        public readonly List<string> Classes;


        public Report(int[,] ConfusionMatrix, List<string> iClasses=null)
        {

            this.ConfusionMatrix = ConfusionMatrix;
            if(iClasses != null)
                this.Classes = iClasses;
            else {
                this.Classes = new List<string>(ConfusionMatrix.GetLength(0));
                for (int i = 0; i < this.Classes.Count; i++)
                    this.Classes[i] = i.ToString();
            }
            //compute errors
            int n = ConfusionMatrix.GetLength(0);
            _precision = new Dictionary<string, double>(Classes.Count);
            _recall = new Dictionary<string, double>(Classes.Count);
            _f1Score = new Dictionary<string, double>(Classes.Count);
            foreach (string cl in Classes)
            {
                int idx = Classes.IndexOf(cl);
                int TP = ConfusionMatrix[idx, idx];
                int FP = -TP;
                for (int i = 0; i < n; i++)
                    FP += ConfusionMatrix[i, idx];
                int FN = -TP;
                for (int i = 0; i < n; i++)
                    FN += ConfusionMatrix[idx, i];
                int TN = -(TP + FP + FN);
                for (int i = 0; i < n; i++)
                    for (int j = 0; j < n; j++)
                        TN += ConfusionMatrix[i, j];


                double p = TP / (double)(TP + FP);
                double r = TP / (double)(TP + FN);
                _precision.Add(cl, p);
                _recall.Add(cl, r);
                _f1Score.Add(cl, 2 * p * r / (p + r));


            }
        }

        public double getPrecision(string vehicleClass)
        {
            return _precision[vehicleClass];
        }

        public double getRecall(string vehicleClass)
        {
            return _recall[vehicleClass];
        }

        public double getF1Score(string vehicleClass)
        {
            return _f1Score[vehicleClass];
        }

        public double getF1Macro()
        {
            double ret = 0;
            foreach (string cl in _f1Score.Keys)
                ret += double.IsNaN(_f1Score[cl]) ? 0.0 : _f1Score[cl];
            return ret / _f1Score.Keys.Count;
        }

        public string ToCsv(string delimiter = ";")
        {
            StringBuilder sb = new StringBuilder();
            string row = "Confusion matrix (1 dimension is true label, and 2 dimension is predicted label): ";
            sb.AppendLine(row);
            //write confusion matrix
            //first row - list of classes
            row = delimiter + string.Join(delimiter, Classes);
            sb.AppendLine(row);
            for (int i = 0; i < ConfusionMatrix.GetLength(0); i++)
            {
                //first column - list of classes
                row = Classes[i];
                for (int j = 0; j < ConfusionMatrix.GetLength(1); j++)
                {
                    row += delimiter + ConfusionMatrix[i, j];
                }
                sb.AppendLine(row);
            }
            sb.AppendLine("");
            sb.AppendLine("");
            //write statistics
            row = "Statistics";
            sb.AppendLine(row);
            //write header
            row = "Class" + delimiter +
                  "Precision" + delimiter +
                  "Recall" + delimiter +
                  "F1 score" + delimiter +
                  "Count";
            sb.AppendLine(row);
            for (int i = 0; i < Classes.Count; i++)
            {
                row = Classes[i] + delimiter;
                row += getPrecision(Classes[i]).ToString("F4") + delimiter;
                row += getRecall(Classes[i]).ToString("F4") + delimiter;
                row += getF1Score(Classes[i]).ToString("F4") + delimiter;
                //compute samples count
                int count = ConfusionMatrix.Cast<int>().Skip(i * ConfusionMatrix.GetLength(1))
                                                        .Take(ConfusionMatrix.GetLength(1))
                                                        .ToArray()
                                                        .Sum();
                row += count.ToString();
                sb.AppendLine(row);

            }
            return sb.ToString();
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            //write statistics
            sb.AppendLine("Statistics:\n");
            //write header
            sb.AppendLine(string.Format("{0, 10} {1, 10} {2, 10} {3, 10} {4, 10}", 
                                          "Class",
                                          "Precision", 
                                          "Recall",
                                          "F1 score", 
                                          "Count"));

            for (int i = 0; i < Classes.Count; i++)
            {
                //compute samples count
                int count = ConfusionMatrix.Cast<int>().Skip(i * ConfusionMatrix.GetLength(1))
                                                        .Take(ConfusionMatrix.GetLength(1))
                                                        .ToArray()
                                                        .Sum();

                sb.AppendLine(string.Format("{0, 10} {1, 10} {2, 10} {3, 10} {4, 10}",
                                              Classes[i],
                                              getPrecision(Classes[i]).ToString("F4"),
                                              getRecall(Classes[i]).ToString("F4"),
                                              getF1Score(Classes[i]).ToString("F4"),
                                              count.ToString()));
            }
            sb.AppendLine("\nF1 macro: " + this.getF1Macro().ToString("F4"));
            sb.AppendLine("\n\nConfusion matrix (1 dimension is true label, and 2 dimension is predicted label):");
            //print confusion matrix

            for (int i = 0; i < ConfusionMatrix.GetLength(0); i++)
            {
                for (int j = 0; j < ConfusionMatrix.GetLength(1); j++)
                    sb.Append((string.Format("\t{0}", ConfusionMatrix[i, j])));
                sb.AppendLine();
            }
            return sb.ToString();
        }

    }
}
