using System;
using System.Drawing;
using System.Linq;
using AForge.Imaging.Filters;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using Size = OpenCvSharp.Size;

namespace OCR
{
    internal static class Preprocessing
    {
        /// <summary>
        /// Binarizes original rgb image using combination of global and local Otsu thesholding and Bradley local thresholding
        /// </summary>
        /// <param name="image">rgb image</param>
        /// <returns>binary image</returns>
        public static Mat InitialConversion(Mat image)
        {
            //convert from rgb to grayscale
            Mat bw = image.CvtColor(ColorConversionCodes.RGB2GRAY);

            //bilateral filter
            Mat bilateralFilter = bw.EmptyClone();
            Cv2.BilateralFilter(bw, bilateralFilter, 5, 25.0, 75.0);
            
            //apply reduction of gaussian noise -> sharpening
            Mat noise = bw.EmptyClone();
            Mat denoised = bw.EmptyClone();
            Cv2.GaussianBlur(bilateralFilter, noise, new Size(0, 0), 5.0);
            Cv2.AddWeighted(bilateralFilter, 1.5, noise, -0.5, 0.0, denoised);

            //global otsu
            Mat globalOtsu = bilateralFilter.EmptyClone();
            Cv2.Threshold(denoised.Clone(), globalOtsu, 0.0, 255.0, ThresholdTypes.Otsu);
            //unify background
            Mat newGlobalOtsu = UnifyBackground(globalOtsu);

            //local and global otsu combined
            int numberOfBlocks = 10;
            var localGlobalOtsu = LocalOtsuThreshold(denoised.Clone(), newGlobalOtsu.Clone(), numberOfBlocks, true);

            //bradley threshold
            BradleyLocalThresholding thresholding = new BradleyLocalThresholding();
            Bitmap bradleyThreshold = denoised.Clone().ToBitmap();
            bradleyThreshold = thresholding.Apply(bradleyThreshold);
            
            //combine globallocalotsu with bradley
            var combined = CombineBinarized(localGlobalOtsu.Clone(), bradleyThreshold.ToMat().Clone(), numberOfBlocks);
            new Window("binarization output", combined);
            Cv2.WaitKey(1);
            return combined;
        }


        /// <summary>
        /// Transforms image to have black text on white background
        /// </summary>
        /// <param name="image">binary image</param>
        /// <returns>binary image with unified white background</returns>
        private static Mat UnifyBackground(Mat image)
        {
            double meanValue = GetBorderMeanValue(image, 3);
            if (meanValue < 120.0)
                Cv2.BitwiseNot(image, image);
            //add border
            Mat srcWithBorder = new Mat();
            int border = 5;
            Cv2.CopyMakeBorder(image, srcWithBorder, border, border, border, border, BorderTypes.Constant, Scalar.White);
            Mat src = srcWithBorder.Clone();
            //apply median filter to omit borders
            Cv2.MedianBlur(src,src,7);
            Mat hierarchy = new Mat();
            Cv2.Canny(src, src, 50.0, 150.0, 3, true);
            Cv2.FindContours(src, out Mat[] contours, hierarchy, RetrievalModes.CComp, ContourApproximationModes.ApproxSimple);
            Mat draw = src.EmptyClone().SetTo(0);
            Cv2.DrawContours(draw, contours, -1, Scalar.White, Cv2.FILLED);

            contours = contours.OrderByDescending(x => x.Rows).ToArray();
            for (var index = 0; index < contours.Length; index++) {
                Mat contour = contours[index];
                Rect box = Cv2.BoundingRect(contour);
                if (box.Width < 30 || box.Height < 20 || box.Width + box.Height < 100)
                    continue;
                Mat toRevert = srcWithBorder.SubMat(box.Top, box.Bottom, box.Left, box.Right);
                if (toRevert.Mean().Val0 < 80) {
                    Mat mask = srcWithBorder.Clone().SetTo(0);
                    Cv2.DrawContours(mask, contours, index, Scalar.White, Cv2.FILLED);
                    Cv2.FillConvexPoly(mask, contour, Scalar.White);
                    Cv2.BitwiseNot(srcWithBorder, srcWithBorder, mask);
                }
            }
            //remove border
            Mat newGlobalOtsu = new Mat(srcWithBorder, new Rect(border, border, image.Width, image.Height));
            return newGlobalOtsu;
        }


