using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Runtime.CompilerServices;
using Color = SixLabors.ImageSharp.Color;

namespace EllieBot.Modules.Searches;

public sealed class ImagesharpStockChartDrawingService : IStockChartDrawingService, IEService
{
    private const int WIDTH = 300;
    private const int HEIGHT = 100;
    private const decimal MAX_HEIGHT = HEIGHT * 0.8m;

    private static readonly Rgba32 _backgroundColor = Rgba32.ParseHex("17181E");
    private static readonly Rgba32 _lineGuideColor = Rgba32.ParseHex("212125");
    private static readonly Rgba32 _sparklineColor = Rgba32.ParseHex("2961FC");
    private static readonly Rgba32 _greenBrush = Rgba32.ParseHex("26A69A");
    private static readonly Rgba32 _redBrush = Rgba32.ParseHex("EF5350");

    private static float GetNormalizedPoint(decimal max, decimal point, decimal range)
        => (float)((MAX_HEIGHT * ((max - point) / range)) + HeightOffset());
        
    private PointF[] GetSparklinePointsInternal(IReadOnlyCollection<CandleData> series)
    {
        var candleStep = WIDTH / (series.Count + 1);
        var max = series.Max(static x => x.High);
        var min = series.Min(static x => x.Low);
    
        var range = max - min;
    
        var points = new PointF[series.Count];
    
        var i = 0;
        foreach (var candle in series)
        {
            var x = candleStep * (i + 1);

            var y = GetNormalizedPoint(max, candle.Close, range);
            points[i++] = new(x, y);
        }

        return points;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static decimal HeightOffset()
        => (HEIGHT - MAX_HEIGHT) / 2m;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static Image<Rgba32> CreateCanvasInternal()
        => new Image<Rgba32>(WIDTH, HEIGHT, _backgroundColor);

    private CandleDrawingData[] GetChartDrawingDataInternal(IReadOnlyCollection<CandleData> series)
    {
        var candleMargin = 2;
        var candleStep = (WIDTH - (candleMargin * series.Count)) / (series.Count + 1);
        var max = series.Max(static x => x.High);
        var min = series.Min(static x => x.Low);

        var range = max - min;

        var drawData = new CandleDrawingData[series.Count];

        var candleWidth = candleStep;
        
        var i = 0;
        foreach (var candle in series)
        {
            var offsetX = (i - 1) * candleMargin; 
            var x = (candleStep * (i + 1)) + offsetX;
            var yOpen = GetNormalizedPoint(max, candle.Open, range);
            var yClose = GetNormalizedPoint(max, candle.Close, range);
            var y = candle.Open > candle.Close
                ? yOpen
                : yClose;

            var sizeH = Math.Abs(yOpen - yClose);

            var high = GetNormalizedPoint(max, candle.High, range);
            var low = GetNormalizedPoint(max, candle.Low, range);
            drawData[i] = new(candle.Open < candle.Close,
                new(x, y, candleWidth, sizeH),
                new(x + (candleStep / 2), high),
                new(x + (candleStep / 2), low));
            ++i;
        }

        return drawData;
    }

    private void DrawChartData(Image<Rgba32> image, CandleDrawingData[] drawData)
        => image.Mutate(ctx =>
        {
            foreach (var data in drawData)
                ctx.DrawLine(data.IsGreen
                        ? _greenBrush
                        : _redBrush,
                    1,
                    data.High,
                    data.Low);


            foreach (var data in drawData)
                ctx.Fill(data.IsGreen
                        ? _greenBrush
                        : _redBrush,
                    data.BodyRect);
        });

    private void DrawLineGuides(Image<Rgba32> image, IReadOnlyCollection<CandleData> series)
    {
        var max = series.Max(x => x.High);
        var min = series.Min(x => x.Low);

        var step = (max - min) / 5;

        var lines = new float[6];
        
        for (var i = 0; i < 6; i++)
        {
            var y = GetNormalizedPoint(max, min + (step * i), max - min);
            lines[i] = y;
        }

        image.Mutate(ctx =>
        {
            // draw guides
            foreach (var y in lines)
                ctx.DrawLine(_lineGuideColor, 1, new PointF(0, y), new PointF(WIDTH, y));
            
            // // draw min and max price on the chart
            // ctx.DrawText(min.ToString(CultureInfo.InvariantCulture),
            //     SystemFonts.CreateFont("Arial", 5),
            //     Color.White,
            //     new PointF(0, (float)HeightOffset() - 5)
            // );
            //
            // ctx.DrawText(max.ToString("N1", CultureInfo.InvariantCulture),
            //     SystemFonts.CreateFont("Arial", 5),
            //     Color.White,
            //     new PointF(0,  HEIGHT - (float)HeightOffset())
            // );
        });
    }
    
    public Task<ImageData?> GenerateSparklineAsync(IReadOnlyCollection<CandleData> series)
    {
        if (series.Count == 0)
            return Task.FromResult<ImageData?>(default);

        using var image = CreateCanvasInternal();

        var points = GetSparklinePointsInternal(series);
        
        image.Mutate(ctx =>
        {
            ctx.DrawLine(_sparklineColor, 2, points);
        });
    
        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
    }

    public Task<ImageData?> GenerateCombinedChartAsync(IReadOnlyCollection<CandleData> series)
    {
        if (series.Count == 0)
            return Task.FromResult<ImageData?>(default);

        using var image = CreateCanvasInternal();
        
        DrawLineGuides(image, series);
        
        var chartData = GetChartDrawingDataInternal(series);
        DrawChartData(image, chartData);

        var points = GetSparklinePointsInternal(series);
        image.Mutate(ctx =>
        {
            ctx.DrawLine(Color.ParseHex("00FFFFAA"), 1, points);
        });

        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
    }
    
    public Task<ImageData?> GenerateCandleChartAsync(IReadOnlyCollection<CandleData> series)
    {
        if (series.Count == 0)
            return Task.FromResult<ImageData?>(default);

        using var image = CreateCanvasInternal();

        DrawLineGuides(image, series);
        
        var drawData = GetChartDrawingDataInternal(series);
        DrawChartData(image, drawData);

        return Task.FromResult<ImageData?>(new("png", image.ToStream()));
    }
}