Imports System
Imports System.Runtime.InteropServices
Imports System.Drawing
Imports System.Diagnostics

''' <summary>
''' Creates and stores a signature of a <see cref="Bitmap"/>.
''' </summary>
''' <remarks>
''' <para>This class stores the signature independantly of the original bitmap. If the bitmap changes, a new signature should be generated using the <see cref="ImageSignature.CreateSignature"/> method.</para>
''' <para>Because the signature is independant of the bitmap, no reference to the original bitmap is kept.</para>
''' <para>You can compare two signatures using the <see cref="ImageSignature.Difference"/> method.</para>
''' </remarks>
<Serializable()> Public Class ImageSignature
    'Resolution of the signature, both horizontally and vertically
    Private Const NUMBEROFDATAPOINTS As Integer = 500

    'Stores the actual array. Back the DataPoint properties.
    Private _DataPoints(NUMBEROFDATAPOINTS - 1, NUMBEROFDATAPOINTS - 1) As RGBALarge

    ''' <summary>
    ''' Creates a new instance of the <see cref="ImageSignature"/> class and stores a signature of <paramref name="FromBitmap"/> in it.
    ''' </summary>
    ''' <param name="FromBitmap">The <see cref="Bitmap"/> from which to create the signature.</param>
    ''' <remarks></remarks>
    Public Sub New(ByRef FromBitmap As Bitmap)
        CreateSignature(FromBitmap)
    End Sub

    ''' <summary>
    ''' Creates a new instance of the <see cref="ImageSignature"/> class.
    ''' </summary>
    ''' <remarks>This constructor creates an empty <c>ImageSignature</c> class.</remarks>
    Public Sub New()
        MyBase.new()
    End Sub

    ''' <summary>
    ''' Creates a signature from <paramref name="FromBitmap"/>
    ''' </summary>
    ''' <param name="FromBitmap">The <see cref="Bitmap"/> from which to create the signature.</param>
    ''' <remarks></remarks>
    Public Sub CreateSignature(ByRef FromBitmap As Bitmap)
        CreateSignature(FromBitmap, Nothing)
    End Sub

    ''' <summary>
    ''' Creates a signature from <paramref name="FromBitmap"/> and reports the progress to <paramref name="BackgroundWorker"/>.
    ''' </summary>
    ''' <param name="FromBitmap">The <see cref="Bitmap"/> from which to create the signature.</param>
    ''' <param name="BackgroundWorker">The <see cref="System.ComponentModel.BackgroundWorker"/> to which to report the progress of the function.</param>
    ''' <remarks>In addition to reporting the progress of the method to the <c>BackgroundWorker</c>, the method can also be canceled by it.</remarks>
    Public Sub CreateSignature(ByRef FromBitmap As Bitmap, ByRef BackgroundWorker As System.ComponentModel.BackgroundWorker)
        'Check to see if the operation is being cancelled
        If BackgroundWorker IsNot Nothing Then
            If BackgroundWorker.CancellationPending Then Return
        End If

        Dim ImageSize As Size = FromBitmap.Size

        'hack. images smaller than NUMBEROFDATAPOINTS in either dimention need that demention to be resized
        If ImageSize.Width < NUMBEROFDATAPOINTS Then ImageSize.Width = NUMBEROFDATAPOINTS
        If ImageSize.Height < NUMBEROFDATAPOINTS Then ImageSize.Height = NUMBEROFDATAPOINTS

        'hack. image must be square. expand it so it's square.
        If ImageSize.Width > ImageSize.Height Then ImageSize.Height = ImageSize.Width
        If ImageSize.Height > ImageSize.Width Then ImageSize.Width = ImageSize.Height

        'hack. image size must be even multiple of NUMBEROFDATAPOINTS
        'the math here is simple: if there's a remainder when deviding imagesize by NUMBEROFDATAPOINTS, 
        'add the remainder so size is a perfect multiple.
        If ImageSize.Width Mod NUMBEROFDATAPOINTS > 0 Then ImageSize.Width += NUMBEROFDATAPOINTS - (ImageSize.Width Mod NUMBEROFDATAPOINTS)
        If ImageSize.Height Mod NUMBEROFDATAPOINTS > 0 Then ImageSize.Height += NUMBEROFDATAPOINTS - (ImageSize.Height Mod NUMBEROFDATAPOINTS)

        Dim Bits(ImageSize.Width * ImageSize.Height) As Integer
        Dim CellSize As New Size(ImageSize.Width \ NUMBEROFDATAPOINTS, ImageSize.Height \ NUMBEROFDATAPOINTS)
        'Dim LastCellSize As New Size(ImageSize.Width Mod NUMBEROFDATAPOINTS, ImageSize.Height Mod NUMBEROFDATAPOINTS)
        Dim CurrentCellX, CurrentCellY As Integer
        Dim CellTotalR, CellTotalG, CellTotalB, CellTotalA As UInteger
        Dim i As Integer

        Dim handle As GCHandle = GCHandle.Alloc(Bits, GCHandleType.Pinned)

        Dim ptr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(Bits, 0)

        Using bmp As New Bitmap(ImageSize.Width, ImageSize.Height, ImageSize.Width * 4, Imaging.PixelFormat.Format32bppArgb, ptr)

            Using g As Graphics = Graphics.FromImage(bmp)
                'g.CompositingMode = Drawing2D.CompositingMode.SourceCopy
                g.SmoothingMode = Drawing2D.SmoothingMode.None

                g.InterpolationMode = Drawing2D.InterpolationMode.NearestNeighbor
                g.DrawImage(FromBitmap, 0, 0, ImageSize.Width, ImageSize.Height)
            End Using

            For CurrentCellY = 0 To NUMBEROFDATAPOINTS - 1
                If BackgroundWorker IsNot Nothing Then
                    BackgroundWorker.ReportProgress(CurrentCellY / NUMBEROFDATAPOINTS * 100)
                End If

                For CurrentCellX = 0 To NUMBEROFDATAPOINTS - 1
                    If BackgroundWorker IsNot Nothing Then
                        If BackgroundWorker.CancellationPending Then
                            handle.Free()
                            Return
                        End If
                    End If

                    Dim CurrentCell As Rectangle

                    With CurrentCell

                        .X = CurrentCellX * CellSize.Width
                        .Y = CurrentCellY * CellSize.Height

                        .Width = CellSize.Width
                        .Height = CellSize.Height

                        CellTotalA = 0 : CellTotalR = 0 : CellTotalG = 0 : CellTotalB = 0 : i = 0

                        'Now... average the current cell together
                        Dim x, y As Integer

                        For y = .Y To (.Y + .Height) - 1
                            For x = .X To (.X + .Width) - 1
                                If BackgroundWorker IsNot Nothing Then
                                    If BackgroundWorker.CancellationPending Then
                                        handle.Free()
                                        Return
                                    End If

                                End If

                                i += 1


                                CellTotalA += Color.FromArgb(Bits((y * ImageSize.Width) + x)).A
                                CellTotalR += Color.FromArgb(Bits((y * ImageSize.Width) + x)).R
                                CellTotalG += Color.FromArgb(Bits((y * ImageSize.Width) + x)).G
                                CellTotalB += Color.FromArgb(Bits((y * ImageSize.Width) + x)).B


                            Next
                        Next

                        _DataPoints(CurrentCellX, CurrentCellY) = New RGBALarge( _
                            ScaleToUInt(CellTotalA / i), _
                            ScaleToUInt(CellTotalR / i), _
                            ScaleToUInt(CellTotalG / i), _
                            ScaleToUInt(CellTotalB / i))

                    End With
                Next
            Next

        End Using

        handle.Free()

    End Sub

    ''' <summary>
    ''' Sets or returns a datapoint from a specified location.
    ''' </summary>
    ''' <param name="Location">A <see cref="Point"/> that specifies which datapoint to set or return.</param>
    ''' <value></value>
    ''' <returns>A <seealso cref="RGBALarge"/> that represents the datapoint that was specified by <paramref name="Location"/>.</returns>
    ''' <remarks>The location can not be greater than <see cref="NumberOfPoints"/> in either the X or the Y coordinates.</remarks>
    ''' <exception cref="IndexOutOfRangeException">Thrown if either coordinate of <paramref name="Location"/> is greater than <see cref="NumberOfPoints"/></exception>
    <System.ComponentModel.Browsable(False)> Default Public Property DataPoint(ByVal Location As Point) As RGBALarge
        <DebuggerStepThrough()> Get
            Return _DataPoints(Location.X, Location.Y)
        End Get
        <DebuggerStepThrough()> Set(ByVal value As RGBALarge)
            _DataPoints(Location.X, Location.Y) = value
        End Set
    End Property

    ''' <summary>
    ''' Sets or returns a datapoint from a specified X and Y coordinates.
    ''' </summary>
    ''' <param name="x">The X coordinate that specifies which datapoint to set or return.</param>
    ''' <param name="y">The Y coordinate that specifies which datapoint to set or return.</param>
    ''' <value></value>
    ''' <returns>A <seealso cref="RGBALarge"/> that represents the datapoint that was specified by <paramref name="x"/> and <paramref name="y"/>.</returns>
    ''' <remarks>The location can not be greater than <see cref="NumberOfPoints"/> in either the X or the Y coordinates.</remarks>
    ''' <exception cref="IndexOutOfRangeException">Thrown if either <paramref name="x"/> and <paramref name="y"/> is greater than <see cref="NumberOfPoints"/></exception>
    <System.ComponentModel.Browsable(False)> Default Public Property DataPoint(ByVal x As Integer, ByVal y As Integer) As RGBALarge
        <DebuggerStepThrough()> Get
            Return _DataPoints(x, y)
        End Get
        <DebuggerStepThrough()> Set(ByVal value As RGBALarge)
            _DataPoints(x, y) = value
        End Set
    End Property

    ''' <summary>
    ''' Sets or returns a datapoint from a specified index, treating the datapoint array as a single dimensional array.
    ''' </summary>
    ''' <param name="Index">An <see cref="Integer"/> that specifies which datapoint to set or return.</param>
    ''' <value></value>
    ''' <returns>A <seealso cref="RGBALarge"/> that represents the datapoint that was specified by <paramref name="Index"/>.</returns>
    ''' <remarks>The location can not be greater than <see cref="NumberOfPoints"/> squared.</remarks>
    ''' <exception cref="IndexOutOfRangeException">Thrown if  <paramref name="Index"/> is greater than <see cref="NumberOfPoints"/> squared.</exception>
    <System.ComponentModel.Browsable(False)> Default Public Property DataPoint(ByVal Index As Integer) As RGBALarge
        <DebuggerStepThrough()> Get
            Return _DataPoints(Index Mod NUMBEROFDATAPOINTS, Index \ NUMBEROFDATAPOINTS)
        End Get
        <DebuggerStepThrough()> Set(ByVal value As RGBALarge)
            _DataPoints(Index Mod NUMBEROFDATAPOINTS, Index \ NUMBEROFDATAPOINTS) = value
        End Set
    End Property

    ''' <summary>
    ''' Returns the size of the datapoint array.
    ''' </summary>
    ''' <value></value>
    ''' <returns>A <seealso cref="size"/> structure that specifies the size of the <seealso cref="ImageSignature.DataPoint"/> array.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)> Public ReadOnly Property Size() As Size
        <DebuggerStepThrough()> Get
            Return New Size(NUMBEROFDATAPOINTS, NUMBEROFDATAPOINTS)
        End Get
    End Property

    ''' <summary>
    ''' Returns the width of the datapoint array.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The width of the <seealso cref="ImageSignature.DataPoint"/> array.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)> Public ReadOnly Property Width() As Integer
        <DebuggerStepThrough()> Get
            Return Size.Width
        End Get
    End Property

    ''' <summary>
    ''' Returns the height of the datapoint array.
    ''' </summary>
    ''' <value></value>
    ''' <returns>The height of the <seealso cref="ImageSignature.DataPoint"/> array.</returns>
    ''' <remarks></remarks>
    <System.ComponentModel.Browsable(False)> Public ReadOnly Property Height() As Integer
        <DebuggerStepThrough()> Get
            Return Size.Height
        End Get
    End Property

    ''' <summary>
    ''' Represents an ARGB color with 128 bits of precision.
    ''' </summary>
    ''' <remarks>Each color component is stored as a 32-bit UInteger providing for four times the resolution of a standard (32bpp) 8-bit color component.</remarks>
    <Serializable()> Public Structure RGBALarge

        ''' <summary>
        ''' Stores the blue component value of this <see cref="RGBALarge"/> structure. 
        ''' </summary>
        ''' <remarks>This value ranges between 0 and <see cref="UInteger.MaxValue"/>.</remarks>
        Public Blue As UInteger

        ''' <summary>
        ''' Stores the green component value of this <see cref="RGBALarge"/> structure. 
        ''' </summary>
        ''' <remarks>This value ranges between 0 and <see cref="UInteger.MaxValue"/>.</remarks>
        Public Green As UInteger

        ''' <summary>
        ''' Stores the red component value of this <see cref="RGBALarge"/> structure. 
        ''' </summary>
        ''' <remarks>This value ranges between 0 and <see cref="UInteger.MaxValue"/>.</remarks>
        Public Red As UInteger

        ''' <summary>
        ''' Stores the alpha component value of this <see cref="RGBALarge"/> structure. 
        ''' </summary>
        ''' <remarks>This value ranges between 0 and <see cref="UInteger.MaxValue"/>.</remarks>
        Public Alpha As UInteger

        ''' <summary>
        ''' Constructs a new <see cref="RGBALarge"/> structure.
        ''' </summary>
        ''' <param name="Alpha">Initial value for <see cref="RGBALarge.Alpha"/></param>
        ''' <param name="Red">Initial value for <see cref="RGBALarge.Red"/></param>
        ''' <param name="Green">Initial value for <see cref="RGBALarge.Green"/></param>
        ''' <param name="Blue">Initial value for <see cref="RGBALarge.Blue"/></param>
        ''' <remarks></remarks>
        Sub New(ByVal Alpha As UInteger, ByVal Red As UInteger, ByVal Green As UInteger, ByVal Blue As UInteger)
            Me.Alpha = Alpha
            Me.Red = Red
            Me.Green = Green
            Me.Blue = Blue
        End Sub

    End Structure

    ''' <summary>
    ''' Scales a <see cref="Byte"/> to a <see cref="UInteger"/>.
    ''' </summary>
    ''' <param name="Byte">The value to scale.</param>
    ''' <returns>The value after scaling.</returns>
    ''' <remarks>This converts a <see cref="Byte"/> to a <see cref="UInteger"/> by treating <paramref name="Byte"/> as a percentage of <see cref="Byte.MaxValue"/> and mutiplying by <see cref="UInteger.MaxValue"/>.</remarks>
    Private Shared Function ScaleToUInt(ByVal [Byte] As Byte) As UInteger
        Return ([Byte] / Byte.MaxValue) * UInteger.MaxValue
    End Function

    ''' <summary>
    ''' Scales a <see cref="UInteger"/> to a <see cref="Byte"/>.
    ''' </summary>
    ''' <param name="UInteger">The value to scale.</param>
    ''' <returns>The value after scaling.</returns>
    ''' <remarks>This converts a <see cref="UInteger"/> to a <see cref="Byte"/> by treating <paramref name="UInteger"/> as a percentage of <see cref="UInteger.MaxValue"/> and mutiplying by <see cref="Byte.MaxValue"/>.</remarks>
    Private Shared Function ScaleToByte(ByVal [UInteger] As UInteger) As Byte
        Return ([UInteger] / UInteger.MaxValue) * Byte.MaxValue
    End Function

    ''' <summary>
    ''' Returns a Bitmap class instance that represents the signature.
    ''' </summary>
    ''' <returns>A new <see cref="Bitmap"/> that represents the signature. The size of the bitmap is identical to <see cref="ImageSignature.Size"/>.</returns>
    ''' <remarks>This function returns a new bitmap object every time. You must remember to dispose the returned bitmap when you're done using it. Otherwise you may end up with a memory leak.</remarks>
    Public Function ToBitmap() As Bitmap
        Dim bits(NUMBEROFDATAPOINTS ^ 2) As Integer
        Dim handle As GCHandle = GCHandle.Alloc(bits, GCHandleType.Pinned)
        Dim ptr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(bits, 0)
        Dim i As Integer
        Dim bmp As New Bitmap(NUMBEROFDATAPOINTS, NUMBEROFDATAPOINTS, NUMBEROFDATAPOINTS * 4, Imaging.PixelFormat.Format32bppArgb, ptr)

        For i = 0 To (NUMBEROFDATAPOINTS ^ 2) - 1
            bits(i) = Color.FromArgb(ScaleToByte(DataPoint(i).Alpha), ScaleToByte(DataPoint(i).Red), ScaleToByte(DataPoint(i).Green), ScaleToByte(DataPoint(i).Blue)).ToArgb
        Next

        handle.Free()
        Return bmp
    End Function

    ''' <summary>
    ''' Specifies the resolution (both horizontal and vertical) of the signature.
    ''' </summary>
    ''' <remarks>This value does not change except across multiple DLL versions of this class.</remarks>
    Public ReadOnly NumberOfPoints As Integer = NUMBEROFDATAPOINTS

    ''' <summary>
    ''' Returns the average difference between this signature and another signature.
    ''' </summary>
    ''' <param name="CompareWith">The <see cref="ImageSignature"/> to compare this one with.</param>
    ''' <param name="CompareAlpha">Set to <c>True</c> to specify that differences in the alpha channel should be considered. Set to <c>False</c> (default) otherwise.</param>
    ''' <returns>A <see cref="UInteger"/> that specifies the amount of difference between this signature and the one specified.</returns>
    ''' <remarks>The returned value ranges from <c>0</c> to <see cref="UInteger.MaxValue"/> with <c>0</c> meaning no difference and <c>UInteger.MaxValue</c> meaning complete difference.</remarks>
    Public Function Difference(ByVal CompareWith As ImageSignature, Optional ByVal CompareAlpha As Boolean = False) As UInteger
        Return Difference(Me, CompareWith, CompareAlpha)
    End Function

    ''' <summary>
    ''' Returns the average difference between two signatures.
    ''' </summary>
    ''' <param name="Comparitor1">The first <see cref="ImageSignature"/> to compare.</param>
    ''' <param name="Comparitor2">The second <see cref="ImageSignature"/> to compare.</param>
    ''' <param name="CompareAlpha">Set to <c>True</c> to specify that differences in the alpha channel should be considered. Set to <c>False</c> (default) otherwise.</param>
    ''' <returns>A <see cref="UInteger"/> that specifies the amount of difference between the two signatures.</returns>
    ''' <remarks>The returned value ranges from <c>0</c> to <see cref="UInteger.MaxValue"/> with <c>0</c> meaning no difference and <c>UInteger.MaxValue</c> meaning complete difference.</remarks>
    Public Shared Function Difference(ByVal Comparitor1 As ImageSignature, ByVal Comparitor2 As ImageSignature, Optional ByVal CompareAlpha As Boolean = False) As UInteger
        Dim x, y As Integer
        Dim RunningTotalR, RunningTotalG, RunningTotalB, RunningTotalA As ULong

        For y = 0 To NUMBEROFDATAPOINTS - 1
            For x = 0 To NUMBEROFDATAPOINTS - 1
                RunningTotalR += Math.Abs(CLng(Comparitor1(x, y).Red) - Comparitor2(x, y).Red)
                RunningTotalG += Math.Abs(CLng(Comparitor1(x, y).Green) - Comparitor2(x, y).Green)
                RunningTotalB += Math.Abs(CLng(Comparitor1(x, y).Blue) - Comparitor2(x, y).Blue)

                If CompareAlpha Then
                    RunningTotalA += Math.Abs(Comparitor1(x, y).Alpha - Comparitor2(x, y).Alpha)
                End If
            Next
        Next

        Dim a As New RGBALarge
        With a
            .Red = RunningTotalR / (NUMBEROFDATAPOINTS ^ 2)
            .Green = RunningTotalR / (NUMBEROFDATAPOINTS ^ 2)
            .Blue = RunningTotalR / (NUMBEROFDATAPOINTS ^ 2)

            If CompareAlpha Then
                .Alpha = RunningTotalR / (NUMBEROFDATAPOINTS ^ 2)
            End If

            Dim ret As UInteger

            If CompareAlpha Then
                ret = (.Red + .Green + .Blue + .Alpha) / 4
            Else
                ret = (.Red + .Green + .Blue) / 3
            End If

            Return ret
        End With

    End Function


End Class
