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

namespace OCR
{
    internal static class Segmentation
    {
        /// <summary>
        /// Segments text boxes into individual characters
        /// </summary>
        /// <param name="image">binary image</param>
        /// <param name="rectangles">list of rectangles containing text</param>
        /// <returns>list of character bounding boxes</returns>
        public static List<CharacterBoundingBox> GetCharacterBoundingBoxes(Mat image, List<Rect> rectangles)
        {
            List<CharacterBoundingBox> characterBoundingBoxes = new List<CharacterBoundingBox>();
            for (var i = 0; i < rectangles.Count; i++) {
                Rect rect = rectangles[i];
                Mat cropped = image.SubMat(rect).Clone();
                
                //separation of lines
                List<Rect> cropLineRects = GetLineRectangles(cropped, 254, rect);
                foreach (var lineRectangle in cropLineRects) {
                    Mat croppedLine = image.SubMat(lineRectangle).Clone();
                    if (!IsRectangleValid(lineRectangle, croppedLine, false))
                        continue;
                    //separation of characters
                    List<SeparationArea> cropPlacesChars = GetCharCropPlaces(croppedLine, 244);
                    //no crop places
                    if (cropPlacesChars.Count == 0) {
                        //validate rectangle for single character
                        if (IsRectangleValid(lineRectangle, croppedLine, true)) {
                            characterBoundingBoxes.Add(new CharacterBoundingBox {
                                RlsaBoundingBoxIndex = i,
                                BoundingBox = lineRectangle,
                                IsLast = true
                            });
                        }
                        continue;
                    }
                    for (int j = 0; j < cropPlacesChars.Count; j++) {
                        int x = (j == 0) ? lineRectangle.X : (lineRectangle.X + cropPlacesChars[j - 1].Position);
                        int width = (j == 0)
                            ? cropPlacesChars[j].Position
                            : (cropPlacesChars[j].Position - cropPlacesChars[j - 1].Position);
                        //extracted character with added border of 2 pixels
                        Rect extractedCharacterRect = new Rect(x, lineRectangle.Y, width, lineRectangle.Height);
                        if (width >= 3 && width <= 40 && IsRectangleValid(extractedCharacterRect, image.SubMat(extractedCharacterRect).Clone(), true)) {
                            characterBoundingBoxes.Add(new CharacterBoundingBox {
                                RlsaBoundingBoxIndex = i,
                                BoundingBox = extractedCharacterRect,
                                IsLast = cropPlacesChars[j].SeparationType == SeparationType.MidWord
                            });
                        } else {
                            if (characterBoundingBoxes.Any())
                                characterBoundingBoxes.Last().IsLast = true;
                        }
                    }
                    Rect lastCharacter = new Rect(lineRectangle.X + cropPlacesChars.Last().Position, lineRectangle.Y,
                        lineRectangle.Width - cropPlacesChars.Last().Position, lineRectangle.Height);
                    if (!IsRectangleValid(lastCharacter, image.SubMat(lastCharacter).Clone(), true)) {
                        if (characterBoundingBoxes.Any())
                            characterBoundingBoxes.Last().IsLast = true;
                        continue;
                    }
                    characterBoundingBoxes.Add(new CharacterBoundingBox {
                        RlsaBoundingBoxIndex = i,
                        BoundingBox = lastCharacter,
                        IsLast = true
                    });
                    }
                }
            /*string str = DateTime.Now.ToString("yyyy-MM-dd_hhmmss");
            string fileName = @"D:\diplomka\text\finalni obrazky\" + str + ".png";
            showRectangles.SaveImage(fileName);*/
            return characterBoundingBoxes;
        }


        /// <summary>
        /// Crops text box into individual text lines
        /// </summary>
        /// <param name="cropped">text box cropped from binary image</param>
        /// <param name="threshold">threshold for cropping</param>
        /// <param name="rectangle">text box rectangle</param>
        /// <returns>list of rectangles containing only one line of original text box</returns>
        private static List<Rect> GetLineRectangles(Mat cropped, int threshold, Rect rectangle)
        {
            List<Rect> lineCropRects = new List<Rect>();
            Mat reducedCols = new Mat();
            Cv2.Reduce(cropped.Clone().Transpose(), reducedCols, ReduceDimension.Row, ReduceTypes.Avg, -1);
            Cv2.Threshold(reducedCols, reducedCols, threshold, 255.0, ThresholdTypes.Binary);
            Mat nonZero = reducedCols.FindNonZero();
            if (nonZero.Empty()) {
                lineCropRects.Add(rectangle);
            } else {
                int start = (nonZero.At<int>(0) == 0) ? 1 : 0;
                int first = nonZero.At<int>(start);
                int count = nonZero.Rows;
                List<int> cropPlace = new List<int>();
                cropPlace.Add(0);
                for (int i = start; i < count; i++) {
                    if (nonZero.At<int>(i + 1) - nonZero.At<int>(i) > 1 || (i == (count - 1))) {
                        var diff = nonZero.At<int>(i) - first;
                        cropPlace.Add(first + (diff / 2));
                        first = nonZero.At<int>(i + 1);
                    }
                }
                if (cropPlace.Last() != reducedCols.Cols-1)
                    cropPlace.Add(reducedCols.Cols-1);
                if (cropPlace[1]<10 || cropPlace.Last()-cropPlace[cropPlace.Count-2]<10) {
                    lineCropRects.Add(rectangle);
                    return lineCropRects;
                }
                for (int j = 0; j < cropPlace.Count-1; j++) {
                    int y = rectangle.Y + cropPlace[j];
                    int height = cropPlace[j + 1] - cropPlace[j];
                    Rect newRectLine = new Rect(rectangle.X, y, rectangle.Width, height);
                    if (newRectLine.Height>10)
                        lineCropRects.Add(newRectLine);
                }
            }
            return lineCropRects;
        }


