using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Tesseract.Internal;
using Tesseract.Interop;

namespace Tesseract
{
    public unsafe sealed class Pix : DisposableBase
    {
        #region Constants

        
        public const int DefaultBinarySearchReduction = 2; 

        public const int DefaultBinaryThreshold = 130;

        
        
        
        private const float VerySmallAngle = 0.001F;

        private static readonly List<int> AllowedDepths = new List<int> { 1, 2, 4, 8, 16, 32 };

        
        
        
        private static readonly Dictionary<string, ImageFormat> imageFomatLookup = new Dictionary<string, ImageFormat>
        {
            { ".jpg", ImageFormat.JfifJpeg },
            { ".jpeg", ImageFormat.JfifJpeg },
            { ".gif", ImageFormat.Gif },
            { ".tif", ImageFormat.Tiff },
            { ".tiff", ImageFormat.Tiff },
            { ".png", ImageFormat.Png },
            { ".bmp", ImageFormat.Bmp }
        };

        #endregion Constants

        #region Fields

        private PixColormap colormap;

        #endregion Fields

        #region Create\Load methods

        
        
        
        
        
        
        
        private Pix(IntPtr handle)
        {
            if (handle == IntPtr.Zero) throw new ArgumentNullException(nameof(handle));

            Handle = new HandleRef(this, handle);
            Width = LeptonicaApi.Native.pixGetWidth(Handle);
            Height = LeptonicaApi.Native.pixGetHeight(Handle);
            Depth = LeptonicaApi.Native.pixGetDepth(Handle);

            var colorMapHandle = LeptonicaApi.Native.pixGetColormap(Handle);
            if (colorMapHandle != IntPtr.Zero)
            {
                colormap = new PixColormap(colorMapHandle);
            }
        }

        public static Pix Create(int width, int height, int depth)
        {
            if (!AllowedDepths.Contains(depth))
                throw new ArgumentException("Depth must be 1, 2, 4, 8, 16, or 32 bits.", nameof(depth));

            if (width <= 0) throw new ArgumentException("Width must be greater than zero", nameof(width));
            if (height <= 0) throw new ArgumentException("Height must be greater than zero", nameof(height));

            var handle = LeptonicaApi.Native.pixCreate(width, height, depth);
            if (handle == IntPtr.Zero) throw new InvalidOperationException("Failed to create pix, this normally occurs because the requested image size is too large, please check Standard Error Output.");

            return Create(handle);
        }

        public static Pix Create(IntPtr handle)
        {
            if (handle == IntPtr.Zero) throw new ArgumentException("Pix handle must not be zero (null).", nameof(handle));

            return new Pix(handle);
        }

        public static Pix LoadFromFile(string filename)
        {
            var pixHandle = LeptonicaApi.Native.pixRead(filename);
            if (pixHandle == IntPtr.Zero)
            {
                throw new IOException($"Failed to load image '{filename}'.");
            }
            return Create(pixHandle);
        }

        public static Pix LoadTiffFromMemory(byte[] bytes)
        {
            IntPtr handle;
            fixed (byte* ptr = bytes)
            {
                handle = LeptonicaApi.Native.pixReadMemTiff(ptr, bytes.Length, 0);
            }
            if (handle == IntPtr.Zero)
            {
                throw new IOException("Failed to load image from memory.");
            }
            return Create(handle);
        }

        public static Pix LoadFromMemory(byte[] bytes)
        {
            IntPtr handle;
            fixed (byte* ptr = bytes)
            {
                handle = LeptonicaApi.Native.pixReadMem(ptr, bytes.Length);
            }
            if (handle == IntPtr.Zero)
            {
                throw new IOException("Failed to load image from memory.");
            }
            return Create(handle);
        }

        #endregion Create\Load methods

        #region Properties

        public PixColormap Colormap
        {
            get { return colormap; }
            set
            {
                if (value != null)
                {
                    if (LeptonicaApi.Native.pixSetColormap(Handle, value.Handle) == 0)
                    {
                        colormap = value;
                    }
                }
                else
                {
                    if (LeptonicaApi.Native.pixDestroyColormap(Handle) == 0)
                    {
                        colormap = null;
                    }
                }
            }
        }

        public int Depth { get; }

        public int Height { get; }

        public int Width { get; }

        public HandleRef Handle { get; private set; }

        public PixData GetData() => new PixData(this);


        #endregion Properties

