using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using DbModel.DB.Model;
using log4net;
using OpenCvSharp;
using OpenCvSharp.CPlusPlus;
using OpenCvSharp.CPlusPlus.Flann;
using YSoft.Rqa.Points;
using Size = OpenCvSharp.CPlusPlus.Size;

namespace ImageProcessing
{
    public class Functions
    {
        public int PrinterId;
        private static readonly ILog Logger = LogManager.GetLogger(typeof(IpMethods));
        public CancellationTokenSource FlannToken = new CancellationTokenSource();

        public Mat NegateWhiteTextAreas(List<FeatureRegion> regions, Mat src)
        {
            Mat srcBw = new Mat();
            Mat dst = new Mat();
            Cv2.CvtColor(src, srcBw, ColorConversion.RgbToGray);
            foreach (var region in regions){
                var pos = ToPixelTr(region.Position);
                var size = ToPixel(region.Size);
                Mat patch = RetrieveRegion(srcBw, region.Id);
                Mat ones = Mat.Ones(patch.Rows, patch.Cols, MatType.CV_8UC1) * 255;
                Cv2.AddWeighted(patch, 1, ones, -0.2,0,patch);
                //Mat res = new Mat();
                //Cv2.Threshold(patch, res, 80, 255, ThresholdType.Binary);
                Cv2.Subtract(ones, patch, dst);
                dst.CopyTo(srcBw.SubMat(new Rect((int)pos.X, (int)pos.Y-(int)size.Height, (int)size.Width, (int)size.Height)));
            }
            return srcBw;
        }

        public List<ScreenProbability> ScreenProbabilities(List<string> words)
        {
            RqaContext context = new RqaContext();
            int totalcount = 0;
            Dictionary<Screen, int> screenCounts = new Dictionary<Screen, int>();
            List<ScreenProbability> probabilities = new List<ScreenProbability>();
            //finds all screens which contains feature region containing each word
            List<List<Screen>> lists = (from word 
                                        in words
                                        where word.Length >= 3
                                        select context.FeatureRegions
                                        .Where(x => x.Text.Contains(word) && x.Device.Id == PrinterId)
                                        .Select(x => x.Screen).Distinct().ToList()).ToList();
            foreach (var list in lists){
                for (var i = 0; i < list.Count; i++){
                    var value = 1;
                    if (screenCounts.ContainsKey(list[i])){
                        screenCounts.TryGetValue(list[i], out value);
                        screenCounts.Remove(list[i]);
                        screenCounts.Add(list[i], value + 1);
                    }
                    else{
                        screenCounts.Add(list[i], value);
                    }
                    totalcount++;
                }
            }
            foreach (var pair in screenCounts)
            {
                probabilities.Add(new ScreenProbability() { Screen = pair.Key, Value = ((double)pair.Value / totalcount) });
            }
            return probabilities;
        }

        public bool DoesPopupExist(Mat src){
            SIFT sift = new SIFT(0, 3, 0.04D, 10D, 1D);
            RqaContext context = new RqaContext();
            var pop = context.FeatureRegions.FirstOrDefault(x => x.Device.Id == PrinterId && x.Text == "Popup");
            if (pop == null){
                Logger.Warn("No such feature region with popup for printer with id: "+PrinterId);
                return false;
            }
            Mat rect = new Mat();
            var size = ToPixel(pop.Size);
            var pos = ToPixelTr(pop.Position);
            Cv2.GetRectSubPix(src, new Size(size.Width,size.Height), new Point2f((float)pos.X+size.Width/2,(float)pos.Y-size.Height/2),rect);/*
            Cv2.ImShow("rect",rect);
            Cv2.WaitKey(0);*/
            FlannBasedMatcher flann = new FlannBasedMatcher(null, new SearchParams(100));
            var kp1 = sift.Run(rect, null);
            var mfp = context.MFPs.Where(x => x.Id == PrinterId).Select(x => x.Model);
            var vendor = mfp.Select(x => x.Manufacturer).First();
            var model = mfp.Select(x => x.Model).First();
            Mat des1 = new Mat();
            Mat des2 = new Mat();
            sift.Compute(rect, ref kp1, des1);
            Mat img2 = Cv2.ImRead("C:\\RQA\\imgs\\" + vendor + "\\" + model + "\\features\\Popup.png");
            var kp2 = sift.Run(img2, null);
            sift.Compute(img2, ref kp2, des2);
            var matches = flann.KnnMatch(des1, des2, 2);
            List<DMatch> good = (from t in matches where t[0].Distance < 0.5*t[1].Distance select t[0]).ToList();
            /*
            Mat mat = new Mat();
            Cv2.DrawMatches(rect, kp1,img2,kp2,good,mat, Scalar.Blue, Scalar.Red);
            Cv2.ImShow("mat",mat);
            Cv2.WaitKey(0);
            Cv2.DestroyAllWindows();*/
            return good.Count > 40;
        }

