using Microsoft.Extensions.Caching.Memory;
using NetVips;

namespace lilos_stitcher;

public sealed record GenerateRequest(
    string Canvas_Rect,
    double[] Crop_Offset,
    double[] Crop_Size,
    double Output_Scale
);

public readonly record struct PlateCoordinate( int Row, int Col )
{
    public static PlateCoordinate Parse( string token )
    {
        if( string.IsNullOrWhiteSpace( token ) )
            throw new ArgumentException( "Empty coordinate." );

        var rowPart = new string( token.TakeWhile( char.IsLetter ).ToArray() ).ToUpperInvariant();
        var colPart = new string( token.SkipWhile( char.IsLetter ).ToArray() );

        int row = 0;
        foreach( char c in rowPart )
            row = row * 26 + ( c - 'A' + 1 );

        return new PlateCoordinate( row, int.Parse( colPart ) );
    }
}


public class TileCache( IMemoryCache cache )
{
    private const long TileBytes = 720L * 720 * 3;

    public Image? Get( string key ) => cache.TryGetValue( key, out Image? img ) ? img : null;

    public void Set( string key, Image img ) =>
      cache.Set( key, img, new MemoryCacheEntryOptions
      {
          Size = TileBytes,
          SlidingExpiration = TimeSpan.FromMinutes( 20 )
      } );
}

public class TileLoader( TileCache cache, string assetDir )
{
    public async Task<Image> LoadAsync( string name, CancellationToken ct )
    {
        if( cache.Get( name ) is { } hit )
            return hit;

        var image = await Task.Run( () =>
          Image.NewFromFile( Path.Combine( assetDir, $"{name}.png" ), access: Enums.Access.Sequential ), ct );

        cache.Set( name, image );
        return image;
    }
}

public class LiloStitcher( TileLoader loader )
{
    public async Task<string> CreateImageAsync( GenerateRequest req, CancellationToken ct )
    {
        (int rowMin, int colMin, int rows, int cols) = ParseCanvas( req.Canvas_Rect );

        Validate(req.Crop_Offset, nameof(req.Crop_Offset));
        Validate(req.Crop_Size,   nameof(req.Crop_Size));
        double scale = req.Output_Scale;
        if( scale <= 0 || scale > 1 )
            throw new ArgumentOutOfRangeException( nameof( req.Output_Scale ) );

        var tiles = new List<Image>( rows * cols );
        for( int row = 0; row < rows; row++ )
        {
            for( int col = 0; col < cols; col++ )
            {
                string id = $"{RowName( rowMin + row )}{colMin + col}";
                var tile = await loader.LoadAsync( id, ct );
                if( scale < 1 ) tile = tile.Resize( scale );
                tiles.Add( tile );
            }
        }

        var mosaic = Image.Arrayjoin( tiles.ToArray(), across: cols );

        int offsetX = (int)Math.Truncate( req.Crop_Offset[0] * mosaic.Width );
        int offsetY = (int)Math.Truncate( req.Crop_Offset[1] * mosaic.Height );

        int restWidth = mosaic.Width - offsetX;
        int restHeight = mosaic.Height - offsetY;

        int cropWidth = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[0] * restWidth ) );
        int cropHeight = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[1] * restHeight ) );

        int cropX = (int)Math.Truncate( offsetX / 2.0 + ( restWidth - cropWidth ) / 2.0 );
        int cropY = (int)Math.Truncate( offsetY / 2.0 + ( restHeight - cropHeight ) / 2.0 );

        var cropped = mosaic.Crop( cropX, cropY, cropWidth, cropHeight );

        string path = Path.Combine(Path.GetTempPath(), $"mosaic-{Guid.NewGuid():N}.png");
        cropped.WriteToFile(path);
        return path;
    }

    private static (int rowMin, int colMin, int rows, int cols) ParseCanvas( string rect )
    {
        var parts = rect.ToUpperInvariant().Split( ':', StringSplitOptions.RemoveEmptyEntries );
        var part1 = PlateCoordinate.Parse( parts[0] );
        var part2 = PlateCoordinate.Parse( parts[1] );
        int rowMin = Math.Min( part1.Row, part2.Row );
        int rowMax = Math.Max( part1.Row, part2.Row );
        int colMin = Math.Min( part1.Col, part2.Col );
        int colMax = Math.Max( part1.Col, part2.Col );
        return (rowMin, colMin, rowMax - rowMin + 1, colMax - colMin + 1);
    }

    private static void Validate( double[] arr, string name )
    {
        if( arr is null || arr.Length < 2 )
            throw new ArgumentException( $"{name} length" );
        if( arr.Any( x => x < 0 || x > 1 ) )
            throw new ArgumentOutOfRangeException( name );
    }

    static string RowName( int row )
    {
        var stringBuilder = new System.Text.StringBuilder();
        while( row > 0 )
        {
            row--;
            stringBuilder.Insert( 0, (char)( 'A' + row % 26 ) );
            row /= 26;
        }
        return stringBuilder.ToString();
    }
}