        /// <summary>
        /// Returns mean value from image border
        /// </summary>
        /// <param name="image">binary image</param>
        /// <param name="borderWidth">border width to be taken into account, should be odd</param>
        /// <returns>mean value of image border pixels</returns>
        private static double GetBorderMeanValue(Mat image, int borderWidth)
        {
            Size horizontalSize = new Size(image.Width, borderWidth);
            Size verticalSize = new Size(borderWidth, image.Height - 2 * borderWidth);
            double sum = image.GetRectSubPix(horizontalSize, new Point2f(image.Width / 2, borderWidth / 2)).Mean().Val0
                         + image.GetRectSubPix(horizontalSize, new Point2f(image.Width / 2, image.Rows - borderWidth / 2)).Mean().Val0
                         + image.GetRectSubPix(verticalSize, new Point2f(borderWidth / 2, image.Height / 2)).Mean().Val0
                         + image.GetRectSubPix(verticalSize, new Point2f(image.Cols - borderWidth / 2, image.Height / 2)).Mean().Val0;
            return sum/4;
        }


        /// <summary>
        /// Returns binary image using local otsu thresholding with the possibility of combination with global otsu thresholding
        /// </summary>
        /// <param name="image">grayscale image to be binarized</param>
        /// <param name="globalOtsu">binarized image to be combined with, usually acquired using global otsu</param>
        /// <param name="numberOfBlocks">number of blocks the image is devided to in each direction</param>
        /// <param name="cobineLocalGlobal"></param>
        /// <returns>binary image</returns>
        private static Mat LocalOtsuThreshold(Mat image, Mat globalOtsu, int numberOfBlocks, bool cobineLocalGlobal)
        {
            int blockWidth = image.Width / numberOfBlocks;
            int blockHeight = image.Height / numberOfBlocks;
            for (int i = 0; i < numberOfBlocks; i++) {
                for (int j = 0; j < numberOfBlocks; j++) {
                    Mat local = image.SubMat(i * blockHeight, (i + 1) * blockHeight, j * blockWidth, (j + 1) * blockWidth);
                    Cv2.Threshold(local, local, 0.0, 255.0, ThresholdTypes.Otsu);
                    Point2f[] localCorners = Cv2.GoodFeaturesToTrack(local, 500, 0.01, 5.0, new Mat(), 3, false, 0.04);
                    Mat global = globalOtsu.SubMat(i * blockHeight, (i + 1) * blockHeight, j * blockWidth, (j + 1) * blockWidth);
                    Point2f[] globalCorners = Cv2.GoodFeaturesToTrack(global, 500, 0.01, 5.0, new Mat(), 3, false, 0.04);
                    if (localCorners.Length < globalCorners.Length && local.Mean().Val0 > global.Mean().Val0)
                        Cv2.Threshold(local, global, 0.0, 255.0, ThresholdTypes.Otsu);
                }
            }
            if (cobineLocalGlobal)
                return globalOtsu;
            
            return image;
        }


        /// <summary>
        /// Combine two binary images to one by blocks, takes the whiter one with less corners
        /// </summary>
        /// <param name="firstImage"></param>
        /// <param name="secondImage"></param>
        /// <param name="numberOfBlocks">number of blocks the image is devided to in each direction</param>
        /// <returns>combined binary image</returns>
        private static Mat CombineBinarized(Mat firstImage, Mat secondImage, int numberOfBlocks)
        {
            int blockWidth = firstImage.Width / numberOfBlocks;
            int blockHeight = firstImage.Height / numberOfBlocks;
            Mat output = firstImage.Clone();
            for (int i = 0; i < numberOfBlocks; i++) {
                for (int j = 0; j < numberOfBlocks; j++) {
                    Mat first = firstImage.SubMat(i * blockHeight, (i + 1) * blockHeight, j * blockWidth, (j + 1) * blockWidth);
                    Point2f[] firstCorners = Cv2.GoodFeaturesToTrack(first, 500, 0.01, 5.0, new Mat(), 3, false, 0.04);
                    Mat second = secondImage.SubMat(i * blockHeight, (i + 1) * blockHeight, j * blockWidth, (j + 1) * blockWidth);
                    Point2f[] secondCorners = Cv2.GoodFeaturesToTrack(second, 500, 0.01, 5.0, new Mat(), 3, false, 0.04);
                    Mat outputBlock = output.SubMat(i * blockHeight, (i + 1) * blockHeight, j * blockWidth, (j + 1) * blockWidth);
                    if (firstCorners.Length > secondCorners.Length || second.Mean().Val0 > first.Mean().Val0)
                        second.CopyTo(outputBlock);
                }
            }
            return output;
        }
    }
}