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

namespace Rokojori
{
  public static class MathX 
  {
    public const float fps120Delta = 1/120f;

    public const float DegreesToRadians = Mathf.Pi / 180f;
    public const float RadiansToDegrees = 180f / Mathf.Pi;

    static Dictionary<int,int> factorialLookUp = new Dictionary<int, int>();

    public static int Factorial( int i )
    { 
      if ( factorialLookUp.ContainsKey( i ) )
      {
        return factorialLookUp[ i ];
      }

      var result = 1;

      if ( i > 1 )
      {
        var before = Factorial( i - 1 );
        result = before * i;
      }

      factorialLookUp[ i ] = result;

      return factorialLookUp[ i ];

    }

    public static float Min( params float[] values )
    {
      var value = float.MaxValue;

      for ( int i = 0; i < values.Length; i++ )
      {
        value = Mathf.Min( values[ i ], value );
      }

      return value;
    }

    public static float Max( params float[] values )
    {
      var value = - float.MaxValue;

      for ( int i = 0; i < values.Length; i++ )
      {
        value = Mathf.Max( values[ i ], value );
      }

      return value;
    }

    public static float Sample( float tl, float tr, float bl, float br, Vector2 uv )
    {
      return Mathf.Lerp( tl, tr, uv.X ) + 
             ( bl - tl ) * uv.Y * ( 1.0f - uv.X ) + 
             ( br - tr ) * uv.X * uv.Y;
    }

    public static float Sample( 
      float ba, float bb, float bc, float bd, 
      float ta, float tb, float tc, float td, 
      Vector3 uvw 
    )
    {
      var bottom = Sample( ba, bb, bc, bd, new Vector2( uvw.X, uvw.Y ) );
      var top    = Sample( ta, tb, tc, td, new Vector2( uvw.X, uvw.Y ) );

      return Mathf.Lerp( bottom, top, uvw.Z );
    }

    public static float Fract( float value )
    {
      return value - Mathf.Floor( value );
    }

    public static float TimeLerpAmountExp( float t, float time )
    {
      return Mathf.Exp( -t * time );
    }

    public static float TimeLerp( float lastValue, float nextValue, float t, float time )
    {
      return Mathf.Lerp( nextValue, lastValue, TimeLerpAmountExp( t, time ) );
    }

    public static float AbsoluteDeltaAngle( float degreesA, float degreesB )
    {
      return Mathf.Abs( AngleDelta( degreesA, degreesB ) );
    } 

    public static float NormalizeAngle( float degrees )
    {
      while ( degrees > 180 )
      {
        degrees -= 360;
      }

      while ( degrees < -180 )
      {
        degrees += 360;
      }
      
      return degrees;
    }

    public static float Mix( float a, float b, float mix )
    {
      return a + mix * ( b - a );
    }

    public static float PowerValue( float polarValue, float positivePower = 10f, float negativePower = 3f  )
    {
      float t = Smoothstep( -1.0f, 1.0f, polarValue );
      float basis = Mix( negativePower, positivePower, t);

      return Mathf.Pow( basis, polarValue );
    }

    public static float Smoothstep ( float edge0, float edge1, float x ) 
    {
      x = MathX.Clamp01( ( x - edge0 ) / ( edge1 - edge0 ) );

      return x * x * ( 3.0f - 2.0f * x );
    }

    public static float SnapRounded( float value, float snappingDistance )
    {
      return Mathf.Round( value / snappingDistance ) * snappingDistance;
    }

    public static float SnapCeiled( float value, float snappingDistance )
    {
      return Mathf.Ceil( value / snappingDistance ) * snappingDistance;
    }

    public static float SnapFloored( float value, float snappingDistance )
    {
      return Mathf.Floor( value / snappingDistance ) * snappingDistance;
    }

    public static Vector2 RadiansToVector2( float angle )
    {
      return new Vector2( Mathf.Cos( angle ), Mathf.Sin( angle ) );
    }

    public static float Vector2ToRadians( Vector2 circle )
    {
      return Mathf.Atan2( circle.Y, circle.X );
    }

    public static float LerpAngleDegrees( float a, float b, float weight )
    {
      return Mathf.LerpAngle( DegreesToRadians * a, DegreesToRadians * b, weight ) * RadiansToDegrees;
    }