        #region Save methods

        
        
        
        
        
        public void Save(string filename, ImageFormat? format = null)
        {
            ImageFormat actualFormat;
            if (!format.HasValue)
            {
                var extension = Path.GetExtension(filename).ToLowerInvariant();
                if (!imageFomatLookup.TryGetValue(extension, out actualFormat))
                {
                    
                    actualFormat = ImageFormat.Default;
                }
            }
            else
            {
                actualFormat = format.Value;
            }

            if (LeptonicaApi.Native.pixWrite(filename, Handle, actualFormat) != 0)
            {
                throw new IOException($"Failed to save image '{filename}'.");
            }
        }

        public byte[] SaveToMemory(ImageFormat format)
        {
            
            var result = LeptonicaApi.Native.pixWriteMem(out byte* buffer, out int size, Handle, format);
            if (result != 0)
            {
                throw new IOException("Failed to save image to memory");
            }
            var ret =  new byte[size];
            Marshal.Copy((IntPtr) buffer, ret, 0, size);
            
            
            
            
            return ret;
        }
        #endregion Save methods

        #region Clone

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix Clone()
        {
            var clonedHandle = LeptonicaApi.Native.pixClone(Handle);
            return new Pix(clonedHandle);
        }

        #endregion Clone

        #region Image manipulation

        
        
        
        
        
        
        
        
        
        public Pix BinarizeOtsuAdaptiveThreshold(int sx, int sy, int smoothx, int smoothy, float scorefract)
        {
            Guard.Verify(Depth == 8, "Image must have a depth of 8 bits per pixel to be binerized using Otsu.");
            Guard.Require("sx", sx >= 16, "The sx parameter must be greater than or equal to 16");
            Guard.Require("sy", sy >= 16, "The sy parameter must be greater than or equal to 16");

            IntPtr ppixd;
            int result = LeptonicaApi.Native.pixOtsuAdaptiveThreshold(Handle, sx, sy, smoothx, smoothy, scorefract, out var ppixth, out ppixd);

            if (ppixth != IntPtr.Zero)
            {
                
                LeptonicaApi.Native.pixDestroy(ref ppixth);
            }

            if (result == 1) throw new TesseractException("Failed to binarize image.");

            return new Pix(ppixd);
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix BinarizeSauvola(int whsize, float factor, bool addborder)
        {
            Guard.Verify(Depth == 8, "Source image must be 8bpp");
            Guard.Verify(Colormap == null, "Source image must not be color mapped.");
            Guard.Require("whsize", whsize >= 2, "The window half-width (whsize) must be greater than 2.");
            int maxWhSize = Math.Min((Width - 3) / 2, (Height - 3) / 2);
            Guard.Require("whsize", whsize < maxWhSize, "The window half-width (whsize) must be less than {0} for this image.", maxWhSize);
            Guard.Require("factor", factor >= 0, "Factor must be greater than zero (0).");

            IntPtr ppixm, ppixsd, ppixth, ppixd;
            int result = LeptonicaApi.Native.pixSauvolaBinarize(Handle, whsize, factor, addborder ? 1 : 0, out ppixm, out ppixsd, out ppixth, out ppixd);

            

            if (ppixm != IntPtr.Zero)
            {
                LeptonicaApi.Native.pixDestroy(ref ppixm);
            }

            if (ppixsd != IntPtr.Zero)
            {
                LeptonicaApi.Native.pixDestroy(ref ppixsd);
            }

            if (ppixth != IntPtr.Zero)
            {
                LeptonicaApi.Native.pixDestroy(ref ppixth);
            }

            if (result == 1) throw new TesseractException("Failed to binarize image.");

            return new Pix(ppixd);
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix BinarizeSauvolaTiled(int whsize, float factor, int nx, int ny)
        {
            Guard.Verify(Depth == 8, "Source image must be 8bpp");
            Guard.Verify(Colormap == null, "Source image must not be color mapped.");
            Guard.Require("whsize", whsize >= 2, "The window half-width (whsize) must be greater than 2.");
            int maxWhSize = Math.Min((Width - 3) / 2, (Height - 3) / 2);
            Guard.Require("whsize", whsize < maxWhSize, "The window half-width (whsize) must be less than {0} for this image.", maxWhSize);
            Guard.Require("factor", factor >= 0, "Factor must be greater than zero (0).");

            IntPtr ppixth, ppixd;
            int result = LeptonicaApi.Native.pixSauvolaBinarizeTiled(Handle, whsize, factor, nx, ny, out ppixth, out ppixd);

            
            if (ppixth != IntPtr.Zero)
            {
                LeptonicaApi.Native.pixDestroy(ref ppixth);
            }

            if (result == 1) throw new TesseractException("Failed to binarize image.");

            return new Pix(ppixd);
        }

        
        
        
        
        
        
        
        public Pix ConvertRGBToGray(float rwt, float gwt, float bwt)
        {
            Guard.Verify(Depth == 32, "The source image must have a depth of 32 (32 bpp).");
            Guard.Require("rwt", rwt >= 0, "All weights must be greater than or equal to zero; red was not.");
            Guard.Require("gwt", gwt >= 0, "All weights must be greater than or equal to zero; green was not.");
            Guard.Require("bwt", bwt >= 0, "All weights must be greater than or equal to zero; blue was not.");

            var resultPixHandle = LeptonicaApi.Native.pixConvertRGBToGray(Handle, rwt, gwt, bwt);
            if (resultPixHandle == IntPtr.Zero) throw new TesseractException("Failed to convert to grayscale.");
            return new Pix(resultPixHandle);
        }
        
        
        
        
        public Pix ConvertRGBToGray()
        {
            return ConvertRGBToGray(0, 0, 0);
        }

        
        
        
        
        
        
        
        
        
        public Pix Deskew()
        {
            Scew scew;
            return Deskew(DefaultBinarySearchReduction, out scew);
        }

        
        
        
        
        
        
        
        
        
        
        public Pix Deskew(out Scew scew)
        {
            return Deskew(DefaultBinarySearchReduction, out scew);
        }

        
        
        
        
        
        
        
        
        
        
        
        public Pix Deskew(int redSearch, out Scew scew)
        {
            return Deskew(ScewSweep.Default, redSearch, DefaultBinaryThreshold, out scew);
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix Deskew(ScewSweep sweep, int redSearch, int thresh, out Scew scew)
        {
            var resultPixHandle = LeptonicaApi.Native.pixDeskewGeneral(Handle, sweep.Reduction, sweep.Range, sweep.Delta, redSearch, thresh, out var pAngle, out var pConf);
            if (resultPixHandle == IntPtr.Zero) throw new TesseractException("Failed to deskew image.");
            scew = new Scew(pAngle, pConf);
            return new Pix(resultPixHandle);
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix Rotate(float angle, RotationMethod method = RotationMethod.AreaMap, RotationFill fillColor = RotationFill.White, int? width = null, int? height = null)
        {
            if (width == null) width = Width;
            if (height == null) height = Height;

            if (Math.Abs(angle) < VerySmallAngle) return Clone();

            IntPtr resultHandle;

            var rotations = 2 * angle / Math.PI;
            resultHandle = 
                Math.Abs(rotations - Math.Floor(rotations)) < VerySmallAngle 
                ? LeptonicaApi.Native.pixRotateOrth(Handle, (int)rotations) 
                : LeptonicaApi.Native.pixRotate(Handle, angle, method, fillColor, width.Value, height.Value);

            if (resultHandle == IntPtr.Zero) throw new LeptonicaException("Failed to rotate image around it's centre.");

            return new Pix(resultHandle);
        }

        public Pix ClipRectangle(Rect rect)
        {
            const Int32 PIX_SRC = 0xc << 1;
            if (rect.X1 > Width || rect.Y1 > Height || rect.X2 < 0 || rect.Y2 < 0)
            {
                return null;
            }

            var ret = Create(rect.Width, rect.Height, Depth);
            ret.Colormap = Colormap;
            LeptonicaApi.Native.pixRasterOp(ret.Handle, 0, 0, rect.Width, rect.Height, PIX_SRC, Handle, rect.X1, rect.Y1);

            return ret;
        }


        #endregion Image manipulation

        #region Scaling


        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public Pix Scale(float scaleX, float scaleY)
        {
            IntPtr result = LeptonicaApi.Native.pixScale(Handle, scaleX, scaleY);

            if (result == IntPtr.Zero) throw new InvalidOperationException("Failed to scale pix.");

            return new Pix(result);
        }

        #endregion

        #region Disposal


        protected override void Dispose(bool disposing)
        {
            var tmpHandle = Handle.Handle;
            LeptonicaApi.Native.pixDestroy(ref tmpHandle);
            Handle = new HandleRef(this, IntPtr.Zero);
        }

        #endregion Disposal
    }
}
