using System;
using System.Drawing;
using System.IO;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Drawing.Drawing2D;

namespace MarketData.CNNProcessing
{
  public class ImageHelper : IDisposable
  {
    private Bitmap bitmap = null;
    private Graphics graphics=null;
    private PointMapping pointMapping;
    private int width;
    private int height;

    public ImageHelper()
    {
    }

    public ImageHelper(ImageHelper imageHelper)
    {
      this.bitmap=Copy(imageHelper.bitmap);
      width=bitmap.Width;
      height=bitmap.Height;
    }

    public void Dispose()
    {
      DisposeAll();
    }

    private void DisposeAll()
    {
      if(null!=bitmap)
      {
        bitmap.Dispose();
        bitmap=null;
      }
      if(null!=graphics)
      {
        graphics.Dispose();
        graphics=null;
      }
    }

    private Bitmap Copy(Bitmap bitmap)
    {
      Bitmap copy = new Bitmap(bitmap.Width, bitmap.Height);  

      for(int rowIndex=0;rowIndex<bitmap.Height;rowIndex++)
      {
        for(int colIndex=0;colIndex<bitmap.Width;colIndex++)
        {
          Color color=bitmap.GetPixel(rowIndex,colIndex);
          copy.SetPixel(rowIndex,colIndex,color);
        }
      }
      return copy;
    }

    public bool LoadImage(Stream stream)
    {
      try
      {
        DisposeAll();
        Image image=Image.FromStream(stream);
        bitmap = new Bitmap(image);
        height = bitmap.Height;
        width = bitmap.Width;
        return true;
      }
      catch(Exception exception)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString());
        return false;
      }
    }

    public bool LoadImage(string pathFileName) 
    {
      Stream bitmapStream = null;
      try
      {
        DisposeAll();
        bitmap = new Bitmap(pathFileName);
        width=bitmap.Width;
        height=bitmap.Height;
        return true;
      }
      catch(Exception exception)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString());
        return false;
      }
      finally
      {
        if(null!=bitmapStream)
        {
          bitmapStream.Close();
          bitmapStream.Dispose();
          bitmapStream.Dispose();
        }
      }
    }

    /// <summary>
    /// Resize image to given width maintaining aspect ration for height.
    /// If aspect ration is used for CNN then you will need to introduce padding.  For instance 640,640 image and then paste the aspect 
    /// mainained image onto that.  The CNN will then have to learn that black means nothing.  Resizing is the recommended way to go.
    /// </summary>    
    public bool Resize(int newWidth)
    {
      double aspectRatio=(double)bitmap.Width/(double)bitmap.Height;
      int newHeight=(int)(((double)newWidth)/aspectRatio);
      if(0!=newHeight%2)newHeight++;
      return Resize(newWidth,newHeight);
    }

    /// <summary>
    /// Resize image to given width and height.  The documenation on CNN's indicates that resizing will work better than maintaining apsect 
    /// ration with padding
    /// </summary>  
    public bool Resize(int newWidth, int newHeight)
    {
      Graphics graphics = null;
      ImageAttributes imageAttributes = null;
      try
      {
        Validate();
        Rectangle destRect=new Rectangle(0,0,newWidth,newHeight);
        Bitmap destBitmap = new Bitmap(newWidth,newHeight);

        destBitmap.SetResolution(bitmap.HorizontalResolution,bitmap.VerticalResolution);
        graphics = Graphics.FromImage(destBitmap);
        graphics.CompositingMode = CompositingMode.SourceCopy;
        graphics.CompositingQuality = CompositingQuality.HighQuality;
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.SmoothingMode = SmoothingMode.HighQuality;
        graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
        imageAttributes = new ImageAttributes();
        imageAttributes.SetWrapMode(WrapMode.TileFlipXY);
        graphics.DrawImage(bitmap, destRect, 0,0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, imageAttributes);
        bitmap.Dispose();
        bitmap=destBitmap;
        imageAttributes.Dispose();
        graphics.Dispose();
        return true;
      }
      catch(Exception exception)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString());
        return false;
      }
      finally
      {
        if(null!=graphics)
        {
          graphics.Dispose();
          graphics=null;
        }
        if(null!=imageAttributes)
        {
          imageAttributes.Dispose();
          imageAttributes=null;
        }
      }
    }

    public void CreateImage(int width, int height,PointMapping pointMapping)
    {
      this.width=width;
      this.height=height;
      this.pointMapping=pointMapping;
      bitmap=new Bitmap(width,height,System.Drawing.Imaging.PixelFormat.Format24bppRgb);
      Validate();
      graphics=Graphics.FromImage(bitmap);
    }

    // This is the method that is currently being used in the CNNClient.Predict to send the image stream to Flask
		public Stream SaveBlackAndWhiteJPG()
		{
			Validate();
			Bitmap bwBitmap=ToBlackAndWhite(bitmap);
		  Stream memoryStream=new MemoryStream();
			BitmapExtensions.SaveJPG100(bwBitmap,memoryStream);
			memoryStream.Position=0;
			bwBitmap.Dispose();
			return memoryStream;
		}

    // Convert bitmap to stream for use in CNNClient
    public Stream ToStream()
    {
			Validate();
		  Stream memoryStream=new MemoryStream();
			BitmapExtensions.SaveJPG100(bitmap,memoryStream);
			memoryStream.Position=0;
			return memoryStream;
    }

    public void SaveGrayScaleJPG(String pathFileName)
    {
      Validate();
			Bitmap bwBitmap=ToGrayScale(bitmap);
			Save(pathFileName,bwBitmap);
			bwBitmap.Dispose();
    }

    // This is the method that is currently being used in the CNNProcessor to generate images
    public void SaveBlackAndWhiteJPG(String pathFileName)
    {
      Validate();
			Bitmap bwBitmap=ToBlackAndWhite(bitmap);
			Save(pathFileName,bwBitmap);
			bwBitmap.Dispose();
    }

    public void Save(String pathFileName)
    {
      Save(pathFileName,null);
    }

		private void Save(String pathFileName,Bitmap altBitmap=null)
		{
			Validate();
			if(null==altBitmap)BitmapExtensions.SaveJPG100(bitmap,pathFileName);
			else BitmapExtensions.SaveJPG100(altBitmap,pathFileName);
		}

