using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using InterpreterLibrary.Models;
using static System.Drawing.Color;
using Point = System.Drawing.Point;
using Rectangle = System.Drawing.Rectangle;

namespace InterpreterLibrary.Processing
{
    public class BitmapMasker
    {
        private readonly Bitmap _bitmap;
        private readonly Color _backgroundColor;

        public BitmapMasker(Bitmap bitmap, Color backgroundColor) : this(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height), backgroundColor)
        {

        }

        public BitmapMasker(Bitmap bitmap, Rectangle rectangle, Color backgroundColor)
        {
            _bitmap = bitmap.Clone(rectangle, PixelFormat.Format32bppArgb);
            _backgroundColor = backgroundColor;
        }

        public Bitmap ApplyMultiPolygonMask(IEnumerable<Point[]> polygons)
        {
            int w = _bitmap.Width, h = _bitmap.Height;
            var polygonArray = polygons as Point[][] ?? polygons.ToArray();
            var xMin = polygonArray.Min(points => points.Min(p => p.X));
            var xMax = polygonArray.Max(points => points.Max(p => p.X));
            var yMin = polygonArray.Min(points => points.Min(p => p.Y));
            var yMax = polygonArray.Max(points => points.Max(p => p.Y));

            var ret = new Bitmap(w, h, _bitmap.PixelFormat);
            using (var graphics = Graphics.FromImage(ret))
            {
                using (var bgBrush = new SolidBrush(_backgroundColor))
                {
                    graphics.FillRectangle(bgBrush, 0, 0, w, h);
                }
            }
            var mask = new Bitmap(w, h);
            var borderColor = Red;
            var interiorColor = Blue;
            var floodFillPoints = new List<Point>();
            using (var graphics = Graphics.FromImage(mask))
            {
                var borderPen = new Pen(borderColor, 2);
                foreach (var polygon in polygonArray)
                {
                    Point? interiorPoint = FindSomeInteriorPoint(polygon);
                    if (interiorPoint == null)
                    {
                        continue;
                    }
                    graphics.DrawPolygon(borderPen, polygon);
                    floodFillPoints.Add(interiorPoint.Value);
                }
            }
            foreach (var ffp in floodFillPoints)
            {
                FloodFill(mask, ffp, interiorColor);
            }

            var bitmapRect = new Rectangle(0, 0, w, h);
            var srcData = _bitmap.LockBits(
                 bitmapRect,
                 ImageLockMode.ReadOnly, _bitmap.PixelFormat);
            var srcBits = new int[srcData.Stride / 4 * srcData.Height];
            Marshal.Copy(srcData.Scan0, srcBits, 0, srcBits.Length);

            var maskData = mask.LockBits(
                bitmapRect,
                ImageLockMode.ReadOnly, mask.PixelFormat);
            var maskBits = new int[maskData.Stride / 4 * maskData.Height];
            Marshal.Copy(maskData.Scan0, maskBits, 0, maskBits.Length);

            var dstData = ret.LockBits(
                bitmapRect,
                ImageLockMode.WriteOnly, ret.PixelFormat);
            var dstBits = new int[dstData.Stride / 4 * dstData.Height];
            Marshal.Copy(dstData.Scan0, dstBits, 0, dstBits.Length);

            for (var y = yMin; y <= yMax; ++y)
            {
                for (var x = xMin; x <= xMax; ++x)
                {
                    var maskValue = GetArgbPixel(x, y, maskBits, maskData.Stride);
                    if (maskValue != interiorColor.ToArgb())
                    {
                        continue;
                    }
                    var px = GetArgbPixel(x, y, srcBits, srcData.Stride);
                    SetArgbPixel(x, y, px, dstBits, dstData.Stride);
                }
            }
            Marshal.Copy(dstBits, 0, dstData.Scan0, dstBits.Length);

            
            ret.UnlockBits(dstData);
            mask.UnlockBits(maskData);
            _bitmap.UnlockBits(srcData);

            mask.Dispose();

            return ret;
        }
        public static bool IsInPolygon(Point[] poly, Point p)
        {
            var inside = false;

            if (poly.Length < 3)
            {
                return inside;
            }

            var oldPoint = new Point(
                poly[poly.Length - 1].X, poly[poly.Length - 1].Y);

            foreach (var t in poly)
            {
                var newPoint = new Point(t.X, t.Y);
                Point p1;
                Point p2;
                if (newPoint.X > oldPoint.X)
                {
                    p1 = oldPoint;
                    p2 = newPoint;
                }
                else
                {
                    p1 = newPoint;
                    p2 = oldPoint;
                }

                if ((newPoint.X < p.X) == (p.X <= oldPoint.X)
                    && (p.Y - (long)p1.Y) * (p2.X - p1.X)
                    < (p2.Y - (long)p1.Y) * (p.X - p1.X))
                {
                    inside = !inside;
                }
                oldPoint = newPoint;
            }
            return inside;
        }

