
using System;
using Godot;
using System.Collections.Generic;
using System.Text;

namespace Rokojori
{  
  public class RDTextureFormats
  {
    public enum PixelChannelEncoding
    {
      Unknown,

      uInt8,
      sInt8,
      
      uInt16,
      sInt16,
      sFloat16,
      uNorm16,
      sNorm16,
      
      uInt32,
      sInt32,
      sFloat32
    }

    public static readonly List<PixelChannelEncoding> bits8 = new List<PixelChannelEncoding>() 
    { 
      PixelChannelEncoding.uInt8,
      PixelChannelEncoding.sInt8
    };

    public static readonly List<PixelChannelEncoding> bits32 = new List<PixelChannelEncoding>() 
    { 
      PixelChannelEncoding.uInt32,
      PixelChannelEncoding.sInt32,
      PixelChannelEncoding.sFloat32
    };

    public static int NumBytesFor( PixelChannelEncoding encoding )
    {
      if ( PixelChannelEncoding.Unknown == encoding )
      {
        return -1;
      }

      if ( bits8.Contains( encoding ) )
      {
        return 1;
      }

      if ( bits32.Contains( encoding ) )
      {
        return 4;
      }

      return 2;
    }

    public static string UsageInfo( RenderingDevice.TextureUsageBits bits )
    {
      var enums = Enum.GetValues<RenderingDevice.TextureUsageBits>();

      var sb = new StringBuilder();
      var first = true;

      Array.ForEach( enums, 
        e => 
        {
          if ( ! BitMath.IsMaskSet( (long)bits, (long)e ) )
          {
            return;
          } 

          if ( first ){ first = false; }
          else { sb.Append( ", " ); }

          sb.Append( e + "" );
          
        }
      );

      return sb.ToString();
    }

    public const RenderingDevice.TextureUsageBits Usage_Default = 
                    RenderingDevice.TextureUsageBits.SamplingBit |
                    RenderingDevice.TextureUsageBits.StorageBit |
                    RenderingDevice.TextureUsageBits.CpuReadBit |
                    RenderingDevice.TextureUsageBits.CanUpdateBit |
                    RenderingDevice.TextureUsageBits.CanCopyFromBit |
                    RenderingDevice.TextureUsageBits.CanCopyToBit;                    
   

    public static RDTextureFormat DefaultFormat( int w, int h,  RenderingDevice.DataFormat dataFormat = RenderingDevice.DataFormat.R16G16B16A16Unorm )
    {
      var format = new RDTextureFormat();

      format.Width  = (uint) w;
      format.Height = (uint) h;
      format.Format = dataFormat;
      format.UsageBits = Usage_Default;

      return format;
    }

    public static RDTextureFormat FormatChangeSize( RDTextureFormat tf, Vector2I size )
    {
      return FormatChangeSize( tf, size.X, size.Y );
    }
    
    public static RDTextureFormat FormatChangeSize( RDTextureFormat tf, int w, int h )
    {
      var clone = FormatCopy( tf );
      clone.Width = (uint) w;
      clone.Height = (uint) h;
      return clone;
    }

    public static int GetNumPixels( SubViewport viewport )
    {
      return viewport.Size.X * viewport.Size.Y;
    }

    public static int GetNumBytes( SubViewport viewport )
    {
      var numChannels = viewport.TransparentBg ? 4 : 3;
      var encoding = viewport.UseHdr2D ? 2 : 1;

      return GetNumPixels( viewport ) * numChannels * encoding;
    }
  
    public static RenderingDevice.DataFormat GetDataFormat( Viewport viewport )
    {
      var imageFormat = viewport.GetTexture().GetImage().GetFormat();
      return ImageFormatToDataFormat( imageFormat );
    }

    public static RDTextureFormat FormatCopy( RDTextureFormat tf )
    {
      var format = new RDTextureFormat();

      format.Format = tf.Format;

      format.Width  = tf.Width;
      format.Height = tf.Height;
      format.Depth = tf.Depth;

      format.ArrayLayers = tf.ArrayLayers;
      format.Mipmaps = tf.Mipmaps;
      format.TextureType = tf.TextureType;
      format.Samples = tf.Samples;      
      format.UsageBits = tf.UsageBits;
      format.IsResolveBuffer = tf.IsResolveBuffer;
      format.IsDiscardable = tf.IsDiscardable;

      return tf;

    }