// Convert to 8 bits per pixel black and white
    private Bitmap ToBlackAndWhite(Bitmap bmp)
		{
        Bitmap result = new Bitmap(bmp.Width, bmp.Height, System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
        BitmapData data = result.LockBits(new Rectangle(0, 0, result.Width, result.Height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
        byte[] bytes = new byte[data.Height * data.Stride];
        Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
        for (int y = 0; y < bmp.Height; y++)
				{
          for (int x = 0; x < bmp.Width; x++)
					{
            Color color = bmp.GetPixel(x, y);
            byte rgb = (byte)((color.R + color.G + color.B) / 3);
						if(rgb>0)rgb=255;
						bytes[y * data.Stride + x] = rgb;
          }
        }
        Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
        result.UnlockBits(data);
        return result;
    }

    public void RotateRight()
    {
      Bitmap result = new Bitmap(bitmap.Height, bitmap.Width);  

      for(int rowIndex=0;rowIndex<bitmap.Height;rowIndex++)
      {
        for(int colIndex=0;colIndex<bitmap.Width;colIndex++)
        {
          Color color=GetPixel(rowIndex,colIndex);
          result.SetPixel((bitmap.Width-1)-colIndex,rowIndex,color);
        }
      }

      bitmap.Dispose();
      bitmap=result;
      width=bitmap.Width;
      height=bitmap.Height;
    }

    public void RotateLeft()
    {
      Bitmap result = new Bitmap(bitmap.Height, bitmap.Width);  

      for(int rowIndex=0;rowIndex<bitmap.Height;rowIndex++)
      {
        for(int colIndex=0;colIndex<bitmap.Width;colIndex++)
        {
          Color color=GetPixel(rowIndex,colIndex);
          result.SetPixel(colIndex, (bitmap.Width-1)-rowIndex,color);
        }
      }
      bitmap.Dispose();
      bitmap=result;
      width=bitmap.Width;
      height=bitmap.Height;
    }

    public void ToGrayScale()
    {
      Validate();
      Bitmap grayScaleBitmap = ToGrayScale(bitmap);
      bitmap.Dispose();
      bitmap=grayScaleBitmap;
      width=bitmap.Width;
      height=bitmap.Height;
    }

    private Bitmap ToGrayScale(Bitmap bmp)
    {
      Bitmap bitmap = new Bitmap(bmp.Width, bmp.Height);

      for (int i = 0; i < bmp.Width; i++)
      {
        for (int x = 0; x < bmp.Height; x++)
        {
          Color oc = bmp.GetPixel(i, x);
          int grayScale = (int)((oc.R * 0.3) + (oc.G * 0.59) + (oc.B * 0.11));
          Color nc = Color.FromArgb(oc.A, grayScale, grayScale, grayScale);
          bitmap.SetPixel(i, x, nc);
        }
      }
      return bitmap;
    }

    public void Blur(Int32 blurSize)
    {
      Bitmap blurBmp=Blur(bitmap, new Rectangle(0, 0, bitmap.Width, bitmap.Height), blurSize);
      bitmap.Dispose();
      bitmap=blurBmp;
      height=bitmap.Height;
      width=bitmap.Width;
    }

    private static Bitmap Blur(Bitmap image, Rectangle rectangle, Int32 blurSize)
    {
      Bitmap blurred = new Bitmap(image.Width, image.Height);

      // make an exact copy of the bitmap provided
      using (Graphics graphics = Graphics.FromImage(blurred))
          graphics.DrawImage(image, new Rectangle(0, 0, image.Width, image.Height), new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel);

      // look at every pixel in the blur rectangle
      for (int xx = rectangle.X; xx < rectangle.X + rectangle.Width; xx++)
      {
          for (int yy = rectangle.Y; yy < rectangle.Y + rectangle.Height; yy++)
          {
            int avgR = 0, avgG = 0, avgB = 0;
            int blurPixelCount = 0;

            // average the color of the red, green and blue for each pixel in the
            // blur size while making sure you don't go outside the image bounds
            for (int x = xx; (x < xx + blurSize && x < image.Width); x++)
            {
                for (int y = yy; (y < yy + blurSize && y < image.Height); y++)
                {
                    Color pixel = blurred.GetPixel(x, y);

                    avgR += pixel.R;
                    avgG += pixel.G;
                    avgB += pixel.B;

                    blurPixelCount++;
                }
            }

            avgR = avgR / blurPixelCount;
            avgG = avgG / blurPixelCount;
            avgB = avgB / blurPixelCount;

            // now that we know the average for the blur size, set each pixel to that color
            for (int x = xx; x < xx + blurSize && x < image.Width && x < rectangle.Width; x++)
            {
              for (int y = yy; y < yy + blurSize && y < image.Height && y < rectangle.Height; y++)
              {
                blurred.SetPixel(x, y, Color.FromArgb(avgR, avgG, avgB));
              }
            }
          }
      }
      return blurred;
    }

    public Color GetPixel(int x,int y)
    {
      return bitmap.GetPixel(x,y);
    }
    public void SetPixel(int x, int y,Color color)
    {
      bitmap.SetPixel(x,y,color);
    }
    public Point TranslatePoint(Point point)
    {
      return pointMapping.TranslatePoint(point);
    }
    public void Validate()
    {
      if(null==bitmap)throw new InvalidDataException("The image has not been initialized");
    }
    public int Width
    {
      get{return width;}
    }
    public int Height
    {
      get{return height;}
    }
    public void Fill(Brush brush)
    {
      Validate();
      Region region=new Region(new Rectangle(0,0,bitmap.Width,bitmap.Height));
      graphics.FillRegion(brush,region);
    }

    public void DrawPoint(Pen pen,Point drawPoint)
    {
      Validate();
      Point txPoint=pointMapping.MapPoint(drawPoint);
      graphics.DrawImage(bitmap,txPoint);
    }
    public void AddNoise(Color color,double percentDecimal)
    {
      Random random=new Random();
      int amount=(int)(percentDecimal*(Width*Height));
      for(int index=0;index<amount;index++)
      {
        int row=random.Next(Height-1);
        int col=random.Next(Width-1);
        SetPixel(row,col,color);
      }
    }
    public void DrawLine(Pen pen,Point srcPoint,Point dstPoint)
    {
      Validate();
      Point txSrcPoint=pointMapping.MapPoint(srcPoint);
      Point txDstPoint=pointMapping.MapPoint(dstPoint);
      graphics.DrawLine(pen,txSrcPoint,txDstPoint);
    }
		public void DrawPath(Pen pen,LineSegments lineSegments)
		{
		  GraphicsPath graphicsPath=new GraphicsPath();
			foreach(LineSegment lineSegment in lineSegments)
			{
			  graphicsPath.AddLine(pointMapping.MapPoint(lineSegment.P1),pointMapping.MapPoint(lineSegment.P2));
			}
			graphics.DrawPath(pen,graphicsPath);
		}
    public void DrawCircle(Brush brush,Point pt,float size=1.00f)
    {
      Validate();
      Point txPoint=pointMapping.MapPoint(pt);
      txPoint.X-=(int)(size/2f);
      txPoint.Y-=(int)(size/2F);
      Rectangle boundingRectangle=new Rectangle();
      boundingRectangle.X=txPoint.X;
      boundingRectangle.Y=txPoint.Y;
      boundingRectangle.Width=(int)size;
      boundingRectangle.Height=(int)size;
      graphics.FillEllipse(brush,boundingRectangle);
    }
  }
}
