using System;
using Avalonia;

namespace Minint.Controls;

/// <summary>
/// Manages zoom level and pan offset for the pixel canvas.
/// Provides screen↔pixel coordinate transforms.
/// </summary>
public sealed class Viewport
{
    public double Zoom { get; set; } = 1.0;
    public double OffsetX { get; set; }
    public double OffsetY { get; set; }

    public const double MinZoom = 0.25;
    public const double MaxZoom = 128.0;

    /// <summary>
    /// Zoom base per 1.0 unit of wheel delta. Actual factor = Pow(base, |delta|).
    /// Touchpad nudge (delta ~0.1) → ~1.01×, mouse tick (delta 1.0) → 1.10×, fast (3.0) → 1.33×.
    /// </summary>
    private const double ZoomBase = 1.10;

    public (int X, int Y) ScreenToPixel(double screenX, double screenY) =>
        ((int)Math.Floor((screenX - OffsetX) / Zoom),
         (int)Math.Floor((screenY - OffsetY) / Zoom));

    public (double X, double Y) PixelToScreen(int pixelX, int pixelY) =>
        (pixelX * Zoom + OffsetX,
         pixelY * Zoom + OffsetY);

    public Rect ImageScreenRect(int imageWidth, int imageHeight) =>
        new(OffsetX, OffsetY, imageWidth * Zoom, imageHeight * Zoom);

    /// <summary>
    /// Zooms keeping the point under cursor fixed.
    /// Uses the actual magnitude of <paramref name="delta"/> for proportional zoom.
    /// </summary>
    public void ZoomAtPoint(double screenX, double screenY, double delta,
                            int imageWidth, int imageHeight, double controlWidth, double controlHeight)
    {
        double absDelta = Math.Abs(delta);
        double factor = delta > 0 ? Math.Pow(ZoomBase, absDelta) : 1.0 / Math.Pow(ZoomBase, absDelta);
        double newZoom = Math.Clamp(Zoom * factor, MinZoom, MaxZoom);
        if (Math.Abs(newZoom - Zoom) < 1e-12) return;

        double pixelX = (screenX - OffsetX) / Zoom;
        double pixelY = (screenY - OffsetY) / Zoom;
        Zoom = newZoom;
        OffsetX = screenX - pixelX * Zoom;
        OffsetY = screenY - pixelY * Zoom;

        ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
    }

    /// <summary>
    /// Pans by screen-space delta, then clamps so the image can't be scrolled out of view.
    /// </summary>
    public void Pan(double deltaX, double deltaY,
                    int imageWidth, int imageHeight, double controlWidth, double controlHeight)
    {
        OffsetX += deltaX;
        OffsetY += deltaY;
        ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
    }

    /// <summary>
    /// Sets offset directly (e.g. from middle-mouse drag), then clamps.
    /// </summary>
    public void SetOffset(double offsetX, double offsetY,
                          int imageWidth, int imageHeight, double controlWidth, double controlHeight)
    {
        OffsetX = offsetX;
        OffsetY = offsetY;
        ClampOffset(imageWidth, imageHeight, controlWidth, controlHeight);
    }

    /// <summary>
    /// Ensures at least <c>minVisible</c> pixels of the image remain on screen on each edge.
    /// </summary>
    public void ClampOffset(int imageWidth, int imageHeight, double controlWidth, double controlHeight)
    {
        double extentW = imageWidth * Zoom;
        double extentH = imageHeight * Zoom;

        double minVisH = Math.Max(32, Math.Min(controlWidth, extentW) * 0.10);
        double minVisV = Math.Max(32, Math.Min(controlHeight, extentH) * 0.10);

        // Image right edge must be >= minVisH from left of control
        // Image left edge must be <= controlWidth - minVisH from left
        OffsetX = Math.Clamp(OffsetX, minVisH - extentW, controlWidth - minVisH);
        OffsetY = Math.Clamp(OffsetY, minVisV - extentH, controlHeight - minVisV);
    }

    public void FitToView(int imageWidth, int imageHeight, double controlWidth, double controlHeight)
    {
        if (imageWidth <= 0 || imageHeight <= 0 || controlWidth <= 0 || controlHeight <= 0)
            return;

        double scaleX = controlWidth / imageWidth;
        double scaleY = controlHeight / imageHeight;
        Zoom = Math.Max(1.0, Math.Floor(Math.Min(scaleX, scaleY)));

        OffsetX = (controlWidth - imageWidth * Zoom) / 2.0;
        OffsetY = (controlHeight - imageHeight * Zoom) / 2.0;
    }

    public (double Min, double Max, double Value, double ViewportSize)
        GetScrollInfo(int imageSize, double controlSize, double offset)
    {
        double extent = imageSize * Zoom;
        double minVis = Math.Max(32, Math.Min(controlSize, extent) * 0.10);
        double min = minVis - extent;
        double max = controlSize - minVis;
        double viewportSize = Math.Min(controlSize, extent);
        return (min, max, offset, viewportSize);
    }
}