    public static RenderingDevice.DataFormat DataFormat( int channels, PixelChannelEncoding encoding )
    {
      if ( 1 == channels )
      {
        return DataFormat_1Channel( encoding );
      }

      if ( 2 == channels )
      {
        return DataFormat_2Channels( encoding );
      }

      if ( 3 == channels )
      {
        return DataFormat_3Channels( encoding );
      }

      if ( 4 == channels )
      {
        return DataFormat_4Channels( encoding );
      }

      return RenderingDevice.DataFormat.Max;
    } 

    public static RenderingDevice.DataFormat ImageFormatToDataFormat( Image.Format imageFormat )
    {
      // RJLog.Log( "ImageFormatToDataFormat", imageFormat );

      switch ( imageFormat )
      {
        case Image.Format.Rgb8: return RenderingDevice.DataFormat.R8G8B8Uint;
        case Image.Format.Rgba8: return RenderingDevice.DataFormat.R8G8B8A8Uint;
        case Image.Format.Rgbah: return RenderingDevice.DataFormat.R16G16B16A16Sfloat;
      }

      return RenderingDevice.DataFormat.Max;
    }

    public static Image.Format DataFormatToImageFormat( RenderingDevice.DataFormat dataFormat )
    {
      switch ( dataFormat  )
      {
        case RenderingDevice.DataFormat.R8G8B8A8Uint: return Image.Format.Rgba8;
        case RenderingDevice.DataFormat.R8G8B8Uint: return Image.Format.Rgb8;  
        case RenderingDevice.DataFormat.R16G16B16A16Sfloat: return Image.Format.Rgbah;  
      }

      return Image.Format.Max;
    }

    public static RenderingDevice.DataFormat DataFormat_1Channel( PixelChannelEncoding encoding )
    {      
      switch ( encoding )
      {
        case PixelChannelEncoding.uInt8: return RenderingDevice.DataFormat.R8Uint;
      }

      return RenderingDevice.DataFormat.Max;
    }

    public static RenderingDevice.DataFormat DataFormat_2Channels( PixelChannelEncoding encoding )
    {      
      switch ( encoding )
      {
        case PixelChannelEncoding.uInt8: return RenderingDevice.DataFormat.R8Uint;
      }

      return RenderingDevice.DataFormat.Max;
    }

    public static RenderingDevice.DataFormat DataFormat_3Channels( PixelChannelEncoding encoding )
    {      
      switch ( encoding )
      {
        case PixelChannelEncoding.uInt8: return RenderingDevice.DataFormat.R8G8B8Uint;
        case PixelChannelEncoding.sInt8: return RenderingDevice.DataFormat.R8G8B8Sint;

        case PixelChannelEncoding.uInt16: return RenderingDevice.DataFormat.R16G16B16Uint;
        case PixelChannelEncoding.sInt16: return RenderingDevice.DataFormat.R16G16B16Sint;
        case PixelChannelEncoding.sFloat16: return RenderingDevice.DataFormat.R16G16B16Sfloat;
        case PixelChannelEncoding.uNorm16: return RenderingDevice.DataFormat.R16G16B16Unorm;
        case PixelChannelEncoding.sNorm16: return RenderingDevice.DataFormat.R16G16B16Snorm;

        case PixelChannelEncoding.uInt32: return RenderingDevice.DataFormat.R32G32B32Uint;
        case PixelChannelEncoding.sInt32: return RenderingDevice.DataFormat.R32G32B32Sint;
        case PixelChannelEncoding.sFloat32: return RenderingDevice.DataFormat.R32G32B32Sfloat;
        
      }

      return RenderingDevice.DataFormat.Max;
    }
    

   

