// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Numerics;
using ManagedCommon;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Windows.UI;

namespace Microsoft.CmdPal.UI.Controls;

internal sealed partial class BlurImageControl : Control
{
    private const string ImageSourceParameterName = "ImageSource";

    private const string BrightnessEffectName = "Brightness";
    private const string BrightnessOverlayEffectName = "BrightnessOverlay";
    private const string BlurEffectName = "Blur";
    private const string TintBlendEffectName = "TintBlend";
    private const string TintEffectName = "Tint";

#pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties
    private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount");
    private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount");
    private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color");
    private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount");
    private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color");
#pragma warning restore CA1507

    private static readonly string[] AnimatableProperties = [
        BrightnessSource1AmountEffectProperty,
        BrightnessSource2AmountEffectProperty,
        BrightnessOverlayColorEffectProperty,
        BlurBlurAmountEffectProperty,
        TintColorEffectProperty
    ];

    public static readonly DependencyProperty ImageSourceProperty =
        DependencyProperty.Register(
            nameof(ImageSource),
            typeof(ImageSource),
            typeof(BlurImageControl),
            new PropertyMetadata(null, OnImageChanged));

    public static readonly DependencyProperty ImageStretchProperty =
        DependencyProperty.Register(
            nameof(ImageStretch),
            typeof(Stretch),
            typeof(BlurImageControl),
            new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged));

    public static readonly DependencyProperty ImageOpacityProperty =
        DependencyProperty.Register(
            nameof(ImageOpacity),
            typeof(double),
            typeof(BlurImageControl),
            new PropertyMetadata(1.0, OnOpacityChanged));

    public static readonly DependencyProperty ImageBrightnessProperty =
        DependencyProperty.Register(
            nameof(ImageBrightness),
            typeof(double),
            typeof(BlurImageControl),
            new PropertyMetadata(1.0, OnBrightnessChanged));

    public static readonly DependencyProperty BlurAmountProperty =
        DependencyProperty.Register(
            nameof(BlurAmount),
            typeof(double),
            typeof(BlurImageControl),
            new PropertyMetadata(0.0, OnBlurAmountChanged));

    public static readonly DependencyProperty TintColorProperty =
        DependencyProperty.Register(
            nameof(TintColor),
            typeof(Color),
            typeof(BlurImageControl),
            new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged));

    public static readonly DependencyProperty TintIntensityProperty =
        DependencyProperty.Register(
            nameof(TintIntensity),
            typeof(double),
            typeof(BlurImageControl),
            new PropertyMetadata(0.0, OnVisualPropertyChanged));

    private Compositor? _compositor;
    private SpriteVisual? _effectVisual;
    private CompositionEffectBrush? _effectBrush;
    private CompositionSurfaceBrush? _imageBrush;

    public BlurImageControl()
    {
        this.DefaultStyleKey = typeof(BlurImageControl);
        this.Loaded += OnLoaded;
        this.SizeChanged += OnSizeChanged;
    }

    public ImageSource ImageSource
    {
        get => (ImageSource)GetValue(ImageSourceProperty);
        set => SetValue(ImageSourceProperty, value);
    }

    public Stretch ImageStretch
    {
        get => (Stretch)GetValue(ImageStretchProperty);
        set => SetValue(ImageStretchProperty, value);
    }

    public double ImageOpacity
    {
        get => (double)GetValue(ImageOpacityProperty);
        set => SetValue(ImageOpacityProperty, value);
    }

    public double ImageBrightness
    {
        get => (double)GetValue(ImageBrightnessProperty);
        set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1));
    }

    public double BlurAmount
    {
        get => (double)GetValue(BlurAmountProperty);
        set => SetValue(BlurAmountProperty, value);
    }

    public Color TintColor
    {
        get => (Color)GetValue(TintColorProperty);
        set => SetValue(TintColorProperty, value);
    }

    public double TintIntensity
    {
        get => (double)GetValue(TintIntensityProperty);
        set => SetValue(TintIntensityProperty, value);
    }

    private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is BlurImageControl control && control._imageBrush != null)
        {
            control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue);
        }
    }

    private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is BlurImageControl control && control._compositor != null)
        {
            control.UpdateEffect();
        }
    }

    private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is BlurImageControl control && control._effectVisual != null)
        {
            control._effectVisual.Opacity = (float)(double)e.NewValue;
        }
    }

    private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is BlurImageControl control && control._effectBrush != null)
        {
            control.UpdateEffect();
        }
    }

    private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is BlurImageControl control && control._effectBrush != null)
        {
            control.UpdateEffect();
        }
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        InitializeComposition();
    }

    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (_effectVisual != null)
        {
            _effectVisual.Size = new Vector2(
                (float)Math.Max(1, e.NewSize.Width),
                (float)Math.Max(1, e.NewSize.Height));
        }
    }

    private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not BlurImageControl control)
        {
            return;
        }

        control.EnsureEffect(force: true);
        control.UpdateEffect();
    }

    private void InitializeComposition()
    {
        var visual = ElementCompositionPreview.GetElementVisual(this);
        _compositor = visual.Compositor;

        _effectVisual = _compositor.CreateSpriteVisual();
        _effectVisual.Size = new Vector2(
            (float)Math.Max(1, ActualWidth),
            (float)Math.Max(1, ActualHeight));
        _effectVisual.Opacity = (float)ImageOpacity;

        ElementCompositionPreview.SetElementChildVisual(this, _effectVisual);

        UpdateEffect();
    }

    private void EnsureEffect(bool force = false)
    {
        if (_compositor is null)
        {
            return;
        }

        if (_effectBrush is not null && !force)
        {
            return;
        }

        var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName);

        // 1) Brightness via ArithmeticCompositeEffect
        // We blend between the original image and either black or white,
        // depending on whether we want to darken or brighten. BrightnessEffect isn't supported
        // in the composition graph.
        var brightnessEffect = new ArithmeticCompositeEffect
        {
            Name = BrightnessEffectName,
            Source1 = imageSource, // original image
            Source2 = new ColorSourceEffect
            {
                Name = BrightnessOverlayEffectName,
                Color = Colors.Black, // we'll swap black/white via properties
            },

            MultiplyAmount = 0.0f,
            Source1Amount = 1.0f, // original
            Source2Amount = 0.0f, // overlay
            Offset = 0.0f,
        };

        // 2) Blur
        var blurEffect = new GaussianBlurEffect
        {
            Name = BlurEffectName,
            BlurAmount = 0.0f,
            BorderMode = EffectBorderMode.Hard,
            Optimization = EffectOptimization.Balanced,
            Source = brightnessEffect,
        };

        // 3) Tint (always in the chain; intensity via alpha)
        var tintEffect = new BlendEffect
        {
            Name = TintBlendEffectName,
            Background = blurEffect,
            Foreground = new ColorSourceEffect
            {
                Name = TintEffectName,
                Color = Colors.Transparent,
            },
            Mode = BlendEffectMode.Multiply,
        };

        var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties);

        _effectBrush?.Dispose();
        _effectBrush = effectFactory.CreateBrush();

        if (ImageSource is not null)
        {
            _imageBrush ??= _compositor.CreateSurfaceBrush();
            LoadImageAsync(ImageSource);
            _effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush);
        }
        else
        {
            _effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush());
        }

        if (_effectVisual is not null)
        {
            _effectVisual.Brush = _effectBrush;
        }
    }

    private void UpdateEffect()
    {
        if (_compositor is null)
        {
            return;
        }

        EnsureEffect();
        if (_effectBrush is null)
        {
            return;
        }

        var props = _effectBrush.Properties;

        // Brightness
        var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0);

        float source1Amount;
        float source2Amount;
        Color overlayColor;

        if (b >= 0)
        {
            // Brighten: blend towards white
            overlayColor = Colors.White;
            source1Amount = 1.0f - b; // original image contribution
            source2Amount = b;        // white overlay contribution
        }
        else
        {
            // Darken: blend towards black
            overlayColor = Colors.Black;
            var t = -b;               // 0..1
            source1Amount = 1.0f - t; // original image
            source2Amount = t;        // black overlay
        }

        props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount);
        props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount);
        props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor);

        // Blur
        props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount);

        // Tint
        var tintColor = TintColor;
        var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0);

        var adjustedColor = Color.FromArgb(
            (byte)(clampedIntensity * 255),
            tintColor.R,
            tintColor.G,
            tintColor.B);

        props.InsertColor(TintColorEffectProperty, adjustedColor);
    }

    private void LoadImageAsync(ImageSource imageSource)
    {
        try
        {
            if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage)
            {
                return;
            }

            _imageBrush ??= _compositor?.CreateSurfaceBrush();
            if (_imageBrush is null)
            {
                return;
            }

            Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
            var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
            loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
            SetLoadedSurfaceToBrush(loadedSurface);
            _effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
        }
        catch (Exception ex)
        {
            Logger.LogError("Failed to load image for BlurImageControl: {0}", ex);
        }

        return;

        void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e)
        {
            switch (e.Status)
            {
                case LoadedImageSourceLoadStatus.Success:
                    Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}");

                    try
                    {
                        SetLoadedSurfaceToBrush(loadedSurface);
                    }
                    catch (Exception ex)
                    {
                        Logger.LogError("Failed to set surface in BlurImageControl", ex);
                        throw;
                    }

                    break;
                case LoadedImageSourceLoadStatus.NetworkError:
                case LoadedImageSourceLoadStatus.InvalidFormat:
                case LoadedImageSourceLoadStatus.Other:
                default:
                    Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}");
                    break;
            }
        }
    }

    private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface)
    {
        var surfaceBrush = _imageBrush;
        if (surfaceBrush is null)
        {
            return;
        }

        surfaceBrush.Surface = loadedSurface;
        surfaceBrush.Stretch = ConvertStretch(ImageStretch);
        surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear;
    }

    private static CompositionStretch ConvertStretch(Stretch stretch)
    {
        return stretch switch
        {
            Stretch.None => CompositionStretch.None,
            Stretch.Fill => CompositionStretch.Fill,
            Stretch.Uniform => CompositionStretch.Uniform,
            Stretch.UniformToFill => CompositionStretch.UniformToFill,
            _ => CompositionStretch.UniformToFill,
        };
    }

    private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}";
}
