using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Skia.Helpers;
using CommunityToolkit.Mvvm.Input;
using FluentResults;
using InABox.Avalonia.Components.ImageEditing;
using InABox.Avalonia.Converters;
using InABox.Core;
using SkiaSharp;
using System.Collections.ObjectModel;

namespace InABox.Avalonia.Components;

public enum ImageEditingMode
{
    Polyline,
    Rectangle,
    Ellipse
}

public class ImageEditorModeButton(ImageEditingMode mode, Canvas? canvas)
{
    public ImageEditingMode Mode { get; set; } = mode;

    public Canvas? Canvas { get; set; } = canvas;
}

public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
{
    public static readonly ImageEditorTransparentImageBrushConverter Instance = new ImageEditorTransparentImageBrushConverter();

    protected override IBrush? Convert(IBrush? value, object? parameter = null)
    {
        if (value is SolidColorBrush solid && solid.Color.A == 255) return solid;

        var brush = new VisualBrush
        {
            TileMode = TileMode.Tile,
            DestinationRect = new(0, 0, 10, 10, RelativeUnit.Absolute)
        };
        var canvas = new Canvas
        {
            Width = 10,
            Height = 10
        };
        var rect1 = new Rectangle { Width = 5, Height = 5 };
        var rect2 = new Rectangle { Width = 5, Height = 5 };

        Canvas.SetLeft(rect2, 5);
        Canvas.SetTop(rect2, 5);

        rect1.Fill = new SolidColorBrush(Colors.LightGray);
        rect2.Fill = new SolidColorBrush(Colors.LightGray);

        var rect3 = new Rectangle { Width = 10, Height = 10 };
        rect3.Fill = value;

        canvas.Children.Add(rect1);
        canvas.Children.Add(rect2);
        canvas.Children.Add(rect3);

        brush.Visual = canvas;
        return brush;
    }
}

public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBrush?>
{
    public static readonly ImageEditorRemoveOpacityConverter Instance = new();
    
    protected override IBrush? Convert(IBrush? value, object? parameter = null)
    {
        if (value is SolidColorBrush solid)
        {
            return new SolidColorBrush(new Color(255, solid.Color.R, solid.Color.G, solid.Color.B));
        }
        return value;
    }
}

// TODO: Make it so we don't re-render everything everytime 'Objects' changes.

public partial class ImageEditor : UserControl
{
    public static readonly StyledProperty<IImage?> SourceProperty =
        AvaloniaProperty.Register<ImageEditor, IImage?>(nameof(Source));