    public static RenderingDevice.DataFormat DataFormat_4Channels( PixelChannelEncoding encoding )
    {      
      switch ( encoding )
      {
        
        case PixelChannelEncoding.uInt8: return RenderingDevice.DataFormat.R8G8B8A8Uint;
        case PixelChannelEncoding.sInt8: return RenderingDevice.DataFormat.R8G8B8A8Sint;

        case PixelChannelEncoding.uInt16: return RenderingDevice.DataFormat.R16G16B16A16Uint;
        case PixelChannelEncoding.sInt16: return RenderingDevice.DataFormat.R16G16B16A16Sint;
        case PixelChannelEncoding.sFloat16: return RenderingDevice.DataFormat.R16G16B16A16Sfloat;
        case PixelChannelEncoding.uNorm16: return RenderingDevice.DataFormat.R16G16B16A16Unorm;
        case PixelChannelEncoding.sNorm16: return RenderingDevice.DataFormat.R16G16B16A16Snorm;

        case PixelChannelEncoding.uInt32: return RenderingDevice.DataFormat.R32G32B32A32Uint;
        case PixelChannelEncoding.sInt32: return RenderingDevice.DataFormat.R32G32B32A32Sint;
        case PixelChannelEncoding.sFloat32: return RenderingDevice.DataFormat.R32G32B32A32Sfloat;
        
      }

      return RenderingDevice.DataFormat.Max;
    }

    // m: case ((?:\w|\.)*): return ((?:\w|\.)*);
    // r: case $2: return $1;

    public static PixelChannelEncoding GetPixelChannelEncoding( RenderingDevice.DataFormat format )
    {
      switch ( format )
      {
        case RenderingDevice.DataFormat.R8G8B8Sint: return PixelChannelEncoding.sInt8;

        case RenderingDevice.DataFormat.R16G16B16Uint: return PixelChannelEncoding.uInt16;
        case RenderingDevice.DataFormat.R16G16B16Sint: return PixelChannelEncoding.sInt16;
        case RenderingDevice.DataFormat.R16G16B16Sfloat: return PixelChannelEncoding.sFloat16;
        case RenderingDevice.DataFormat.R16G16B16Unorm: return PixelChannelEncoding.uNorm16;
        case RenderingDevice.DataFormat.R16G16B16Snorm: return PixelChannelEncoding.sNorm16;

        case RenderingDevice.DataFormat.R32G32B32Uint: return PixelChannelEncoding.uInt32;
        case RenderingDevice.DataFormat.R32G32B32Sint: return PixelChannelEncoding.sInt32;
        case RenderingDevice.DataFormat.R32G32B32Sfloat: return PixelChannelEncoding.sFloat32;

        case RenderingDevice.DataFormat.R8G8B8A8Uint: return PixelChannelEncoding.uInt8;
        case RenderingDevice.DataFormat.R8G8B8A8Sint: return PixelChannelEncoding.sInt8;

        case RenderingDevice.DataFormat.R16G16B16A16Uint: return PixelChannelEncoding.uInt16;
        case RenderingDevice.DataFormat.R16G16B16A16Sint: return PixelChannelEncoding.sInt16;
        case RenderingDevice.DataFormat.R16G16B16A16Sfloat: return PixelChannelEncoding.sFloat16;
        case RenderingDevice.DataFormat.R16G16B16A16Unorm: return PixelChannelEncoding.uNorm16;
        case RenderingDevice.DataFormat.R16G16B16A16Snorm: return PixelChannelEncoding.sNorm16;

        case RenderingDevice.DataFormat.R32G32B32A32Uint: return PixelChannelEncoding.uInt32;
        case RenderingDevice.DataFormat.R32G32B32A32Sint: return PixelChannelEncoding.sInt32;
        case RenderingDevice.DataFormat.R32G32B32A32Sfloat: return PixelChannelEncoding.sFloat32;
      }

      return PixelChannelEncoding.Unknown;
    }

