Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Windows.Forms
Imports Nexamas.UI.Architecture
Imports Nexamas.UI.Composition
Imports Nexamas.UI.Controls
Imports Nexamas.UI.General
Imports Nexamas.UI.Layout
Imports Nexamas.UI.Motion
Imports Nexamas.UI.Rendering
Imports Nexamas.UI.TextRendering
Imports Nexamas.UI.Theming
Imports Nexamas.UI.Values
Imports SkiaSharp

Namespace Nexamas.UI.Components

    ''' <summary>
    ''' Official Nexamas UI visual rating surface. It owns bounded rating display,
    ''' pointer/keyboard value selection, half-step policy, readonly presentation,
    ''' RTL-aware hit testing, and MAS-owned premium drawing. Review storage,
    ''' analytics, feedback submission, moderation, persistence, recommendation
    ''' models, and domain scoring remain outside this control.
    ''' </summary>
    Public NotInheritable Class MASRating
        Inherits MASControlBase
        Implements IMASLayoutParticipant
        Implements IMASIntrinsicSizeContract

#Region "Fields"

        Private Shared ReadOnly _contract As MASResolvedComponentContract =
            MASArchitectureRuntime.ResolveContractOrThrow(GetType(MASRating))

        Private ReadOnly _primitives As New MASVisualPrimitivesPainter()
        Private ReadOnly _starRects As New List(Of SKRect)()
        Private _label As String = RatingTokens.DefaultLabel
        Private _value As Double = 0.0R
        Private _maximum As Integer = RatingTokens.DefaultMaximum
        Private _allowHalfValues As Boolean = True
        Private _isReadOnly As Boolean
        Private _hoverStarIndex As Integer = -1
        Private _pressedStarIndex As Integer = -1
        Private _hoverValue As Double = 0.0R
        Private _contentRect As SKRect = SKRect.Empty
        Private _starTrackRect As SKRect = SKRect.Empty
        Private _valueMotionRunner As MASMotionTimelineRunner
        Private _visualValue As Double = 0.0R
        Private _hasVisualValueOverride As Boolean
        Private _lastDpi As Single = 1.0F

#End Region

#Region "Constructor / Factory"

        Public Sub New()
            MyBase.New()
        End Sub

        Public Shared Function Create(Optional value As Double = 0.0R,
                                      Optional maximum As Integer = 5) As MASRating
            Dim control As New MASRating()
            control.Maximum = maximum
            control.Value = value
            Return control
        End Function

#End Region

#Region "Public API"

        Public Event ValueChanged As EventHandler

        Public Property Label As String
            Get
                Return _label
            End Get
            Set(value As String)
                Dim normalized As String = MASRatingPolicy.NormalizeLabel(value)
                If String.Equals(_label, normalized, StringComparison.Ordinal) Then Return
                _label = normalized
                RequestSizeLayoutRefreshForSizeAffectingChange("MASRating.Label")
                InvalidateVisual()
            End Set
        End Property

        Public Property Value As Double
            Get
                Return _value
            End Get
            Set(value As Double)
                SetValueInternal(value, True)
            End Set
        End Property

        Public Property Maximum As Integer
            Get
                Return _maximum
            End Get
            Set(value As Integer)
                Dim normalized As Integer = MASRatingPolicy.NormalizeMaximum(value)
                If _maximum = normalized Then Return
                _maximum = normalized
                _value = MASRatingPolicy.NormalizeValue(_value, _maximum, _allowHalfValues)
                StopValueMotion(syncToSemanticValue:=True)
                RequestSizeLayoutRefreshForSizeAffectingChange("MASRating.Maximum")
                InvalidateVisual()
                RaiseValueChangedSafe()
            End Set
        End Property

        Public Property AllowHalfValues As Boolean
            Get
                Return _allowHalfValues
            End Get
            Set(value As Boolean)
                If _allowHalfValues = value Then Return
                _allowHalfValues = value
                SetValueInternal(_value, True)
                InvalidateVisual()
            End Set
        End Property

        Public Property IsReadOnly As Boolean
            Get
                Return _isReadOnly
            End Get
            Set(value As Boolean)
                If _isReadOnly = value Then Return
                _isReadOnly = value
                ResetPointerState()
                InvalidateVisual()
            End Set
        End Property

        Public ReadOnly Property ValueText As String
            Get
                Return MASRatingPolicy.BuildValueText(_value, _maximum, _allowHalfValues)
            End Get
        End Property

        Public Function SetRating(value As Double) As MASRating
            Me.Value = value
            Return Me
        End Function

        Public Function ClearRating() As MASRating
            Value = 0.0R
            Return Me
        End Function

        Public Function WithValue(value As Double) As MASRating
            Me.Value = value
            Return Me
        End Function

        Public Function WithMaximum(value As Integer) As MASRating
            Maximum = value
            Return Me
        End Function

        Public Function WithHalfValues(value As Boolean) As MASRating
            AllowHalfValues = value
            Return Me
        End Function

        Public Function WithReadOnly(value As Boolean) As MASRating
            IsReadOnly = value
            Return Me
        End Function

        Public Function WithLabel(value As String) As MASRating
            Label = value
            Return Me
        End Function

        Public Shadows Function WithSize(sizeIntent As MASSize) As MASRating
            MyBase.SetSize(sizeIntent)
            Return Me
        End Function

#End Region

#Region "Layout"

        Friend Function GetPreferredLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetPreferredLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).DesiredSize
        End Function

        Friend Function GetMinLayoutSize(context As MASLayoutMeasureContext) As SKSize Implements IMASLayoutParticipant.GetMinLayoutSize
            Return MeasureIntrinsicSize(CreateSizeContext(context), SizeIntent).MinSize
        End Function

        Private Shared Function CreateSizeContext(context As MASLayoutMeasureContext) As MASSizeContext
            If context Is Nothing Then Return MASIntrinsicControlSizeMetrics.EnsureContext(Nothing)
            Return context.ToSizeContext()
        End Function

        Friend Function MeasureIntrinsicSize(context As MASSizeContext,
                                             intent As MASSize) As MASSizeResult Implements IMASIntrinsicSizeContract.MeasureIntrinsicSize
            Dim safeContext As MASSizeContext = MASIntrinsicControlSizeMetrics.EnsureContext(context)
            Dim dynamicWidth As Single = Math.Max(RatingTokens.PreferredWidthDip, RatingTokens.PaddingDip * 2.0F + CSng(_maximum) * (RatingTokens.StarSizeDip + RatingTokens.StarGapDip) + 78.0F)
            Dim desired As New SKSize(dynamicWidth, RatingTokens.PreferredHeightDip)
            Dim minimum As New SKSize(RatingTokens.ContractMinWidthDip, RatingTokens.ContractMinHeightDip)
            Return MASSizeResolver.FromMeasured(
                desiredSize:=desired,
                minSize:=minimum,
                maxSize:=New SKSize(RatingTokens.MaxWidthDip, Single.PositiveInfinity),
                intent:=intent,
                context:=safeContext,
                canGrowWidth:=True,
                canGrowHeight:=False,
                contentInset:=New MASLayoutInset(RatingTokens.PaddingDip,
                                                 RatingTokens.PaddingDip,
                                                 RatingTokens.PaddingDip,
                                                 RatingTokens.PaddingDip),
                visualOverflowInset:=MASLayoutInset.Empty,
                hitOverflowInset:=MASLayoutInset.Empty,
                isFallback:=False)
        End Function

#End Region

#Region "Rendering"

        Protected Overrides Sub Render(canvas As SKCanvas,
                                       ctx As MASThemeContext,
                                       pixelBounds As SKRect)
            If canvas Is Nothing OrElse ctx Is Nothing Then Return
            If pixelBounds.Width <= 1.0F OrElse pixelBounds.Height <= 1.0F Then Return

            Dim dpi As Single = PixelSnap.SafeDpi(ctx.Dpi)
            _lastDpi = dpi
            Dim bounds As SKRect = PixelSnap.SnapRect(pixelBounds, dpi)
            If bounds.Width <= 1.0F OrElse bounds.Height <= 1.0F Then Return

            Dim responsive As MASResponsiveLayoutProfile = MASResponsiveLayoutResolver.FromTheme(ctx, bounds, SizeIntent, MASRatingLayoutPlan.Thresholds)
            Dim plan As MASRatingLayoutPlan = MASRatingLayoutPlan.Create(responsive, _maximum)

            _primitives.DrawCardSurface(canvas, ctx, bounds, dpi, _contract, MASCardVisualStyle.Secondary)
            _contentRect = PixelSnap.SnapRect(Inset(bounds, plan.PaddingPx), dpi)
            If _contentRect.Width <= 1.0F OrElse _contentRect.Height <= 1.0F Then Return

            DrawHeader(canvas, ctx, _contentRect, plan)
            DrawStars(canvas, ctx, _contentRect, plan, MASRatingPolicy.ResolveRightToLeft())
        End Sub

        Private Sub DrawHeader(canvas As SKCanvas,
                               ctx As MASThemeContext,
                               content As SKRect,
                               plan As MASRatingLayoutPlan)
            If Not plan.ShowHeader OrElse plan.HeaderHeightPx <= 0.0F Then Return
            Dim header As New SKRect(content.Left, content.Top, content.Right, content.Top + plan.HeaderHeightPx)
            Dim primary As SKColor = MASRatingPolicy.ResolvePrimaryTextColor(ctx)
            Dim muted As SKColor = MASRatingPolicy.ResolveMutedTextColor(ctx)
            TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, _label, header, plan.Dpi, MASTypography.MASTextStyle.Small, primary, TypographyTextPrimitives.TextAlign.Left, False)
            If plan.ShowValueText Then TypographyTextPrimitives.DrawSingleLineText(canvas, ctx, ValueText, header, plan.Dpi, MASTypography.MASTextStyle.Micro, muted.WithAlpha(RatingTokens.ValueTextAlpha), TypographyTextPrimitives.TextAlign.Right, False)
        End Sub

        Private Sub DrawStars(canvas As SKCanvas,
                              ctx As MASThemeContext,
                              content As SKRect,
                              plan As MASRatingLayoutPlan,
                              isRtl As Boolean)
            _starRects.Clear()
            Dim max As Integer = MASRatingPolicy.NormalizeMaximum(_maximum)
            Dim starSize As Single = plan.StarSizePx
            Dim gap As Single = plan.StarGapPx
            Dim totalWidth As Single = CSng(max) * starSize + CSng(Math.Max(0, max - 1)) * gap
            Dim startX As Single = If(isRtl, content.Right - totalWidth, content.Left)
            Dim y As Single = content.Top + plan.HeaderHeightPx + Math.Max(3.0F * plan.Dpi, (content.Height - plan.HeaderHeightPx - starSize) * 0.52F)
            _starTrackRect = PixelSnap.SnapRect(New SKRect(startX, y, startX + totalWidth, y + starSize), plan.Dpi)
            Dim activeValue As Double = If(_hasVisualValueOverride, ResolveRenderValue(), If(_hoverStarIndex >= 0 AndAlso Not _isReadOnly, _hoverValue, _value))
            Dim accent As SKColor = MASRatingPolicy.ResolveAccentColor(ctx)
            Dim muted As SKColor = MASRatingPolicy.ResolveMutedTextColor(ctx)
            Dim inactive As SKColor = muted.WithAlpha(If(Enabled, RatingTokens.StarInactiveAlpha, RatingTokens.DisabledTextAlpha))

            For visualIndex As Integer = 0 To max - 1
                Dim x As Single = startX + CSng(visualIndex) * (starSize + gap)
                Dim rect As SKRect = PixelSnap.SnapRect(New SKRect(x, y, x + starSize, y + starSize), plan.Dpi)
                _starRects.Add(rect)
                Dim logicalIndex As Integer = If(isRtl, max - visualIndex - 1, visualIndex)
                Dim fillAmount As Single = CSng(Math.Max(0.0R, Math.Min(1.0R, activeValue - CDbl(logicalIndex))))
                DrawRatingStar(canvas, rect, plan, fillAmount, accent, inactive)
            Next
        End Sub

        Private Sub DrawRatingStar(canvas As SKCanvas,
                                   rect As SKRect,
                                   plan As MASRatingLayoutPlan,
                                   fillAmount As Single,
                                   accent As SKColor,
                                   inactive As SKColor)
            Dim starPath As SKPath = CreateStarPath(rect)

            Using inactivePaint As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = inactive}
                canvas.DrawPath(starPath, inactivePaint)
            End Using

            If fillAmount > 0.0F Then
                canvas.Save()
                Dim clip As SKRect = New SKRect(rect.Left, rect.Top, rect.Left + rect.Width * fillAmount, rect.Bottom)
                canvas.ClipRect(clip)
                Using activePaint As New SKPaint With {.IsAntialias = True, .Style = SKPaintStyle.Fill, .Color = accent.WithAlpha(If(Enabled, CByte(220), CByte(108)))}
                    canvas.DrawPath(starPath, activePaint)
                End Using
                canvas.Restore()
            End If

            Using strokePaint As New SKPaint With {
                .IsAntialias = True,
                .Style = SKPaintStyle.Stroke,
                .StrokeWidth = plan.StarStrokePx,
                .StrokeJoin = SKStrokeJoin.Round,
                .Color = accent.WithAlpha(RatingTokens.StarStrokeAlpha)
            }
                canvas.DrawPath(starPath, strokePaint)
            End Using
            starPath.Dispose()
        End Sub

        Private Shared Function CreateStarPath(rect As SKRect) As SKPath
            Dim path As New SKPath()
            Dim cx As Single = rect.MidX
            Dim cy As Single = rect.MidY
            Dim outerRadius As Single = Math.Min(rect.Width, rect.Height) * 0.48F
            Dim innerRadius As Single = outerRadius * 0.46F
            For i As Integer = 0 To 9
                Dim radius As Single = If(i Mod 2 = 0, outerRadius, innerRadius)
                Dim angle As Double = -Math.PI / 2.0R + CDbl(i) * Math.PI / 5.0R
                Dim x As Single = cx + CSng(Math.Cos(angle) * radius)
                Dim y As Single = cy + CSng(Math.Sin(angle) * radius)
                If i = 0 Then
                    path.MoveTo(x, y)
                Else
                    path.LineTo(x, y)
                End If
            Next
            path.Close()
            Return path
        End Function

#End Region

#Region "Input"

        Protected Overrides Sub OnMouseMove(ctx As MASThemeContext, ptPx As SKPoint)
            If _isReadOnly OrElse Not Enabled Then Return
            Dim oldIndex As Integer = _hoverStarIndex
            Dim oldValue As Double = _hoverValue
            ResolvePointerValue(ptPx, _hoverStarIndex, _hoverValue)
            If oldIndex = _hoverStarIndex AndAlso Math.Abs(oldValue - _hoverValue) < 0.001R Then Return
            InvalidateVisual()
        End Sub

        Protected Overrides Function OnMouseDown(ctx As MASThemeContext,
                                                 ptPx As SKPoint,
                                                 button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then Return False
            If _isReadOnly OrElse Not Enabled Then Return _contentRect.Contains(ptPx.X, ptPx.Y)
            ResolvePointerValue(ptPx, _pressedStarIndex, _hoverValue)
            If _pressedStarIndex >= 0 Then
                _hoverStarIndex = _pressedStarIndex
                InvalidateVisual()
                Return True
            End If
            Return _contentRect.Contains(ptPx.X, ptPx.Y)
        End Function

        Protected Overrides Function OnMouseUp(ctx As MASThemeContext,
                                               ptPx As SKPoint,
                                               button As Integer) As Boolean
            If button <> CInt(MouseButtons.Left) Then Return False
            Dim wasPressed As Integer = _pressedStarIndex
            _pressedStarIndex = -1
            If _isReadOnly OrElse Not Enabled Then
                InvalidateVisual()
                Return wasPressed >= 0
            End If
            Dim upIndex As Integer = -1
            Dim upValue As Double = 0.0R
            ResolvePointerValue(ptPx, upIndex, upValue)
            If wasPressed >= 0 AndAlso wasPressed = upIndex Then
                SetValueInternal(upValue, True, animate:=True)
                InvalidateVisual()
                Return True
            End If
            If wasPressed >= 0 Then
                InvalidateVisual()
                Return True
            End If
            Return False
        End Function

        Protected Overrides Sub OnMouseLeave(ctx As MASThemeContext)
            ResetPointerState()
        End Sub

        Protected Overrides Sub OnMouseCancel(ctx As MASThemeContext)
            ResetPointerState()
        End Sub

        Protected Overrides Sub OnHostLostFocus(ctx As MASThemeContext)
            _pressedStarIndex = -1
            StopValueMotion(syncToSemanticValue:=True)
            InvalidateVisual()
        End Sub

        Protected Overrides Function OnKeyDown(ctx As MASThemeContext,
                                               keyCode As Keys) As Boolean
            If Not Enabled Then Return False
            If _isReadOnly Then Return False
            Dim stepSize As Double = If(_allowHalfValues, 0.5R, 1.0R)
            Dim isRtl As Boolean = MASRatingPolicy.ResolveRightToLeft()
            Select Case keyCode
                Case Keys.Left
                    SetValueInternal(_value + If(isRtl, stepSize, -stepSize), True, animate:=True)
                    Return True
                Case Keys.Right
                    SetValueInternal(_value + If(isRtl, -stepSize, stepSize), True, animate:=True)
                    Return True
                Case Keys.Up
                    SetValueInternal(_value + stepSize, True, animate:=True)
                    Return True
                Case Keys.Down
                    SetValueInternal(_value - stepSize, True, animate:=True)
                    Return True
                Case Keys.Home, Keys.Delete, Keys.Back
                    SetValueInternal(0.0R, True, animate:=True)
                    Return True
                Case Keys.End
                    SetValueInternal(CDbl(_maximum), True, animate:=True)
                    Return True
            End Select
            Return False
        End Function

        Protected Overrides Function WantsKeyboardFocus() As Boolean
            Return MASCompositeContentFocusPolicy.CanReceiveKeyboardFocus(_contract, Visible, Enabled)
        End Function

        Protected Overrides Function WantsPointerFocus() As Boolean
            Return MASCompositeContentFocusPolicy.AllowsPointerFocusForCompositeContent(_contract, Visible, Enabled)
        End Function

#End Region

#Region "Helpers"

        Private Sub ResolvePointerValue(pt As SKPoint,
                                        ByRef starIndex As Integer,
                                        ByRef ratingValue As Double)
            starIndex = -1
            ratingValue = 0.0R
            If _starRects.Count = 0 OrElse _starTrackRect.Width <= 1.0F OrElse _starTrackRect.Height <= 1.0F Then Return
            If Not _starTrackRect.Contains(pt.X, pt.Y) Then Return

            Dim starWidth As Single = _starRects(0).Width
            Dim gapWidth As Single = 0.0F
            If _starRects.Count > 1 Then gapWidth = Math.Max(0.0F, _starRects(1).Left - _starRects(0).Right)
            Dim isRtl As Boolean = MASRatingPolicy.ResolveRightToLeft()
            ratingValue = MASRatingPolicy.ValueFromPointerTrack(pt.X, _starTrackRect.Left, starWidth, gapWidth, _maximum, _allowHalfValues, isRtl, starIndex)
        End Sub

        Private Sub ResetPointerState()
            If _hoverStarIndex < 0 AndAlso _pressedStarIndex < 0 Then Return
            _hoverStarIndex = -1
            _pressedStarIndex = -1
            _hoverValue = 0.0R
            InvalidateVisual()
        End Sub

        Private Sub SetValueInternal(value As Double,
                                     shouldRaiseEvent As Boolean,
                                     Optional animate As Boolean = False)
            Dim normalized As Double = MASRatingPolicy.NormalizeValue(value, _maximum, _allowHalfValues)
            If Math.Abs(_value - normalized) < 0.001R Then Return

            Dim previousVisualValue As Double = ResolveRenderValue()
            _value = normalized

            If animate Then
                StartValueMotion(previousVisualValue, normalized)
            Else
                StopValueMotion(syncToSemanticValue:=True)
            End If

            InvalidateVisual()
            If shouldRaiseEvent Then RaiseValueChangedSafe()
        End Sub

        Private Function ResolveRenderValue() As Double
            If _hasVisualValueOverride Then
                Return MASRatingPolicy.NormalizeValue(_visualValue, _maximum, _allowHalfValues)
            End If

            Return _value
        End Function

        Private Sub StartValueMotion(fromValue As Double, toValue As Double)
            StopValueMotion(syncToSemanticValue:=False)

            fromValue = MASRatingPolicy.NormalizeValue(fromValue, _maximum, _allowHalfValues)
            toValue = MASRatingPolicy.NormalizeValue(toValue, _maximum, _allowHalfValues)
            _visualValue = fromValue
            _hasVisualValueOverride = True

            Dim plan As MASMotionTransitionPlan = MASMotionSystem.CreateDistanceScaledTransitionPlan(
                MASMotionTokenId.ValueChange,
                CSng(fromValue),
                CSng(toValue),
                CSng(Math.Max(1, _maximum)))

            _valueMotionRunner = MASMotionSystem.CreateTimelineRunner(
                owner:=Me,
                consumerName:="MASRating.ValueChange",
                plan:=plan,
                requestFrame:=AddressOf InvalidateVisual,
                applyFrame:=AddressOf ApplyValueMotionFrame,
                completed:=AddressOf CompleteValueMotionFrame)
            _valueMotionRunner.Start()

            InvalidateVisual()
        End Sub

        Private Sub ApplyValueMotionFrame(frame As MASMotionFrame)
            If frame Is Nothing Then Return
            _visualValue = MASRatingPolicy.NormalizeValue(CDbl(frame.Value), _maximum, _allowHalfValues)
            _hasVisualValueOverride = True
            InvalidateVisual()
        End Sub

        Private Sub CompleteValueMotionFrame(frame As MASMotionFrame)
            StopValueMotion(syncToSemanticValue:=True)
            InvalidateVisual()
        End Sub

        Private Sub StopValueMotion(syncToSemanticValue As Boolean)
            If _valueMotionRunner IsNot Nothing Then
                _valueMotionRunner.Dispose()
                _valueMotionRunner = Nothing
            End If

            If syncToSemanticValue Then
                _visualValue = _value
                _hasVisualValueOverride = False
            End If
        End Sub

        Private Shared Function Inset(rect As SKRect,
                                      insetPx As Single) As SKRect
            Return New SKRect(rect.Left + insetPx,
                              rect.Top + insetPx,
                              rect.Right - insetPx,
                              rect.Bottom - insetPx)
        End Function

        Private Sub RaiseValueChangedSafe()
            Try
                RaiseEvent ValueChanged(Me, EventArgs.Empty)
            Catch masCaughtException1 As Exception
                Nexamas.UI.Diagnostics.MASExceptionSilencer.SwallowInputBoundary(masCaughtException1, "MASRating.ValueChanged")
            End Try
        End Sub

#End Region

#Region "Dispose"

        Protected Overrides Sub Dispose(disposing As Boolean)
            If disposing Then
                StopValueMotion(syncToSemanticValue:=True)
                _primitives.Dispose()
            End If

            MyBase.Dispose(disposing)
        End Sub

#End Region

    End Class

End Namespace