        /// <summary>
        /// Finds places to crop characters
        /// </summary>
        /// <param name="cropped">line of text cropped from binary image</param>
        /// <param name="threshold">threshold for cropping</param>
        /// <returns>list of separation areas</returns>
        private static List<SeparationArea> GetCharCropPlaces(Mat cropped, int threshold)
        {
            List<SeparationArea> cropPlaces = new List<SeparationArea>();
            Mat reducedRows = new Mat();
            Cv2.Reduce(cropped, reducedRows, ReduceDimension.Row, ReduceTypes.Avg, -1);
            Cv2.Threshold(reducedRows, reducedRows, threshold, 255.0, ThresholdTypes.Binary);
            var indexes = new List<int>();
            for (int i = 0; i < reducedRows.Cols; i++) {
                if (reducedRows.Get<byte>(0, i) > 0) {
                    indexes.Add(i);
                }
            }
            if (indexes.Count != 0) {
                if (indexes.Count == 1 && indexes.First() == 0)
                    return new List<SeparationArea>();
                var gapSize = GetAverageGapSize(indexes);
                int start = indexes[0] == 0 ? 2 : 1;
                int first = indexes[start-1];
                int count = indexes.Count;
                for (int i = start; i < count; i++) {
                    if (indexes[i] - indexes[i-1] > 1) {
                        var diff = indexes[i-1] - first + 1;
                        var separation = diff >= Math.Round(gapSize * 2) ? SeparationType.MidWord : SeparationType.MidChar;
                        cropPlaces.Add(new SeparationArea {
                            Position = first + diff / 2,
                            SeparationType = separation
                        });
                        first = indexes[i];
                    }
                }
                var diffLast = indexes.Last() - first;
                var separationLast = diffLast >= Math.Round( gapSize * 2) ? SeparationType.MidWord : SeparationType.MidChar;
                cropPlaces.Add(new SeparationArea {
                    Position = first + diffLast/2,
                    SeparationType = separationLast
                });
            }
            return cropPlaces;
        }


        /// <summary>
        /// Finds the average gap size
        /// </summary>
        /// <param name="nonZeros">list of indexes under threshold(crop places)</param>
        /// <returns>average gap size</returns>
        private static double GetAverageGapSize(List<int> nonZeros)
        {
            if (nonZeros.Count == 1)
                return 1;
            int start = nonZeros[0] == 0 ? 2 : 1;
            int first = nonZeros[start-1];
            var gapSize = new List<int>();
            for (int i = start; i < nonZeros.Count; i++) {
                if (nonZeros[i] - nonZeros[i-1]>1) {
                    gapSize.Add(nonZeros[i-1]-first+1);
                    first = nonZeros[i];
                }
            }
            gapSize.Add(nonZeros.Last() - first + 1);
            gapSize.Sort();
            var median = gapSize[gapSize.Count / 2];
            var mean = gapSize.Min() < 15 ? gapSize.Where(x => x < 15).Average() : gapSize.Min();
            var decision = Math.Abs(mean - median) > Math.Min(mean, median) ? median : mean;
            return decision;
        }


        /// <summary>
        /// Validates the rectangle according to its size and content
        /// </summary>
        /// <param name="rectangle"></param>
        /// <param name="croppedRect">rectangle area cropped from binary image</param>
        /// <param name="singleChar">is rectangle of single character</param>
        /// <returns></returns>
        private static bool IsRectangleValid(Rect rectangle, Mat croppedRect, bool singleChar)
        {
            if (singleChar) {
                Mat reverted = croppedRect.EmptyClone();
                Cv2.BitwiseNot(croppedRect, reverted);
                Mat nonZeros = new Mat();
                Cv2.FindNonZero(reverted, nonZeros);
                if (nonZeros.Empty())
                    return false;
                var boundingRect = Cv2.BoundingRect(nonZeros);
                Mat resizedCharacter = croppedRect.SubMat(boundingRect);
                return boundingRect.Width <= 40 && boundingRect.Height >= 10 && resizedCharacter.Mean().Val0 <= 254.0;
            }
            return rectangle.Height <= 50 && rectangle.Height >= 10 && croppedRect.Mean().Val0 <= 254.0 && croppedRect.Mean().Val0 >= 10.0;
        }
    }
}