using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using CNTK;
using CrossCutting;
using Point = System.Drawing.Point;
using PointF = System.Drawing.PointF;
using Rectangle = CrossCutting.Rectangle;

namespace CognitiveToolkit
{
    public class NumberInterpreterCoreCompat : INumberInterpreter
    {
        
        private readonly DeviceDescriptor _device;

        private readonly Function _model;

        public NumberInterpreterCoreCompat() : this(Path.Combine("Data", "Models", "MNISTConvolution.model"))
        {
        }

        public NumberInterpreterCoreCompat(string modelFilename)
        {
            _device = DeviceDescriptor.CPUDevice; 
            _model = Function.Load(modelFilename, _device);
        }

        public (int Value, float Confidence) Interpret(byte[] imageBytes)
        {
            var imageData = ReadImageData(imageBytes);
            var c = imageData.Count;
            var inputs = CreateInput(imageData);
            var outputs = new Dictionary<Variable, Value> {{_model.Output, null}};
            _model.Evaluate(inputs, outputs, _device);
            var value = outputs.FirstOrDefault().Value;
            var dense = value.GetDenseData<float>(_model.Output);
            var first = dense.First();
            return (first.IndexOf(first.Max()), first.Max());
        }

        public (int Value, float Confidence)[] Interpret(byte[] imageBytes, Rectangle area)
        {
            var imageData = ReadImageData(imageBytes, area);
            var inputs = CreateInput(imageData);
            var outputs = new Dictionary<Variable, Value> {{_model.Output, null}};
            _model.Evaluate(inputs, outputs, _device);
            var value = outputs.FirstOrDefault().Value;
            var dense = value.GetDenseData<float>(_model.Output);
            return dense.First()?.Select((qaz, wsx) => (wsx, qaz)).ToArray();
        }

        public IEnumerable<(int Value, float Confidence)> InterpretAll(byte[] imageBytes, Rectangle area)
        {
            throw new NotImplementedException();
        }


        private IReadOnlyCollection<float> ReadImageData(byte[] pngBytes, Rectangle area)
        {
            var ret = new List<float>();

            using (var stream = new MemoryStream(pngBytes))
            using (var sourceImage = Image.FromStream(stream) as Bitmap)
            using (var croppedImage =
                sourceImage?.Clone(new System.Drawing.Rectangle(area.X, area.Y, area.Width, area.Height),
                    sourceImage.PixelFormat))
            {
                
                using (var targetImage = To28X28(croppedImage))
                {
                    for (var y = 0; y < targetImage.Height; ++y)
                    for (var x = 0; x < targetImage.Width; ++x)
                    {
                        var clr = targetImage.GetPixel(x, y);
                        ret.Add((byte) (255 - clr.B));
                    }
                }
            }
            return ret;
        }

        private IReadOnlyCollection<float> ReadImageData(byte[] pngBytes)
        {
            var ret = new List<float>();

            using (var stream = new MemoryStream(pngBytes))
            using (var sourceImage = Image.FromStream(stream) as Bitmap)
            {
                using (var targetImage = To28X28(sourceImage))
                {
                    for (var y = 0; y < targetImage.Height; ++y)
                    for (var x = 0; x < targetImage.Width; ++x)
                    {
                        var clr = targetImage.GetPixel(x, y);
                        ret.Add((byte) (255 - clr.B));
                    }
                }
            }
            return ret;
        }


        private IDictionary<Variable, Value> CreateInput(IEnumerable<float> data)
        {
            var ret = new Dictionary<Variable, Value>();
            var variable = _model.Inputs.First(i => i.IsInput); 
            
            var value = Value.CreateBatch(variable.Shape, data, _device, true);
            ret[variable] = value;
            return ret;
        }

        private static void CenterBitmapAtCenterOfMass(Bitmap bitmap)
        {
            
            var centerOfMass = FindCenterOfMass(bitmap);
            if (float.IsNaN(centerOfMass.X) || float.IsNaN(centerOfMass.Y))
                return;
            var envelope = FindEnvelope(bitmap);
            using (var clone = bitmap.Clone(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height),
                bitmap.PixelFormat))
            {
                var centerNow = new PointF(bitmap.Width / 2f, bitmap.Height / 2f);
                var diff = new SizeF(centerOfMass.X - centerNow.X, centerOfMass.Y - centerNow.Y);
                
                float dx = diff.Width, dy = diff.Height;
                
                if (envelope.Left - diff.Width < 0)
                    dx = envelope.Left;
                if (envelope.Right - diff.Width > bitmap.Width)
                    dx = envelope.Right - bitmap.Width;
                if (envelope.Top - diff.Height < 0)
                    dy = envelope.Top;
                if (envelope.Bottom - diff.Height > bitmap.Height)
                    dy = envelope.Bottom - bitmap.Height;

                var newOrigin = new PointF(-dx, -dy);
                using (var graphics = Graphics.FromImage(bitmap))
                {
                    graphics.FillRectangle(new SolidBrush(Color.White),
                        new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
                    graphics.DrawImage(clone, newOrigin);
                }
            }
            bitmap.Save(@"C:\temp\shit_after.png");
        }

        private static PointF FindCenterOfMass(Bitmap bitmap)
        {
            var cumX = 0f;
            var cumY = 0f;
            var numberOfPoints = 0;
            for (var x = 0; x < bitmap.Width; ++x)
            for (var y = 0; y < bitmap.Height; ++y)
            {
                var pixel = bitmap.GetPixel(x, y);
                var gray = 255 - (pixel.R + pixel.G + pixel.B) / 3;
                if (gray > 0)
                {
                    cumX += x; 
                    cumY += y; 
                    ++numberOfPoints;
                }
            }
            var avgX = cumX / numberOfPoints;
            var avgY = cumY / numberOfPoints;
            return new PointF(avgX, avgY);
        }

        private static System.Drawing.Rectangle FindEnvelope(Bitmap bitmap)
        {
            int minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue;

            var bakcgroundColor = bitmap.GetPixel(0, 0).ToArgb();
            for (var x = 0; x < bitmap.Width; ++x)
            for (var y = 0; y < bitmap.Height; ++y)
            {
                var pixel = bitmap.GetPixel(x, y).ToArgb();
                if (pixel == bakcgroundColor) continue;
                if (x < minX)
                    minX = x;
                if (y < minY)
                    minY = y;
                if (x > maxX)
                    maxX = x;
                if (y > maxY)
                    maxY = y;
            }
            return System.Drawing.Rectangle.FromLTRB(minX, minY, maxX + 1, maxY + 1);
        }

        private static Bitmap To28X28(Bitmap bitmap)
        {
            using (var squareImage = new Bitmap(Math.Max(bitmap.Width, bitmap.Height),
                Math.Max(bitmap.Width, bitmap.Height)))
            {
                squareImage.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);
                
                
                
                
                using (var squareGraphics = Graphics.FromImage(squareImage))
                {
                    var origin = new Point((squareImage.Width - bitmap.Width) / 2,
                        (squareImage.Height - bitmap.Height) / 2);
                    squareGraphics.DrawImage(bitmap, origin);
                }


                
                var targetImage = new Bitmap(squareImage, 28, 28);
                CenterBitmapAtCenterOfMass(targetImage);
                return targetImage;
            }
        }
    }
}
