/// *********************************************************************************************************
///  © 2014 www.jakemdrew.com All rights reserved. 
///  This source code is licensed under The GNU General Public License (GPLv3):  
///  http://opensource.org/licenses/gpl-3.0.html
/// *********************************************************************************************************

/// *********************************************************************************************************
/// ImagesDistanceMatrix - MapReduce An Image Distance Matrix in Parallel.
///
/// Created By - Jake Drew 
/// Version -    1.0, 06/23/2014
/// *********************************************************************************************************

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ImageClustering
{
    /// 
    /// This class hold all information for a single pairwise distance match within the matrix.
    /// 
    public class PairwiseMatch
    {
        public int rowIndex;
        public int colIndex;
        public RgbProjection projectionR;
        public RgbProjection projectionC;
        public PairwiseMatch(int row, int column,RgbProjection pR, RgbProjection pC)
        {
            rowIndex = row;
            colIndex = column;
            projectionR = pR;
            projectionC = pC;
        }
        public PairwiseMatch(int row, int column)
        {
            rowIndex = row;
            colIndex = column;
        }
    }

    /// 
    /// Creates a pairwise distance matrix from an input directory of images.
    /// 
    public class ImagesDistanceMatrix
    {
        
        public string[] filePaths;
        public double[][] matrixRows;

        //We save the RGB projection features for each image since they are used multiple times 
        public ConcurrentDictionary imageFeatures = new ConcurrentDictionary();

        BlockingCollection MatrixMatches = new BlockingCollection(new ConcurrentQueue());
        BlockingCollection ImageFeatures = new BlockingCollection(new ConcurrentQueue());

        public void createImageDistanceMatrix(string inputDirLoc, string outputFileLoc)
        {
            File.Delete(outputFileLoc);

            //get all of the image files in a directory
            filePaths = Directory.GetFiles(inputDirLoc,"*.png");
            matrixRows = new double[filePaths.Length][];

            //initialize matrix array
            for (int i = 1; i < matrixRows.Length; i++)
            {
                matrixRows[i] = new double[i];
            }

            //Produce / map pairwise matches in a background thread.
            Task.Run(() => createPairwiseMatches(inputDirLoc));

            //Consume /Reduce pairwise matches into matrix distances at the same time.
            calculateDistances(outputFileLoc);

            //We are down with all of the features at this point.
            imageFeatures.Clear();

            //Write the distance matrix to a csv 
            createMatrix(outputFileLoc, matrixRows);
        }

        /// 
        /// Rapidly generate all required matrix matches in the background for processing.
        /// (this allows a much more balanced execution of Parallel.ForEach on all matches)
        /// Yeild return passes each match to parallel foreach in createPairwiseMatches()
        /// as they are produced.
        /// 
        /// 
        private IEnumerable generateMatches()
        {
            for (int r = 1; r < filePaths.Length; r++)
            {
                //for each column less than the current row
                for (int c = 0; c < r; c++)
                {
                    yield return new PairwiseMatch(r, c);
                }
            }
        }

        /// 
        /// Parallel asynchronous background process to create all pairwise matches for a 
        /// given matrix, extracting image features one time (as required).
        /// 
        /// 
        public void createPairwiseMatches(string inputDirLoc)
        {
            Parallel.ForEach(generateMatches(), match =>
            {
                match.projectionR = getImageFeatures(filePaths[match.rowIndex]);
                match.projectionC = getImageFeatures(filePaths[match.colIndex]);
                MatrixMatches.Add(match);
            });

            MatrixMatches.CompleteAdding();
        }

        /// 
        /// Asynchronous background process to extract rgb projections (when needed) 
        /// and calculate similarity between images in parallel.
        /// 
        public void calculateDistances(string outputFileLoc)
        {
            Parallel.ForEach(MatrixMatches.GetConsumingEnumerable(), matrixMatch =>
            {
                //Calculate similarity between two images and save it to the correct matrix row.
                double similarity = RgbProjector.CalculateSimilarity(matrixMatch.projectionR, matrixMatch.projectionC);
                double distance = 1 - similarity;
                matrixRows[matrixMatch.rowIndex][matrixMatch.colIndex] = distance;
            });
        }

        public void createMatrix(string outputFileLoc, double[][] matrixRows)
        {
            StringBuilder matrix = new StringBuilder();
            
            //Create header and 1st row
            matrix.AppendLine(createHeaderRow());

            int rowIndex = 0;
            foreach (var row in matrixRows)
            {
                //matrix.Append("\"" + Path.GetFileNameWithoutExtension(filePaths[rowIndex]) + "\"");
                matrix.Append("\"" + filePaths[rowIndex] + "\"");

                if (row != null)
                { 
                    foreach (var rowValue in row)
	                {
                        matrix.Append(",\"" + rowValue + "\"");   
	                }

                    matrixRows[rowIndex] = null;
                }

                rowIndex++;
                matrix.Append("\r\n");

                //write output in blocks of 2500 characters.
                if (matrix.Length >= 2500)
                {
                    File.AppendAllText(outputFileLoc, matrix.ToString());
                    matrix.Clear();
                }
            }

            //write any remaining text to the output file.
            File.AppendAllText(outputFileLoc,matrix.ToString());
        }

        public string createHeaderRow()
        {
            StringBuilder row = new StringBuilder();
            
            row.Append("www.jakemdrew.com");

            foreach (var filePath in filePaths)
            {
                //row.Append("," + "\"" + Path.GetFileNameWithoutExtension(filePath) + "\"");
                row.Append("," + "\"" + filePath + "\"");
            }
            return row.ToString();
        }

        public RgbProjection getImageFeatures(string imagePath)
        {
            RgbProjection imageProjections;

            if (imageFeatures.TryGetValue(imagePath.GetHashCode(), out imageProjections))
            {
                return imageProjections;
            }
            else
            {
                using (var bitmap = RgbProjector.ResizeBitmap(new Bitmap(imagePath), 1000, 1000))
                {
                    imageProjections = RgbProjector.GetRgbProjections(bitmap);
                    imageFeatures.TryAdd(imagePath.GetHashCode(), imageProjections);    
                }

                return imageProjections;
            }
        }
    }
}