    public static int MultiIndexToFlatIndex( List<int> indices, List<int> sizes )
    {
      var index = 0;
      var scale = 1;

      for ( int i = sizes.Count - 1; i >= 0; i-- )
      {
        index  += indices[ i ] * scale;
        scale *= sizes[ i ];
      }

      return index;
    }

    public static int MultiIndexToFlatIndex( Vector3I indices, Vector3I sizes )
    {
      var index = 0;
      
      index += indices.Z;
      index += indices.Y * sizes.Z;
      index += indices.X * sizes.Z * sizes.Y;
      return index;
    }

    public static int MultiIndexToFlatIndex( Vector2I indices, Vector2I sizes )
    {
      var index = 0;
      
      index += indices.Y;
      index += sizes.X * sizes.Y;
      return index;
    }

    public static List<int> FlatIndexToMultiIndex( int index, List<int> sizes, List<int> multiIndex = null )
    {
      multiIndex = multiIndex == null ? new List<int>( new int[ sizes.Count ] ) : multiIndex;

      for ( int i = sizes.Count - 1; i >= 0; i-- )
      {
        multiIndex[ i ] = index % sizes[ i ];
        index /= sizes[i];
      }

      return multiIndex;
    }

    public static Vector3I FlatIndexToMultiIndex( int index, Vector3I sizes )
    {
      var multiIndex = Vector3I.Zero;

      multiIndex.Z = index % sizes.Z;
      index /= sizes.Z;

      multiIndex.Y = index % sizes.Y;
      index /= sizes.Y;

      multiIndex.X = index % sizes.X;
      
      return multiIndex;
    }


    public static Vector2I FlatIndexToMultiIndex( int index, Vector2I sizes )
    {
      var multiIndex = Vector2I.Zero;

      multiIndex.Y = index % sizes.Y;
      index /= sizes.Y;

      multiIndex.X = index % sizes.X;
      
      return multiIndex;
    }


    

    public static float AngleDelta( float degreesA, float degreesB)
    {
      var angleDelta = degreesB - degreesA;
      angleDelta = ( angleDelta + 180f ) % 360f - 180f;
      return angleDelta;
    }

    static float CustomModulo( float a, float n )
    {
      return a - Mathf.Floor( a / n ) * n;
    } 

    public static bool IsInvalidFloat( float value )
    {
      return float.IsNaN( value ) || float.IsInfinity( value );
    }

    public static float EnsureValidFloat( float value, float invalidValue = 0f )
    {
      if ( float.IsNaN( value ) || float.IsInfinity( value ) )
      {
        return invalidValue;
      }

      return value;
    }

    public static float Clamp01( float value, float invalidValue = 0f )
    {
      if ( float.IsNaN( value ) || float.IsInfinity( value ) )
      {
        return invalidValue;
      }

      return Mathf.Clamp( value, 0, 1 );
    }   

    public static float RemapClamped( float value, float inMin, float inMax, float outMin, float outMax )
    {
      var mapped = MathX.Clamp01( ( value - inMin ) / ( inMax - inMin ) );
      return mapped * ( outMax - outMin ) + outMin;
    }

    public static float Normalize( float value, float min, float max )
    {
      return ( value - min ) / ( max - min );
    }

    public static float NormalizeClamped( float value, float min, float max )
    {
      return MathX.Clamp01( Normalize( value, min, max ) );
    }

    public static float Map( float value, float inputMin, float inputMax, 
    float outputMin, float outputMax )
    {
      var normalized = Normalize( value, inputMin, inputMax );
      return normalized * ( outputMax - outputMin ) + outputMin;
    }

    public static float MapPositive( float value, float inputMax, float outputMax )
    {
      return Map( value, 0, inputMax, 0, outputMax );
    }

    
    public static float MapClamped( float value, float inputMin, float inputMax, 
    float outputMin, float outputMax )
    {
      var normalized = NormalizeClamped( value, inputMin, inputMax );
      return normalized * ( outputMax - outputMin ) + outputMin;
    }

    public static float Map01( float value, float outputMin, float outputMax )
    {
      return value * ( outputMax - outputMin ) + outputMin;
    }

    public static float MapPolar( float value, float min, float max )
    {
      return Map01( value * 0.5f + 0.5f, min, max );
    }

    public static float MapPolarTo01( float value)
    {
      return value * 0.5f + 0.5f;
    }

    public static int NextPowerOfTwoExponent( int num )
    {
      var p = Exponent( 2, num );

      return Mathf.CeilToInt( p );
    }