    public static int GetNumChannels( RenderingDevice.DataFormat format )
    {
      switch ( format )
      {
        case RenderingDevice.DataFormat.R8G8B8Sint: 

        case RenderingDevice.DataFormat.R16G16B16Uint: 
        case RenderingDevice.DataFormat.R16G16B16Sint: 
        case RenderingDevice.DataFormat.R16G16B16Sfloat: 
        case RenderingDevice.DataFormat.R16G16B16Unorm:
        case RenderingDevice.DataFormat.R16G16B16Snorm:

        case RenderingDevice.DataFormat.R32G32B32Uint: 
        case RenderingDevice.DataFormat.R32G32B32Sint: 
        case RenderingDevice.DataFormat.R32G32B32Sfloat:
        
         return 3;

        case RenderingDevice.DataFormat.R8G8B8A8Uint:
        case RenderingDevice.DataFormat.R8G8B8A8Sint:

        case RenderingDevice.DataFormat.R16G16B16A16Uint:
        case RenderingDevice.DataFormat.R16G16B16A16Sint: 
        case RenderingDevice.DataFormat.R16G16B16A16Sfloat: 
        case RenderingDevice.DataFormat.R16G16B16A16Unorm:
        case RenderingDevice.DataFormat.R16G16B16A16Snorm: 

        case RenderingDevice.DataFormat.R32G32B32A32Uint:
        case RenderingDevice.DataFormat.R32G32B32A32Sint: 
        case RenderingDevice.DataFormat.R32G32B32A32Sfloat:
          return 4;
      }

      return -1;
    }

    static void WriteUShort( ushort shortValue, byte[] output, int offset )
    {
      output[ offset + 0 ] = (byte)(shortValue & 0xFF); 
      output[ offset + 1 ] = (byte)((shortValue >> 8) & 0xFF); 
    }

    static void WriteUNorm( float value, byte[] output, int offset )
    {
      var shortValue = (ushort)( Mathf.Clamp( value, 0.0f, 1.0f ) * 65535 );
      WriteUShort( shortValue, output, offset );
    }

    static ushort FloatToHalf( float value )
    {
      uint fbits = BitConverter.SingleToUInt32Bits(value);

      uint sign = (fbits >> 16) & 0x8000;
      uint val  = (fbits & 0x7FFFFFFF);

      // NaN / Inf -> maximal darstellen
      if ( val > 0x47FFEFFF )
      {
        return (ushort)(sign | 0x7FFF); 
      }
      
      if ( val < 0x38800000)  // Subnormal
      {
        uint mant = (val & 0x007FFFFF) | 0x00800000;
        int exp = 113 - (int)(val >> 23);
        mant = (mant >> exp);

        return (ushort)(sign | (mant >> 13));
      }
      else
      {
        uint exp = (val >> 23) - 112;
        uint mant = (val & 0x007FFFFF);          
        return (ushort)(sign | (exp << 10) | (mant >> 13));
      }
    }

    static void WriteFloat16( float value, byte[] output, int offset )
    {
      ushort r = FloatToHalf( value );
      WriteUShort( r, output, offset );
    } 
    
    public static byte[] ColorToBytes( Color color, RenderingDevice.DataFormat format )
    {
      switch ( format )
      {
        case RenderingDevice.DataFormat.R8G8B8A8Uint:
        {
          var bytes = new byte[ 4 ];
          bytes[ 0 ] = (byte) ( color.R * 255 ); 
          bytes[ 1 ] = (byte) ( color.G * 255 ); 
          bytes[ 2 ] = (byte) ( color.B * 255 ); 
          bytes[ 3 ] = (byte) ( color.A * 255 ); 
          return bytes;
        }

        case RenderingDevice.DataFormat.R16G16B16A16Unorm:
        {
          var bytes = new byte[ 8 ];
          WriteUNorm( color.R, bytes, 0 );
          WriteUNorm( color.G, bytes, 2 );
          WriteUNorm( color.B, bytes, 4 );
          WriteUNorm( color.A, bytes, 6 );
          return bytes;
        }

        case RenderingDevice.DataFormat.R16G16B16A16Sfloat:
        {
          var bytes = new byte[ 8 ];
          WriteFloat16( color.R, bytes, 0 );
          WriteFloat16( color.G, bytes, 2 );
          WriteFloat16( color.B, bytes, 4 );
          WriteFloat16( color.A, bytes, 6 );
          return bytes;
        }
        
      }

      return null;
    }
  }
}