        public Mat Transform(Mat image, Point2f[] corners)
        {
            Point2f[] srcPoints = new Point2f[4];
            Point2f[] dstPoints = new Point2f[4];
            Mat transformed = new Mat(new Size(1280, 720), MatType.CV_8UC3);
            for (int i = 0; i < 4; i++)
            {
                srcPoints[i] = corners[i];
            }
            dstPoints[0] = new Point2f(0, 720);
            dstPoints[1] = new Point2f(0, 0);
            dstPoints[2] = new Point2f(1280, 0);
            dstPoints[3] = new Point2f(1280, 720);
            // transformation matrix
            var translate = Cv2.GetPerspectiveTransform(srcPoints, dstPoints);
            Cv2.WarpPerspective(image, transformed, translate, transformed.Size(), Interpolation.Cubic);
            Mat gauss = new Mat();
            Cv2.GaussianBlur(transformed, gauss, new Size(0, 0), 5);
            Cv2.AddWeighted(transformed, 1.5, gauss, -0.5, 0, transformed);
            return transformed;
        }

        public List<ScreenProbability> FlannFeatureDetector(Mat src)
        {
            SIFT sift = new SIFT(0, 3, 0.04D, 10D, 1D);
            RqaContext context = new RqaContext();
            List<ScreenProbability> screens = new List<ScreenProbability>();
            FlannBasedMatcher flann = new FlannBasedMatcher(null, new SearchParams(30));
            var kp1 = sift.Run(src, null);
            var mfp = context.MFPs.Where(x => x.Id == PrinterId).Select(x => x.Model);
            var vendor = mfp.Select(x => x.Manufacturer).First();
            var model = mfp.Select(x => x.Model).First();
            //todo: non-relative path
            string[] files = new string[] {};
            try{
                files = Directory.GetFiles("C:\\RQA\\imgs\\" + vendor + "\\" + model + "\\");
            }
            catch (Exception ex){
                Logger.Error(
                    "File path C:\\RQA\\imgs\\" + vendor + "\\" + model + "\\ does not exist, flann cannot proceed", ex);
                return new List<ScreenProbability>();
            }
            Mat des1 = new Mat();
            Mat des2 = new Mat();
            sift.Compute(src, ref kp1, des1);
            foreach (var file in files){
                if (FlannToken.IsCancellationRequested)
                    break;
                Mat img2 = Cv2.ImRead(file);
                var kp2 = sift.Run(img2, null);
                sift.Compute(img2, ref kp2, des2);
                var matches = flann.KnnMatch(des1, des2, 2);
                List<DMatch> good =
                    (from t in matches where t[0].Distance < 0.75*t[1].Distance select t[0]).ToList();
                var screenString = file.Split('\\').Last().Split('.').First();
                var screen = context.Screens.SingleOrDefault(x => x.Text == screenString);
                screens.Add(new ScreenProbability(){Value = good.Count, Screen = screen});
            }
            return screens;
        }

        public List<ScreenProbability> BruteFeatureDetector(Mat src)
        {
            SIFT sift = new SIFT();
            var kp1 = sift.Run(src, null);
            RqaContext context = new RqaContext();
            var mfp = context.MFPs.Where(x => x.Id == PrinterId).Select(x => x.Model);
            var vendor = mfp.Select(x => x.Manufacturer).First();
            var model = mfp.Select(x => x.Model).First();
            //todo: non-relative path
            var files = Directory.GetFiles("C:\\RQA\\imgs\\" + vendor + "\\" + model + "\\");
            List<ScreenProbability> screens = new List<ScreenProbability>();
            foreach (var file in files)
            {
                Mat el = Cv2.ImRead(file);
                var kp2 = sift.Run(el, null);
                Mat des1 = new Mat();
                Mat des2 = new Mat();
                sift.Compute(src, ref kp1, des1);
                sift.Compute(src, ref kp2, des2);
                BFMatcher bf = new BFMatcher();
                var matches = bf.KnnMatch(des1, des2, 2);
                List<DMatch> good = new List<DMatch>();
                foreach (var match in matches)
                {
                    if (match[0].Distance < 0.75 * match[1].Distance)
                        good.Add(match[0]);
                }
                var screenString = file.Split('\\').Last().Split('.').First();
                var screen = context.Screens.SingleOrDefault(x => x.Text == screenString);
                screens.Add(new ScreenProbability() { Value = good.Count, Screen = screen });
            }
            return screens;
        }

