using SkiaSharp;

namespace MarketData.CNNProcessing
{
  public class ImageHelper : IDisposable
  {
      private SKBitmap bitmap = null;
      private PointMapping pointMapping;

      public ImageHelper()
      {
      }

      /// <summary>
      /// Copy constructor
      /// </summary>
      /// <param name="imageHelper"></param>
      public ImageHelper(ImageHelper imageHelper)
      {
        this.bitmap=Copy(imageHelper.bitmap);
        pointMapping = new PointMapping(imageHelper.pointMapping);
      }    

      public void Dispose()
      {
        DisposeAll();
      }

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

      private SKBitmap Copy(SKBitmap bitmap)
      {
        return bitmap.Copy(bitmap.ColorType);
      }      

      /// <summary>
      /// Load image from file
      /// </summary>
      /// <param name="pathFileName"></param>
      /// <returns></returns>
      public bool LoadImage(string pathFileName) 
      {
        using FileStream stream = new FileStream(pathFileName, FileMode.Open);
        return LoadImage(stream);
      } 

      /// <summary>
      /// Load image from stream retaining the color space and pixel depth of the source image
      /// </summary>
      /// <param name="stream"></param>
      /// <returns></returns>
      public bool LoadImage(Stream stream)
      {
        try
        {
          DisposeAll();
          using SKImage image=SKImage.FromEncodedData(stream);
          SKImageInfo imageInfo = image.Info;
          bitmap = new SKBitmap(image.Info);
          if(!image.ReadPixels (imageInfo, bitmap.GetPixels (), imageInfo.RowBytes,0, 0))
          {
            bitmap.Dispose ();
            bitmap = null;
            return false;
          }          
          pointMapping = new PointMapping(Width,Height,Width,0,Height,0);
          return true;
        }
        catch(Exception exception)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString());
          return false;
        }
      } 

      /// <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>    
      /// <param name="newWidth"></param>
      /// <returns></returns>
      /// 
      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)
      {
        try
        {
          Validate();
          SKImageInfo imageInfo = new SKImageInfo(newWidth, newHeight);
          SKBitmap newBitmap = bitmap.Resize(imageInfo,SKSamplingOptions.Default);
          bitmap.Dispose();
          bitmap = newBitmap;
          pointMapping = new PointMapping(Width,Height,Width,0,Height,0);          
          return true;
        }
        catch(Exception exception)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString());
          return false;
        }
      }  

      /// <summary>
      /// Create a new 32 bit bitmap with 8 bits per channel which includes 8 bits for the alpha channel where 0 = fully transparent and 255 = fully opaque
      /// </summary>
      /// <param name="width"></param>
      /// <param name="height"></param>
      /// <param name="pointMapping"></param>
      public void CreateImage(int width, int height,PointMapping pointMapping)
      {
        DisposeAll();
        this.pointMapping=pointMapping;
        bitmap=new SKBitmap(width,height,SKColorType.Rgba8888, SKAlphaType.Premul);
        Validate();
      }
      
      /// <summary>
      /// Create a new 32 bit bitmap with 8 bits per channel which includes 8 bits for the alpha channel where 0 = fully transparent and 255 = fully opaque
      /// </summary>
      /// <param name="width"></param>
      /// <param name="height"></param>
      public void CreateImage(int width, int height)
      {
        DisposeAll();
        this.pointMapping = new PointMapping(width, height, width-1, 0, height-1, 0, 0,0);
        bitmap=new SKBitmap(width,height,SKColorType.Rgba8888, SKAlphaType.Premul);
        Validate();
      }      

      /// <summary>
      /// Save the bitmap as black and white jpeg and return the stream
      /// This is the method that is currently being used in the CNNClient.Predict to send the image stream to Flask
      /// </summary>
      /// <returns></returns>
      public Stream SaveBlackAndWhiteJPG()
      {
        Validate();
        SKBitmap bwBitmap=ToBlackAndWhite(bitmap);
        Stream memoryStream=new MemoryStream();
        BitmapExtensions.SaveJPG100(bwBitmap,memoryStream);
        memoryStream.Position=0;
        bwBitmap.Dispose();
        return memoryStream;
      } 

      /// <summary>
      /// Convert bitmap to stream for use in CNNClient
      /// </summary>
      /// <param name="pathFileName"></param>
      /// <returns></returns>      
      public Stream ToStream()
      {
        Validate();
        Stream memoryStream=new MemoryStream();
        BitmapExtensions.SaveJPG100(bitmap,memoryStream);
        memoryStream.Position=0;
        return memoryStream;
      }  

      /// <summary>
      /// Save the bitmap as gray scale jpeg to the specified file
      /// </summary>
      /// <param name="pathFileName"></param>
      /// <returns></returns>
      public bool SaveGrayScaleJPG(String pathFileName)
      {
        Validate();
        SKBitmap bwBitmap=ToGrayScale(bitmap);
        BitmapExtensions.SaveJPG100(bwBitmap,pathFileName);
        bwBitmap.Dispose();
        return true;
      }                  

      /// <summary>
      /// Save the bitmap as black and white jpeg to the specified file
      /// </summary>
      /// <param name="pathFileName"></param>
      /// <returns></returns>
      public bool SaveBlackAndWhiteJPG(String pathFileName)
      {
        Validate();
        SKBitmap bwBitmap=ToBlackAndWhite(bitmap);
        BitmapExtensions.SaveJPG100(bwBitmap,pathFileName);
        bwBitmap.Dispose();
        return true;
      }    

      /// <summary>
      /// Save the bitmap to the specified file
      /// </summary>
      /// <param name="pathFileName"></param>
      public void Save(String pathFileName)
      {
        Save(pathFileName,null);
      }

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

      /// <summary>
      /// Save the bitmap in PNG format.  Note: PNG files support alpha channels, JPG files do not
      /// </summary>
      /// <param name="pathFileName"></param>
      /// <param name="altBitmap"></param>
      public void SavePng(String pathFileName,SKBitmap altBitmap=null)
      {
       Validate();
       if(null==altBitmap)BitmapExtensions.SavePNG100(bitmap,pathFileName);
       else BitmapExtensions.SavePNG100(altBitmap,pathFileName);
      }  


    // Convert to 8 bits per pixel black and white
      private SKBitmap ToBlackAndWhite(SKBitmap bitmap)
      {
        float n = 1.0f/3.0f;
        SKColorFilter bwColorFilter = SKColorFilter.CreateColorMatrix(new float[]
                      {
                          n, n, n, 0, 0,
                          n, n, n, 0, 0,
                          n, n, n, 0, 0,
                          0, 0, 0, 1, 0
                      });   

        SKBitmap dstBitmap = new SKBitmap(bitmap.Width, bitmap.Height,SKColorType.Gray8, SKAlphaType.Premul);
        using SKCanvas canvas = new SKCanvas(dstBitmap);
        using SKPaint paint = new SKPaint();
        paint.ColorFilter = bwColorFilter;
        canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint);
        return dstBitmap;
      }
      
      /// <summary>
      /// Rotates the bitmap right by 90 degrees
      /// </summary>
      /// <returns></returns>
      public bool RotateRight()
      {
        Validate();
        SKBitmap rotated = Rotate(bitmap, 90);
        bitmap.Dispose();
        bitmap=rotated;
        pointMapping = new PointMapping(Width,Height,Width,0,Height,0);         
        return true;
      }

      /// <summary>
      /// Rotates the bitmap left by 90 degrees
      /// </summary>
      /// <returns></returns>
      public bool RotateLeft()
      {
        Validate();
        SKBitmap rotated = Rotate(bitmap, -90);
        bitmap.Dispose();
        bitmap=rotated;
        pointMapping = new PointMapping(Width,Height,Width,0,Height,0);         
        return true;
      }

      /// <summary>
      /// Rotates the bitmap to the specified angle
      /// </summary>
      /// <param name="bitmap"></param>
      /// <param name="angle"></param>
      /// <returns></returns>
      private SKBitmap Rotate(SKBitmap bitmap, double angle)
      {
        double radians = Math.PI * angle / 180;
        float sine = (float)Math.Abs(Math.Sin(radians));
        float cosine = (float)Math.Abs(Math.Cos(radians));
        int originalWidth = bitmap.Width;
        int originalHeight = bitmap.Height;
        int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
        int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);

        SKBitmap rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);

        using (SKCanvas surface = new SKCanvas(rotatedBitmap))
        {
          surface.Clear();
          surface.Translate(rotatedWidth / 2, rotatedHeight / 2);
          surface.RotateDegrees((float)angle);
          surface.Translate(-originalWidth / 2, -originalHeight / 2);
          surface.DrawBitmap(bitmap, new SKPoint());
        }
        return rotatedBitmap;
      } 

      public void ToGrayScale()
      {
        Validate();
        SKBitmap grayScaleBitmap = ToGrayScale(bitmap);
        bitmap.Dispose();
        bitmap=grayScaleBitmap;
      }

      // Convert to 8 bits per pixel black and white
      private SKBitmap ToGrayScale(SKBitmap bitmap)
      {
        SKColorFilter grayScaleColorFilter = SKColorFilter.CreateColorMatrix(new float[]
                      {
                          0.2126f, 0.7152f, 0.0722f, 0, 0,
                          0.2126f, 0.7152f, 0.0722f, 0, 0,
                          0.2126f, 0.7152f, 0.0722f, 0, 0,
                          0,     0,     0,     1, 0
                      });

        SKBitmap dstBitmap = new SKBitmap(bitmap.Width, bitmap.Height,SKColorType.Gray8, SKAlphaType.Premul);
        using SKCanvas canvas = new SKCanvas(dstBitmap);
        using SKPaint paint = new SKPaint();
        paint.ColorFilter = grayScaleColorFilter;
        canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint);
        return dstBitmap;
      } 

      /// <summary>
      /// Adds a blur effect to the bitmap
      /// </summary>
      /// <param name="sigmaX"></param>
      /// <returns></returns>
      public bool Blur(float sigmaX)
      {
        Validate();
        SKBitmap blurredBitmap = Blur(bitmap, sigmaX);
        bitmap.Dispose();
        bitmap = blurredBitmap;
        return true;
      }
      
      /// <summary>
      /// Applies a blur effect to the bitmap
      /// </summary>
      /// <param name="image"></param>
      /// <param name="sigmaX"></param>
      /// <param name="sigmaY"></param>
      /// <returns></returns>
      private SKBitmap Blur(SKBitmap image, float sigmaX=5.0f, float sigmaY=5.0f)
      {
        SKImageFilter imageFilter = SKImageFilter.CreateBlur(sigmaX, sigmaY);
        SKBitmap blurredBitmap = new SKBitmap(image.Width, image.Height);
        using SKCanvas canvas = new SKCanvas(blurredBitmap);
        using SKPaint paint = new SKPaint()
        {
          ImageFilter = imageFilter
        };
        canvas.DrawBitmap(bitmap, new SKPoint(0,0), paint);
        return blurredBitmap;
      }

      /// <summary>
      /// GetPixel - No point translation
      /// </summary>
      /// <param name="x"></param>
      /// <param name="y"></param>
      /// <returns></returns>
      public SKColor GetPixel(int x,int y)
      {
        Validate();
        return bitmap.GetPixel(x, y);
      }

      /// <summary>
      /// SetPixel - No point translation
      /// </summary>
      /// <param name="x"></param>
      /// <param name="y"></param>
      /// <param name="color"></param>
      public void SetPixel(int x, int y,SKColor color)
      {
        Validate();
        bitmap.SetPixel(x, y, color);
      }

      public SKPoint TranslatePoint(SKPoint 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 bitmap.Width;
        }
      }

      public int Height
      {
        get
        {
          return bitmap.Height;
        }
      } 

      /// <summary>
      /// Fills the rectangle with the specified color
      /// </summary>
      /// <param name="color"></param>
      public void Fill(SKColor color)
      {
        Validate();
        SKRectI rect = new SKRectI(0, 0, Width, Height); // x, y, width, height
        bitmap.Erase(color, rect);
      }
      
      /// <summary>
      /// Make the bitmap transparent.  Note: PNG format preserves alpha channel while JPG format does not.. so save as PNG if using transparency 
      /// </summary>
      public void Transparent(SKColor color)
      {
        Validate();
        int pixelCount = Width * Height;
        SKColor[] colors = new SKColor[pixelCount];
        SKColor transparent = new SKColor(color.Red, color.Green, color.Blue, 0);
        for (int index = 0; index < pixelCount; index++)
        {
          colors[index] = transparent;
        }
        bitmap.Pixels = colors;
      }      

			/// <summary>
			/// DrawPoint - With translation
			/// </summary>
			/// <param name="color"></param>
			/// <param name="drawPoint"></param>
			public void DrawPoint(SKColor color, SKPoint drawPoint)
			{
				Validate();
				using SKCanvas canvas = new SKCanvas(bitmap);
				canvas.Clear();
				SKPoint txPoint = pointMapping.MapPoint(drawPoint);
				canvas.DrawPoint(drawPoint, color);
			}

      /// <summary>
      /// DrawPoint - with given strokeWidth and translation 
      /// </summary>
      /// <param name="color"></param>
      /// <param name="strokeWidth"></param>
      /// <param name="drawPoint"></param>
      public void DrawPoint(SKColor color, float strokeWidth, SKPoint drawPoint)
      {
        Validate();
        SKPoint txPoint = pointMapping.MapPoint(drawPoint);
        using SKCanvas canvas = new SKCanvas(bitmap);
        using SKPaint paint = new SKPaint();
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = strokeWidth; // Set the desired stroke width
        paint.Color = SKColors.Black;

        SKPoint[] points = new SKPoint[] { txPoint};

        canvas.DrawPoints(SKPointMode.Points, points, paint);       
      }

      /// <summary>
      /// Add noise to the image
      /// </summary>
      /// <param name="color"></param>
      /// <param name="percentDecimal"></param>
      public void AddNoise(SKColor color,double percentDecimal)
      {
        Random random=new Random();
        int amount=(int)(percentDecimal*(Width*Height));
        for(int index=0;index<amount;index++)
        {
          int x=random.Next(Width-1);
          int y=random.Next(Height-1);
          SetPixel(x, y,color);
        }
      } 

      /// <summary>
      /// Draw a line on the bitmap
      /// </summary>
      /// <param name="color"></param>
      /// <param name="strokeWidth"></param>
      /// <param name="srcPoint"></param>
      /// <param name="dstPoint"></param>
      public void DrawLine(SKColor color, float strokeWidth, SKPoint srcPoint, SKPoint dstPoint)
      {
        Validate();
        SKPoint txSrcPoint = pointMapping.MapPoint(srcPoint);
        SKPoint txDstPoint = pointMapping.MapPoint(dstPoint);
        using SKCanvas canvas = new SKCanvas(bitmap);
        using SKPaint paint = new SKPaint();
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = strokeWidth; // Set the desired stroke width
        paint.Color = color;
        canvas.DrawLine(txSrcPoint, txDstPoint, paint);
      }

      /// <summary>
      /// Draw text on the bitmap
      /// </summary>
      /// <param name="color"></param>
      /// <param name="strokeWidth"></param>
      /// <param name="srcPoint"></param>
      /// <param name="dstPoint"></param>
      public void DrawText(String text, SKPoint srcPoint, SKColor color, SKTextAlign align, SKFont font,SKPaintStyle paintStyle=SKPaintStyle.Fill,float strokeWidth = 1)
      {
        Validate();
        SKPoint txSrcPoint = pointMapping.MapPoint(srcPoint);
        using SKCanvas canvas = new SKCanvas(bitmap);
        using SKPaint paint = new SKPaint();
        paint.Style = paintStyle;
        paint.StrokeWidth = strokeWidth; // Set the desired stroke width
        paint.Color = color;
        canvas.DrawText(text,srcPoint, align, font, paint);
      }
      
      /// <summary>
      /// Create a rectangle with the specified text
      /// </summary>
      /// <param name="text"></param>
      /// <param name="textColor"></param>
      /// <param name="fillColor"></param>
      /// <param name="align"></param>
      /// <param name="font"></param>
      /// <param name="paintStyle"></param>
      /// <param name="strokeWidth"></param>
      public void CreateBoundedText(String text, SKColor textColor, SKColor fillColor, SKTextAlign align, SKFont font, SKPaintStyle paintStyle = SKPaintStyle.Fill, float strokeWidth = 1)
      {
        SKPoint srcPoint = new SKPoint(2, (int)font.Size + 4);
        int width = GetTextLength(text, font) + 4;
        int height = (int)font.Size * 2;
        CreateImage(width, height);
        Fill(fillColor);
        DrawLine(SKColors.Black, 1, new SKPoint(0, 0), new SKPoint(width - 1, 0)); // bottom left to right
        DrawLine(SKColors.Black, 1, new SKPoint(width - 1, 0), new SKPoint(width - 1, height - 1)); // up lefthand side
        DrawLine(SKColors.Black, 1, new SKPoint(0, height - 1), new SKPoint(width - 1, height - 1)); // top left to right
        DrawLine(SKColors.Black, 1, new SKPoint(0, height - 1), new SKPoint(0, 0)); // left hand side top to bottom
        DrawText(text, srcPoint, textColor, align, font);
      } 
      
      /// <summary>
      /// Gets the length of the text 
      /// </summary>
      /// <param name="text"></param>
      /// <param name="font"></param>
      /// <param name="paintStyle"></param>
      /// <param name="strokeWidth"></param>
      /// <returns></returns>
      public int GetTextLength(String text, SKFont font, SKPaintStyle paintStyle = SKPaintStyle.Fill, float strokeWidth = 1)
      {
        using SKPaint paint = new SKPaint();
        paint.Style = paintStyle;
        paint.StrokeWidth = strokeWidth; // Set the desired stroke width
        paint.Color = SKColors.Transparent;

        SKRect rect = new SKRect(0, 0, 0, 0);
        float textLength = font.MeasureText(text, out rect, paint);
        return (int)textLength;
      }
 
      /// <summary>
      /// Draws the path along the line segments
      /// </summary>
      /// <param name="color"></param>
      /// <param name="strokeWidth"></param>
      /// <param name="lineSegments"></param>
      public void DrawPath(SKColor color, float strokeWidth, LineSegments lineSegments)
      {
        Validate();

        using SKCanvas canvas = new SKCanvas(bitmap);
        using SKPaint paint = new SKPaint();
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = strokeWidth; // Set the desired stroke width
        paint.Color = SKColors.Black;
        foreach (LineSegment lineSegment in lineSegments)
        {
          SKPoint txSrcPoint = pointMapping.MapPoint(lineSegment.P1);
          SKPoint txDstPoint = pointMapping.MapPoint(lineSegment.P2);
          canvas.DrawLine(txSrcPoint, txDstPoint, paint);
        }
      }                  
      
      /// <summary>
      /// Draws a circle on the bitmap
      /// </summary>
      /// <param name="color"></param>
      /// <param name="center"></param>
      /// <param name="radius"></param>
      /// <returns></returns>
      public bool DrawCircle(SKColor color, SKPoint center, float radius = 1.00f)
      {
        Validate();
        SKPoint txPointCenter = pointMapping.MapPoint(center);
        using SKPaint paint = new SKPaint();
        paint.Color = color;
        paint.Style = SKPaintStyle.Fill;
        using SKCanvas canvas = new SKCanvas(bitmap);
        canvas.DrawCircle(txPointCenter, radius, paint);
        return true;
      }

      /// <summary>
      /// Draw a triangle centered inside the bounded area defined by the width and height of the bitmap.
      /// </summary>
      /// <param name="paintStroke">The paint stroke to to use to outline the triangle</param>
      /// <param name="paintFill">The paint stroke to use to fill the triangle</param>
      public void DrawTriangle(SKPaint paintStroke, SKPaint paintFill)
      {
        Validate();
        using SKPath path = new SKPath();
        float hLength = (float)Width / 2f;
        SKPoint txOrigin = pointMapping.MapPoint(new SKPoint(hLength,Height));
        using SKCanvas canvas = new SKCanvas(bitmap);
        path.MoveTo(txOrigin.X, txOrigin.Y);
        path.LineTo(txOrigin.X - hLength, txOrigin.Y + Height - 1f);
        path.LineTo(txOrigin.X + hLength - 1f, txOrigin.Y + Height - 1f);
        path.LineTo(txOrigin.X, txOrigin.Y);
        path.Close();
        canvas.DrawPath(path, paintStroke);
        canvas.DrawPath(path, paintFill);
      }

      /// <summary>
      /// Draw a triangle centered at the specified 'origin' and being 'length' width at it's base
      /// </summary>
      /// <param name="origin">The point origin</param>
      /// <param name="length">The length of the base of the triangle</param>
      /// <param name="paintStroke">The paint stroke to to use to outline the triangle</param>
      /// <param name="paintFill">The paint stroke to use to fill the triangle</param>
      public void DrawTriangle(SKPoint origin, int length, SKPaint paintStroke, SKPaint paintFill)
      {
        Validate();
        SKPath path = new SKPath();
        SKPoint txOrigin = pointMapping.MapPoint(origin);
        float lengthF = (float)length;
        float hLength = lengthF / 2f;
        float height = (float)Math.Sqrt(Math.Pow(lengthF, 2) - Math.Pow(hLength, 2));
        height/=1.15f;               // reduce the height by 15%
        path.MoveTo(txOrigin.X, txOrigin.Y);
        path.LineTo(txOrigin.X - hLength, txOrigin.Y + height);
        path.LineTo(txOrigin.X + hLength, txOrigin.Y + height);
        path.LineTo(txOrigin.X, txOrigin.Y);
        path.Close();
        using SKCanvas canvas = new SKCanvas(bitmap);
        canvas.DrawPath(path, paintStroke);
        canvas.DrawPath(path, paintFill);
      }
   }
}