    public static int NextPowerOfTwoExponent( float num )
    {
      return NextPowerOfTwoExponent( Mathf.RoundToInt( num ) );
    }

    public static int NextPowerOfTwo( float num )
    {
      var exponent = NextPowerOfTwoExponent( num );
      return 1 << exponent;
    }

    public static float Repeat( float value, float length )
    {
      while ( value > length )
      {
        value -=length;
      }

      while ( value < 0 )
      {
        value += length;
      }

      return value;
    }

    public static float Triangle( float value )
    {
      value = MathX.Repeat( value, 1 ) * 2;
      
      if ( value > 1 )
      {
        value = 2 - value;
      }

      return value;
    }

    public static float EaseSine( float value )
    {
      return Mathf.Sin( Mathf.Pi * ( value *  0.5f + 1.5f ) ) + 1;
    }

    public static int Sign( int value )
    {
      return value == 0 ? 0 : value < 0 ? -1 : 1;
    }

    
    public static T LerpList<T>( List<T> data, float t, Func<T,T,float,T> lerpElementsFunction, 
      int dataFillSize = -1 )
    {
      dataFillSize = dataFillSize == -1 ? data.Count : dataFillSize;
      var floatIndex = t * dataFillSize;
      
      if ( floatIndex <= 0 )
      {
        return data[ 0 ];
      }

      if ( floatIndex >= ( dataFillSize - 1 ) )
      {
        return data[ dataFillSize - 1 ];
      }

      var flooredIndex = Mathf.FloorToInt( floatIndex );
      var ceiledIndex = flooredIndex + 1;
      
      var flooredValue = data[ flooredIndex ];
      var ceiledValue = data[ ceiledIndex ];

      var interpolationAmount = floatIndex - flooredIndex;
      
      return lerpElementsFunction( flooredValue, ceiledValue, interpolationAmount );

    }

    public static float PolarTriangle( float value )
    {
      return Triangle( value ) * 2f - 1f; 
    }

    public static float Step( float phase, float phaseStart, float phaseEnd )
    {
      if ( phase < phaseStart )
      {
        return 0;
      }
    
      if ( phase >= phaseEnd )
      {
        return 1;
      }

      return ( phase - phaseStart ) / ( phaseStart - phaseEnd ); 
    }

    public static int SafeIndex<T>( int index, List<T> elements, bool wrap = false )
    { 
      return SafeIndex( index, elements.Count, wrap );
    }

    public static int SafeIndex<T>( int index, T[] elements, bool wrap = false )
    { 
      return SafeIndex( index, elements.Length, wrap );
    }

    public static int SafeIndex( int index, int maxElements, bool wrap = false )
    {
      if ( wrap )
      {
        return MathX.Repeat( index, maxElements );
      }
      else
      {
        return Mathf.Clamp( index, 0, maxElements - 1 );
      }
    }


    public static int Repeat( int value, int range )
    {
      while ( value < 0 )
      {
        value += range;
      }

      while ( value >= range )
      {
        value -= range;
      }

      return value;
    }

    public static float PolarRepeat( float value, float range )
    {
      while ( value < -range )
      {
        value += range * 2;
      }

      while ( value >= range )
      {
        value -= range * 2; 
      }

      return value;
    }

    public static Curve CurveFromPoints( params float[] points )
    {
      var curve = new Curve();

      for ( int i = 0; i < points.Length; i++ )
      {
        var t = (float) i / (float) ( points.Length - 1f );
        curve.AddPoint( new Vector2( t, points[ i ] ) );

        if ( i != 0 )
        {
          curve.SetPointLeftMode( i, Godot.Curve.TangentMode.Linear );
        }

        if ( i != points.Length - 1  )
        {
          curve.SetPointRightMode( i, Godot.Curve.TangentMode.Linear );
        }        
        
      }

      return curve;
    }

    public static Curve Curve( float y0, float y1, float minValue = 0, float maxValue = 1, float minX = 0, float maxX = 1 )
    {
      var curve = new Curve();
      curve.AddPoint( new Vector2( minX, y0 ) );
      curve.AddPoint( new Vector2( maxX, y1 ) );

      curve.SetPointRightMode( 0, Godot.Curve.TangentMode.Linear );
      curve.SetPointLeftMode( 1, Godot.Curve.TangentMode.Linear );

      curve.MinDomain = minX;
      curve.MaxDomain = maxX; 
      curve.MinValue = minValue;  
      curve.MaxValue = maxValue;

      return curve;
    }