        private static void FloodFill(Bitmap bitmap, Point point, Color color)
        {
            var x = point.X;
            var y = point.Y;

            BitmapData data = bitmap.LockBits(
                new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
            var bits = new int[data.Stride / 4 * data.Height];
            Marshal.Copy(data.Scan0, bits, 0, bits.Length);

            LinkedList<Point> check = new LinkedList<Point>();
            int floodTo = color.ToArgb();
            int floodFrom = bits[x + y * data.Stride / 4];
            bits[x + y * data.Stride / 4] = floodTo;

            if (floodFrom != floodTo)
            {
                check.AddLast(new Point(x, y));
                while (check.Count > 0)
                {
                    Point cur = check.First.Value;
                    check.RemoveFirst();

                    foreach (var off in new[] {
                        new Size(0, -1), new Size(0, 1),
                        new Size(-1, 0), new Size(1, 0)})
                    {
                        var next = cur + off;
                        if (next.X < 0 || next.Y < 0 || next.X >= data.Width || next.Y >= data.Height) continue;
                        var nextColor = GetArgbPixel(next.X, next.Y, bits, data.Stride);
                        if (nextColor != floodFrom)
                        {
                            continue;
                        }

                        check.AddLast(next);
                        SetArgbPixel(next.X, next.Y, floodTo, bits, data.Stride);
                    }
                }
            }

            Marshal.Copy(bits, 0, data.Scan0, bits.Length);
            bitmap.UnlockBits(data);
        }

        private static void SetArgbPixel(int x, int y, int argb, IList<int> bits, int stride)
        {
            bits[x + y * stride / 4] = argb;
        }

        private static int GetArgbPixel(int x, int y, int[] bits, int stride)
        {
            return bits[x + y * stride / 4];
        }

        private static Point? FindSomeInteriorPoint(IReadOnlyList<Point> polygonPoints)
        {
            if (polygonPoints.Count < 4)
            {
                return null;
            }
            var xMin = polygonPoints.Min(p => p.X);
            var yMin = polygonPoints.Min(p => p.Y);
            var xMax = polygonPoints.Max(p => p.X);
            var yMax = polygonPoints.Max(p => p.Y);

            var centerPoint = new Point((xMin + xMax) / 2, (yMin + yMax) / 2);
            if (IsInPolygon(polygonPoints.ToArray(), centerPoint))
            {
                return centerPoint;
            }

            var startPoint = new PointF(xMin - 1, (yMin + yMax) / 2);
            var endPoint = new PointF(xMax + 1, startPoint.Y);
            var line = Line.FromPoints(startPoint, endPoint);

            var intersections = new List<PointF>();
            for (var i = 0; i < polygonPoints.Count - 1; ++i)
            {
                PointF p1 = polygonPoints[i];
                PointF p2 = polygonPoints[i + 1];
                if (p1 == p2)
                {
                    continue;
                }
                var segment = new LineSegment(p1, p2);
                try
                {
                    var intersection = line.GetIntersectionWith(segment);
                    if (intersection != null)
                    {
                        intersections.Add(intersection.Value);
                        if (intersections.Count == 2)
                        {
                            break;
                        }
                    }
                }
                catch (Exception)
                {
                    
                }

            }
            if (intersections.Count < 2)
            {
                return null;
            }
            return new Point((int)intersections.Average(p => p.X), (int)startPoint.Y);
        }
    }
}

