using System;
using System.Collections.Generic;
using System.Drawing;
using Tesseract;
using Point = InterpreterLibrary.Models.Point;
using PointF = System.Drawing.PointF;
using Rectangle = InterpreterLibrary.Models.Rectangle;

namespace InterpreterLibrary.Processing
{
    
    
    
    
    
    
    
    public enum ObjectsOrder
    {
        
        
        
        None,

        
        
        
        
        Size,

        
        
        
        Area,

        
        
        
        
        YX,

        
        
        
        
        XY
    }

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    public abstract class BlobCounterBase
    {
        
        private readonly List<Blob> _blobs = new List<Blob>();

        

        private readonly object _filter = null;

        

        
        
        
        protected int ImageHeight;

        
        
        
        protected int ImageWidth;

        

        
        
        
        protected int[] objectLabels;

        
        
        
        protected int objectsCount;

        

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        protected BlobCounterBase()
        {
        }

        
        
        
        
        
        
        
        
        protected BlobCounterBase(Pix image)
        {
            ProcessImage(image);
        }

        protected BlobCounterBase(Pix image, Rectangle areOfInterest)
        {
            ProcessImage(image, areOfInterest);
        }

        
        
        
        
        
        
        
        
        public int ObjectsCount => objectsCount;

        
        
        
        
        
        
        
        
        public int[] ObjectLabels => objectLabels;

        
        
        
        
        
        
        
        
        
        public ObjectsOrder ObjectsOrder { get; set; } = ObjectsOrder.None;

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public bool FilterBlobs { get; set; } = false;

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public bool CoupledSizeFiltering { get; set; } = false;

        
        
        
        
        
        
        
        
        
        
        
        
        public int MinWidth { get; set; } = 1;

        
        
        
        
        
        
        
        
        
        
        
        
        public int MinHeight { get; set; } = 1;

        
        
        
        
        
        
        
        
        
        
        
        
        public int MaxWidth { get; set; } = int.MaxValue;

        
        
        
        
        
        
        
        
        
        
        
        
        public int MaxHeight { get; set; } = int.MaxValue;

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public void ProcessImage(Pix image)
        {
            ProcessImage(image, null);
        }

        public void ProcessImage(Pix image, Rectangle areaOfInterest)
        {
            ImageWidth = image.Width;
            ImageHeight = image.Height;

            
            BuildObjectsMap(image, areaOfInterest);

            
            CollectObjectsInfo(image);

            
            if (FilterBlobs)
            {
                
                var labelsMap = new int[objectsCount + 1];
                for (var i = 1; i <= objectsCount; i++)
                    labelsMap[i] = i;

                
                var objectsToRemove = 0;

                if (_filter == null)
                    for (var i = objectsCount - 1; i >= 0; i--)
                    {
                        var blobWidth = _blobs[i].Rectangle.Width;
                        var blobHeight = _blobs[i].Rectangle.Height;

                        if (CoupledSizeFiltering == false)
                        {
                            
                            if (
                                blobWidth < MinWidth || blobHeight < MinHeight ||
                                blobWidth > MaxWidth || blobHeight > MaxHeight)
                            {
                                labelsMap[i + 1] = 0;
                                objectsToRemove++;
                                _blobs.RemoveAt(i);
                            }
                        }
                        else
                        {
                            
                            if (
                                blobWidth < MinWidth && blobHeight < MinHeight ||
                                blobWidth > MaxWidth && blobHeight > MaxHeight)
                            {
                                labelsMap[i + 1] = 0;
                                objectsToRemove++;
                                _blobs.RemoveAt(i);
                            }
                        }
                    }

                
                var label = 0;
                for (var i = 1; i <= objectsCount; i++)
                    if (labelsMap[i] != 0)
                    {
                        label++;
                        
                        labelsMap[i] = label;
                    }

                
                for (int i = 0, n = objectLabels.Length; i < n; i++)
                    objectLabels[i] = labelsMap[objectLabels[i]];

                objectsCount -= objectsToRemove;

                
                for (int i = 0, n = _blobs.Count; i < n; i++)
                    _blobs[i].Id = i + 1;
            }

            
            if (ObjectsOrder != ObjectsOrder.None)
                _blobs.Sort(new BlobsSorter(ObjectsOrder));
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public CrossCutting.Rectangle[] GetObjectsRectangles()
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            var rects = new CrossCutting.Rectangle[objectsCount];

            for (var i = 0; i < objectsCount; i++)
                rects[i] = _blobs[i].Rectangle;

            return rects;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Blob[] GetObjectsInformation()
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            var blobsToReturn = new Blob[objectsCount];

            
            for (var k = 0; k < objectsCount; k++)
                blobsToReturn[k] = new Blob(_blobs[k]);

            return blobsToReturn;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        internal Blob[] GetObjects(Pix image, bool extractInOriginalSize)
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            
            var width = image.Width;
            var height = image.Height;
            var srcStride = image.GetData().WordsPerLine * sizeof(int);
            const int
                pixelSize =
                    1; 

            var objects = new Blob[objectsCount];

            
            for (var k = 0; k < objectsCount; k++)
            {
                var objectWidth = _blobs[k].Rectangle.Width;
                var objectHeight = _blobs[k].Rectangle.Height;

                var blobImageWidth = extractInOriginalSize ? width : objectWidth;
                var blobImageHeight = extractInOriginalSize ? height : objectHeight;

                var xmin = _blobs[k].Rectangle.X;
                var xmax = xmin + objectWidth - 1;
                var ymin = _blobs[k].Rectangle.Y;
                var ymax = ymin + objectHeight - 1;

                var label = _blobs[k].Id;

                
                var dstImage = Pix.Create(blobImageWidth, blobImageHeight, image.Depth);

                
                unsafe
                {
                    var src = (byte*) image.GetData().Data.ToPointer() + ymin * srcStride + xmin * pixelSize;
                    var dst = (byte*) dstImage.GetData().Data.ToPointer();
                    var p = ymin * width + xmin;
                    var dstStride = dstImage.GetData().WordsPerLine * sizeof(int);

                    if (extractInOriginalSize)
                        dst += ymin * dstStride + xmin * pixelSize;

                    var srcOffset = srcStride - objectWidth * pixelSize;
                    var dstOffset = dstStride - objectWidth * pixelSize;
                    var labelsOffset = width - objectWidth;

                    
                    for (var y = ymin; y <= ymax; y++)
                    {
                        
                        for (var x = xmin; x <= xmax; x++, p++, dst += pixelSize, src += pixelSize)
                            if (objectLabels[p] == label)
                            {
                                
                                *dst = *src;

                                    dst[1] = src[1];
                                    dst[2] = src[2];
                            }
                        src += srcOffset;
                        dst += dstOffset;
                        p += labelsOffset;
                    }
                }

                objects[k] = new Blob(_blobs[k]) { Image = dstImage, OriginalSize = extractInOriginalSize};
            }

            return objects;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        internal void GetBlobsLeftAndRightEdges(Blob blob, out List<Point> leftEdge, out List<Point> rightEdge)
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            leftEdge = new List<Point>();
            rightEdge = new List<Point>();

            var xmin = blob.Rectangle.X;
            var xmax = xmin + blob.Rectangle.Width - 1;
            var ymin = blob.Rectangle.Y;
            var ymax = ymin + blob.Rectangle.Height - 1;

            var label = blob.Id;

            
            for (var y = ymin; y <= ymax; y++)
            {
                
                var p = y * ImageWidth + xmin;
                for (var x = xmin; x <= xmax; x++, p++)
                    if (objectLabels[p] == label)
                    {
                        leftEdge.Add(new Point(x, y));
                        break;
                    }

                
                p = y * ImageWidth + xmax;
                for (var x = xmax; x >= xmin; x--, p--)
                    if (objectLabels[p] == label)
                    {
                        rightEdge.Add(new Point(x, y));
                        break;
                    }
            }
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        internal void GetBlobsTopAndBottomEdges(Blob blob, out List<Point> topEdge, out List<Point> bottomEdge)
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            topEdge = new List<Point>();
            bottomEdge = new List<Point>();

            var xmin = blob.Rectangle.X;
            var xmax = xmin + blob.Rectangle.Width - 1;
            var ymin = blob.Rectangle.Y;
            var ymax = ymin + blob.Rectangle.Height - 1;

            var label = blob.Id;

            
            for (var x = xmin; x <= xmax; x++)
            {
                
                var p = ymin * ImageWidth + x;
                for (var y = ymin; y <= ymax; y++, p += ImageWidth)
                    if (objectLabels[p] == label)
                    {
                        topEdge.Add(new Point(x, y));
                        break;
                    }

                
                p = ymax * ImageWidth + x;
                for (var y = ymax; y >= ymin; y--, p -= ImageWidth)
                    if (objectLabels[p] == label)
                    {
                        bottomEdge.Add(new Point(x, y));
                        break;
                    }
            }
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public ICollection<Point> GetBlobsEdgePoints(Blob blob)
        {
            
            if (objectLabels == null)
                throw new ApplicationException("Image should be processed before to collect objects map.");

            var edgePoints = new List<Point>();

            var xmin = blob.Rectangle.X;
            var xmax = xmin + blob.Rectangle.Width - 1;
            var ymin = blob.Rectangle.Y;
            var ymax = ymin + blob.Rectangle.Height - 1;

            var label = blob.Id;

            
            var leftProcessedPoints = new int[blob.Rectangle.Height];
            var rightProcessedPoints = new int[blob.Rectangle.Height];

            
            for (var y = ymin; y <= ymax; y++)
            {
                
                var p = y * ImageWidth + xmin;
                for (var x = xmin; x <= xmax; x++, p++)
                    if (objectLabels[p] == label)
                    {
                        edgePoints.Add(new Point(x, y));
                        leftProcessedPoints[y - ymin] = x;
                        break;
                    }

                
                p = y * ImageWidth + xmax;
                for (var x = xmax; x >= xmin; x--, p--)
                    if (objectLabels[p] == label)
                    {
                        
                        if (leftProcessedPoints[y - ymin] != x)
                            edgePoints.Add(new Point(x, y));
                        rightProcessedPoints[y - ymin] = x;
                        break;
                    }
            }

            
            for (var x = xmin; x <= xmax; x++)
            {
                
                var p = ymin * ImageWidth + x;
                for (int y = ymin, y0 = 0; y <= ymax; y++, y0++, p += ImageWidth)
                    if (objectLabels[p] == label)
                    {
                        
                        if (leftProcessedPoints[y0] != x &&
                            rightProcessedPoints[y0] != x)
                            edgePoints.Add(new Point(x, y));
                        break;
                    }

                
                p = ymax * ImageWidth + x;
                for (int y = ymax, y0 = ymax - ymin; y >= ymin; y--, y0--, p -= ImageWidth)
                    if (objectLabels[p] == label)
                    {
                        
                        if (leftProcessedPoints[y0] != x &&
                            rightProcessedPoints[y0] != x)
                            edgePoints.Add(new Point(x, y));
                        break;
                    }
            }

            return edgePoints;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        protected abstract void BuildObjectsMap(Pix image, Rectangle areaOfInterest);


        #region Private Methods - Collecting objects' rectangles

        
        private void CollectObjectsInfo(Pix image)
        {
            int i = 0;

            
            var x1 = new int[objectsCount + 1];
            var y1 = new int[objectsCount + 1];
            var x2 = new int[objectsCount + 1];
            var y2 = new int[objectsCount + 1];

            var area = new int[objectsCount + 1];
            var xc = new long[objectsCount + 1];
            var yc = new long[objectsCount + 1];

            var meanR = new long[objectsCount + 1];
            var meanG = new long[objectsCount + 1];
            var meanB = new long[objectsCount + 1];

            var stdDevR = new long[objectsCount + 1];
            var stdDevG = new long[objectsCount + 1];
            var stdDevB = new long[objectsCount + 1];

            for (var j = 1; j <= objectsCount; j++)
            {
                x1[j] = ImageWidth;
                y1[j] = ImageHeight;
            }


            var pixHelper = PixHelper.Create(image);
            
            for (var y = 0; y < ImageHeight; y++)
            for (var x = 0; x < ImageWidth; x++, i++ )
            {
                
                var label = objectLabels[i];

                
                if (label == 0)
                    continue;

                

                if (x < x1[label])
                    x1[label] = x;
                if (x > x2[label])
                    x2[label] = x;
                if (y < y1[label])
                    y1[label] = y;
                if (y > y2[label])
                    y2[label] = y;

                area[label]++;
                xc[label] += x;
                yc[label] += y;

                var g = (byte) pixHelper.GetPixel(x, y); 
                meanG[label] += g;
                stdDevG[label] += g * g;
            }


            
            _blobs.Clear();

            for (var j = 1; j <= objectsCount; j++)
            {
                var blobArea = area[j];

                var blob = new Blob(j, new CrossCutting.Rectangle(x1[j], y1[j], x2[j] - x1[j] + 1, y2[j] - y1[j] + 1))
                {
                    Area = blobArea,
                    Fullness = (double) blobArea / ((x2[j] - x1[j] + 1) * (y2[j] - y1[j] + 1)),
                    CenterOfGravity = new PointF((float) xc[j] / blobArea, (float) yc[j] / blobArea),
                    ColorMean = Color.FromArgb((byte) (meanR[j] / blobArea), (byte) (meanG[j] / blobArea),
                        (byte) (meanB[j] / blobArea))
                };
                blob.ColorStdDev = Color.FromArgb(
                    (byte) Math.Sqrt(stdDevR[j] / blobArea - blob.ColorMean.R * blob.ColorMean.R),
                    (byte) Math.Sqrt(stdDevG[j] / blobArea - blob.ColorMean.G * blob.ColorMean.G),
                    (byte) Math.Sqrt(stdDevB[j] / blobArea - blob.ColorMean.B * blob.ColorMean.B));

                _blobs.Add(blob);
            }
        }

        
        private class BlobsSorter : IComparer<Blob>
        {
            private readonly ObjectsOrder order;

            public BlobsSorter(ObjectsOrder order)
            {
                this.order = order;
            }

            public int Compare(Blob a, Blob b)
            {
                var aRect = a.Rectangle;
                var bRect = b.Rectangle;

                switch (order)
                {
                    case ObjectsOrder.Size: 

                        
                        return bRect.Width * bRect.Height - aRect.Width * aRect.Height;

                    case ObjectsOrder.Area: 
                        return b.Area - a.Area;

                    case ObjectsOrder.YX: 

                        return aRect.Y * 100000 + aRect.X - (bRect.Y * 100000 + bRect.X);

                    case ObjectsOrder.XY: 

                        return aRect.X * 100000 + aRect.Y - (bRect.X * 100000 + bRect.Y);
                }
                return 0;
            }
        }

        #endregion
    }
}