    public static Curve Curve( float y )
    {
      return Curve( y, y );
    }

    public static float CurveStartPosition( this Curve c )
    {
      if ( c.PointCount == 0 )
      {
        return 0;
      }

      return c.GetPointPosition( 0 ).X;
    }

    public static float CurveEndPosition( this Curve c )
    {
      if ( c.PointCount == 0 )
      {
        return 0;
      }

      return c.GetPointPosition( c.PointCount - 1 ).X;
    }

    public static float CurveAngle( Curve c, float t, float samplingRange = 0.01f )
    {
      var x0 = Mathf.Max( t - samplingRange, 0 );
      var x1 = Mathf.Min( t + samplingRange, 1 );

      var y0 = c.Sample( x0 );
      var y1 = c.Sample( x1 );

      return Mathf.Atan2( y1 - y0, x1 - x0 );
    }

    public static Curve ScaleCurve( Curve c, float scale )
    {
      var clone = (Curve) c.Duplicate( true );

      for ( int i = 0; i < clone.PointCount; i++ )
      {
        var y = clone.GetPointPosition( i ).Y;
        clone.SetPointValue( i, y * scale);
      }

      return clone;
    }

    public static float CurveMaximum( Curve c, int numSamples = 20 )
    {
      var max = 0f;

      for ( int i = 0; i < numSamples; i++ )
      {
        max = Mathf.Max( max, c.Sample( (float) i / ( numSamples - 1 ) ) ); 
      }

      return max;
    }

    public static float CurveAverage( this Curve c, int numSamples = 20 )
    {
      if ( c == null )
      {
        return 0;
      }

      var value = 0f;

      for ( int i = 0; i < numSamples; i++ )
      {
        value += c.Sample( (float) i / ( numSamples - 1 ) ); 
      }

      return value / numSamples;
    }

    public static List<float> GetCurveWeights( Curve curve, int num, bool normalize = true )
    {
      var sum = 0f;
      
      var weights = new List<float>();

      for ( int i = 0; i < num; i++ )
      { 
        var t = (float)i / ( num - 1 );
        var w = curve.Sample( t );
        sum += w;
        weights.Add( w );
      }

      if ( normalize )
      {
        for ( int i = 0; i < num; i++ )
        {
          weights[ i ] = weights[ i ] / sum;
        }
      }

      return weights;
    }

    public static float Exponent( float base_, float power  )
    {
      return Mathf.Log( power ) / Mathf.Log( base_ );
    }

    public static float Base( float exponent, float power )
    {
      return Mathf.Pow( power, 1f / exponent );
    }

    public static float SmoothingCoefficient( float ms, float reachingTarget = 0.1f, float frameDurationMS = MathX.fps120Delta )
    {
      return 1f - Base( ms / frameDurationMS, reachingTarget );
    }

    public static float SmoothValue( float oldValue, float newValue, float ms, float reachingTarget = 0.1f, float frameDurationMS = MathX.fps120Delta )
    {
      return oldValue + SmoothingCoefficient( ms, reachingTarget, frameDurationMS ) * ( newValue - oldValue ); 
    } 

    public static float SmoothDegrees( float oldValue, float newValue, float ms, float reachingTarget = 0.1f, float frameDurationMS = MathX.fps120Delta )
    {
      oldValue = Mathf.Wrap( oldValue, 0, 360 );
      newValue = Mathf.Wrap( newValue, 0, 360 );

      var difference = newValue - oldValue;

      if ( Mathf.Abs( difference ) > 180 )
      {
        if ( newValue > oldValue )
        {
          oldValue += 360;
        }
        else
        {
          newValue += 360;
        }
      }

      return oldValue + SmoothingCoefficient( ms, reachingTarget, frameDurationMS ) * ( newValue - oldValue ); 
    } 

    public static Vector3 SmoothVector3( Vector3 oldValue, Vector3 newValue, float ms, float reachingTarget = 0.1f, float frameDurationMS = 8.33333333333f )
    {
      return oldValue + SmoothingCoefficient( ms, reachingTarget, frameDurationMS ) * ( newValue - oldValue ); 
    } 


   

    public static float PolarAxis( bool negative, bool positive )
    {
      return ( negative ? -1 : 0 ) + ( positive ? 1 : 0 );
    }

  }
}