    public static readonly StyledProperty<IBrush?> PrimaryBrushProperty =
        AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(PrimaryBrush), new SolidColorBrush(Colors.Black));

    public static readonly StyledProperty<IBrush?> SecondaryBrushProperty =
        AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(SecondaryBrush), new SolidColorBrush(Colors.White));

    public static readonly StyledProperty<double> LineThicknessProperty =
        AvaloniaProperty.Register<ImageEditor, double>(nameof(LineThickness), 3.0);

    public static readonly StyledProperty<int> ImageWidthProperty =
        AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageWidth), 100);

    public static readonly StyledProperty<int> ImageHeightProperty =
        AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);

    public static readonly StyledProperty<ImageEditingMode> ModeProperty =
        AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);

    public static readonly StyledProperty<bool> ShowButtonsProperty =
        AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);

    public IImage? Source
    {
        get => GetValue(SourceProperty);
        set => SetValue(SourceProperty, value);
    }

    public int ImageWidth
    {
        get => GetValue(ImageWidthProperty);
        set => SetValue(ImageWidthProperty, value);
    }

    public int ImageHeight
    {
        get => GetValue(ImageHeightProperty);
        set => SetValue(ImageHeightProperty, value);
    }

    public bool ShowButtons
    {
        get => GetValue(ShowButtonsProperty);
        set => SetValue(ShowButtonsProperty, value);
    }

    #region Editing Properties

    public ImageEditingMode Mode
    {
        get => GetValue(ModeProperty);
        set => SetValue(ModeProperty, value);
    }

    public IBrush? PrimaryBrush
    {
        get => GetValue(PrimaryBrushProperty);
        set => SetValue(PrimaryBrushProperty, value);
    }

    public IBrush? SecondaryBrush
    {
        get => GetValue(SecondaryBrushProperty);
        set => SetValue(SecondaryBrushProperty, value);
    }

    public double LineThickness
    {
        get => GetValue(LineThicknessProperty);
        set => SetValue(LineThicknessProperty, value);
    }

    #endregion

    #region Events

    public event EventHandler? Changed;

    #endregion

    #region Private Properties

    public ObservableCollection<ImageEditorModeButton> ModeButtons { get; set; } = new();

    private ObservableCollection<IImageEditorObject> Objects = new();

    private IImageEditorObject? CurrentObject;

    private Stack<IImageEditorObject> RedoStack = new();

    private double ScaleFactor = 1.0;

    #endregion

    static ImageEditor()
    {
        SourceProperty.Changed.AddClassHandler<ImageEditor>(Source_Changed);
    }

    private static void Source_Changed(ImageEditor editor, AvaloniaPropertyChangedEventArgs args)
    {
        if(editor.Source is not null)
        {
            editor.ImageWidth = (int)Math.Floor(editor.Source.Size.Width);
            editor.ImageHeight = (int)Math.Floor(editor.Source.Size.Height);
            editor.PositionImage();
        }
    }

    public ImageEditor()
    {
        InitializeComponent();

        Objects.CollectionChanged += Objects_CollectionChanged;

        AddModeButtons();

        SetMode(Mode);

        OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
    }

    #region Layout

    private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
    {
        PositionImage();
    }

    protected override void OnLoaded(RoutedEventArgs e)
    {
        base.OnLoaded(e);
        PositionImage();
    }

    private void PositionImage()
    {
        var canvasWidth = OuterCanvas.Bounds.Width;
        var canvasHeight = OuterCanvas.Bounds.Height;

        var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
        ScaleFactor = scaleFactor;

        var imageWidth = ImageWidth * scaleFactor;
        var imageHeight = ImageHeight * scaleFactor;

        ImageBorder.Width = imageWidth;
        ImageBorder.Height = imageHeight;

        Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
        Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);

        Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
        Canvas.Width = ImageWidth;
        Canvas.Height = ImageHeight;
    }

    #endregion

    #region Editing Commands

    private void UpdateUndoRedoButtons()
    {
        UndoButton.IsEnabled = Objects.Count > 0;
        RedoButton.IsEnabled = RedoStack.Count > 0;
    }

    [RelayCommand]
    private void Undo()
    {
        if (Objects.Count == 0) return;

        RedoStack.Push(Objects[^1]);
        Objects.RemoveAt(Objects.Count - 1);
        UpdateUndoRedoButtons();
        Changed?.Invoke(this, new EventArgs());
    }

    [RelayCommand]
    private void Redo()
    {
        if (!RedoStack.TryPop(out var top)) return;

        Objects.Add(top);
        UpdateUndoRedoButtons();
        Changed?.Invoke(this, new EventArgs());
    }

    private void AddObject(IImageEditorObject obj)
    {
        Objects.Add(obj);
        RedoStack.Clear();
        UpdateUndoRedoButtons();
        Changed?.Invoke(this, new EventArgs());
    }

    [RelayCommand]
    private void SetMode(ImageEditingMode mode)
    {
        Mode = mode;
        ShapeButton.Content = CreateModeButtonContent(mode);
        SecondaryColour.IsVisible = HasSecondaryColour();
    }

    private bool HasSecondaryColour()
    {
        return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
    }

    #endregion

    #region Mode Buttons

    private void AddModeButtons()
    {
        AddModeButton(ImageEditingMode.Polyline);
        AddModeButton(ImageEditingMode.Rectangle);
        AddModeButton(ImageEditingMode.Ellipse);
    }

    private void AddModeButton(ImageEditingMode mode)
    {
        ModeButtons.Add(new(mode, CreateModeButtonContent(mode)));
    }

    private Canvas? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
    {
        switch (mode)
        {
            case ImageEditingMode.Polyline:
                var canvas = new Canvas();
                var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
                var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
                var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
                line1.StrokeThickness = 4;
                line1.StrokeLineCap = PenLineCap.Round;
                line1.StrokeJoin = PenLineJoin.Round;
                line1.Stroke = new SolidColorBrush(Colors.Black);
                canvas.Children.Add(line1);

                if (bindColour)
                {
                    line1.StrokeThickness = 5;
                    line2.StrokeThickness = 4;
                    line2.StrokeLineCap = PenLineCap.Round;
                    line2.StrokeJoin = PenLineJoin.Round;
                    line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
                    {
                        Source = this,
                        Converter = ImageEditorRemoveOpacityConverter.Instance
                    });
                    canvas.Children.Add(line2);
                }
                return canvas;
            case ImageEditingMode.Rectangle:
                canvas = new Canvas();
                canvas.Width = 25;
                canvas.Height = 25;

                var rectangle = new Rectangle();
                if (bindColour)
                {
                    rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
                    {
                        Source = this,
                        Converter = ImageEditorRemoveOpacityConverter.Instance
                    });
                    rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
                    {
                        Source = this,
                        Converter = ImageEditorTransparentImageBrushConverter.Instance
                    });
                }
                else
                {
                    rectangle.Stroke = new SolidColorBrush(Colors.Black);
                    rectangle.Fill = new SolidColorBrush(Colors.White);
                }
                rectangle.StrokeThickness = 1.0;
                rectangle.Width = 25;
                rectangle.Height = 25;
                canvas.Children.Add(rectangle);

                return canvas;
            case ImageEditingMode.Ellipse:
                canvas = new Canvas();
                canvas.Width = 25;
                canvas.Height = 25;

                var ellipse = new Ellipse();
                if (bindColour)
                {
                    ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
                    {
                        Source = this,
                        Converter = ImageEditorRemoveOpacityConverter.Instance
                    });
                    ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
                    {
                        Source = this,
                        Converter = ImageEditorTransparentImageBrushConverter.Instance
                    });
                }
                else
                {
                    ellipse.Stroke = new SolidColorBrush(Colors.Black);
                    ellipse.Fill = new SolidColorBrush(Colors.White);
                }
                ellipse.StrokeThickness = 1.0;
                ellipse.Width = 25;
                ellipse.Height = 25;
                canvas.Children.Add(ellipse);

                return canvas;
            default:
                return null;
        }
    }

    #endregion

    #region Public Interface

    public void Reset()
    {
        Objects.Clear();
        RedoStack.Clear();
        UpdateUndoRedoButtons();
        Changed?.Invoke(this, new EventArgs());
    }

    public Bitmap GetImage()
    {
        var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
        renderBitmap.Render(Image);
        using var context = renderBitmap.CreateDrawingContext();
        if(Source is not null)
        {
            context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
        }

        foreach (var obj in Objects)
        {
            var control = obj.GetControl();
            var left = Canvas.GetLeft(control);
            var top = Canvas.GetTop(control);
            if (double.IsNaN(left)) left = 0;
            if (double.IsNaN(top)) top = 0;
            using (context.PushTransform(Matrix.CreateTranslation(new(left, top))))
            {
                control.Render(context);
            }
        }
        return renderBitmap;
    }

    public byte[] SaveImage()
    {
        var bitmap = GetImage();

        var stream = new MemoryStream();
        bitmap.Save(stream);
        return stream.ToArray();
    }

    #endregion

    #region Editing

    private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        Canvas.Children.Clear();
        foreach(var item in Objects)
        {
            item.Update();
            Canvas.Children.Add(item.GetControl());
        }
    }

    Point ConvertToImageCoordinates(Point canvasCoordinates)
    {
        return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
    }

    private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
    {
        var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
        switch (Mode)
        {
            case ImageEditingMode.Polyline:
                CurrentObject = new PolylineObject
                {
                    Points = [position],
                    PrimaryBrush = PrimaryBrush,
                    Thickness = LineThickness
                };
                AddObject(CurrentObject);
                break;
            case ImageEditingMode.Rectangle:
                CurrentObject = new RectangleObject
                {
                    Point1 = position,
                    Point2 = position,
                    PrimaryBrush = PrimaryBrush,
                    SecondaryBrush = SecondaryBrush,
                    Thickness = LineThickness
                };
                AddObject(CurrentObject);
                break;
            case ImageEditingMode.Ellipse:
                CurrentObject = new EllipseObject
                {
                    Point1 = position,
                    Point2 = position,
                    PrimaryBrush = PrimaryBrush,
                    SecondaryBrush = SecondaryBrush,
                    Thickness = LineThickness
                };
                AddObject(CurrentObject);
                break;
        }
    }

    private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
    {
        var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
        switch (CurrentObject)
        {
            case PolylineObject polyline:
                polyline.Points.Add(position);
                polyline.Update();
                Changed?.Invoke(this, new EventArgs());
                break;
            case RectangleObject rectangle:
                rectangle.Point2 = position;
                rectangle.Update();
                Changed?.Invoke(this, new EventArgs());
                break;
            case EllipseObject ellipse:
                ellipse.Point2 = position;
                ellipse.Update();
                Changed?.Invoke(this, new EventArgs());
                break;
        }
    }

    private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
    {
        var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
        switch (CurrentObject)
        {
            case PolylineObject polyline:
                polyline.Points.Add(position);
                polyline.Update();
                CurrentObject = null;
                Changed?.Invoke(this, new EventArgs());
                break;
            case RectangleObject rectangle:
                rectangle.Point2 = position;
                rectangle.Update();
                CurrentObject = null;
                Changed?.Invoke(this, new EventArgs());
                break;
            case EllipseObject ellipse:
                ellipse.Point2 = position;
                ellipse.Update();
                CurrentObject = null;
                Changed?.Invoke(this, new EventArgs());
                break;
        }

    }

    #endregion
}