        /// <summary>
         /// Retrieves an element according to tag and mfd from db
         /// </summary>
         /// <param name="img">input transformed image</param>
         /// <param name="tag"></param>
         /// <returns></returns>
        public Mat RetrieveElement(Mat img, string tag)
        {
            Mat patch = new Mat();
            RqaContext context = new RqaContext();
            var element = context.ButtonPlacements.FirstOrDefault(x => x.Button.Tag.ToLower() == tag.ToLower() && x.Printer.Id == PrinterId);
            if (element == null)
            {
                Logger.Error("No button with tag: " + tag + " and ID: " + PrinterId + " has been found");
                return null;
            }
            var increm = new Point2d(2, 5);
            var center = new Point2d(element.Position.X + element.Size.Width / 2,
                element.Position.Y + element.Size.Height / 2);
            var centerP = ToPixelTr(center).ToFloat();
            var sizeRect = ToPixelTr(new Point2d(element.Size.Width, element.Size.Height), true);
            Cv2.GetRectSubPix(img, new Size(sizeRect.X + increm.X, sizeRect.Y + increm.Y), centerP, patch);
            return patch;
        }

        public Mat RetrieveRegion(Mat img, int id)
        {
            Mat patch = new Mat();
            RqaContext context = new RqaContext();
            var element = context.FeatureRegions.FirstOrDefault(x => x.Id == id);
            if (element == null)
            {
                Logger.Error("No region with id: " + id + " has been found");
                return null;
            }
            var center = new Point2d(element.Position.X + element.Size.Width / 2,
                element.Position.Y + element.Size.Height / 2);
            var centerP = ToPixelTr(center).ToFloat();
            var sizeRect = ToPixelTr(new Point2d(element.Size.Width, element.Size.Height), true);
            Cv2.GetRectSubPix(img, new Size(sizeRect.X, sizeRect.Y), centerP, patch);
            return patch;
        }

        public Point2d ToPixelTr(Point2d mili, bool size = false)
        {
            RqaContext context = new RqaContext();
            Size2f screenPixels = new Size2f(1280, 720);
            Size2f screenMilis = new Size2f(context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Width,
                context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Height);
            if (size)
                mili.Y = mili.Y * (screenPixels.Height / screenMilis.Height);
            else
                mili.Y = screenPixels.Height - mili.Y * (screenPixels.Height / screenMilis.Height);
            mili.X = mili.X * (screenPixels.Width / screenMilis.Width);
            return mili;
        }

        public DbModel.DB.Model.Size ToPixel(DbModel.DB.Model.Size mili)
        {
            RqaContext context = new RqaContext();
            Size2f screenPixels = new Size2f(1280, 720);
            Size2f screenMilis = new Size2f(context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Width,
                context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Height);
            mili.Height = mili.Height * (screenPixels.Height / screenMilis.Height);
            mili.Width = mili.Width * (screenPixels.Width / screenMilis.Width);
            return mili;
        }

        public Point2 ToPixelTr(Point2 mili)
        {
            RqaContext context = new RqaContext();
            Size2f screenPixels = new Size2f(1280, 720);
            Size2f screenMilis = new Size2f(context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Width,
                context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Height);
            mili.Y = screenPixels.Height - mili.Y * (screenPixels.Height / screenMilis.Height);
            mili.X = mili.X * (screenPixels.Width / screenMilis.Width);
            return mili;
        }
        public Point2 ToPixel(Point2 mili)
        {
            RqaContext context = new RqaContext();
            Size2f screenPixels = new Size2f(1280, 720);
            Size2f screenMilis = new Size2f(context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Width,
                context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Height);
            mili.Y = mili.Y * (screenPixels.Height / screenMilis.Height);
            mili.X = mili.X * (screenPixels.Width / screenMilis.Width);
            return mili;
        }

        public Point2f ToPixelTr(Point2f mili)
        {
            RqaContext context = new RqaContext();
            Size2f screenPixels = new Size2f(1280, 720);
            Size2f screenMilis = new Size2f(context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Width,
                context.MFPs.FirstOrDefault(x => x.Id == PrinterId).Model.Screen.Height);
            mili.Y = screenPixels.Height - mili.Y * (screenPixels.Height / screenMilis.Height);
            mili.X = mili.X * (screenPixels.Width / screenMilis.Width);
            return mili;
        }
    }
}
