#include "VentuzPluginModule.h"
#include "Framework/Application/SlateApplication.h"
#include "Windows/WindowsApplication.h"
#include "RendererInterface.h"

#include "EngineGlobals.h"
#include "Engine.h"
#include <WinSock.h>
#include "Slate/SceneViewport.h"
#include "Scalability.h"
#include "Runtime/Engine/Classes/Engine/UserInterfaceSettings.h"
#include "Runtime/Engine/Classes/Engine/RendererSettings.h"
#include "GameFramework/GameUserSettings.h"
#include "Runtime/Launch/Resources/Version.h"

#include "Misc/ConfigCacheIni.h"
#include "Blueprints/TextureShareBlueprintContainers.h"
#include "ITextureShare.h"
#include "TextureSharePlugin/Private/ITextureShareAPI.h"
#include "TextureSharePlugin/Private/ITextureShareObject.h"

#if WITH_EDITOR
#include "Editor/EditorPerformanceSettings.h"
#include "Editor/UnrealEd/Classes/Editor/EditorEngine.h"
#include "UnrealEd.h"
#include "IAssetViewport.h"
#include "LevelEditor.h"
#include "LevelEditorViewport.h"
#include "SceneView.h"
#endif

uint ChecksumCRC32( const uint8* data, __int64 size );
uint ExpandChecksumCRC32( uint startHash, const uint8* data, __int64 size );

FString GuidToText( const GUID& guid )
{
  char guidText[256];
  sprintf_s( guidText, "%x%x%x%x%x_\0", (uint32_t)guid.Data1, (uint32_t)guid.Data2, (uint32_t)guid.Data3, *( (uint32_t*)&guid.Data4[0] ), *( (uint32_t*)&guid.Data4[4] ) );
  return FString( guidText );
}

FString BinToHex( const void* data, int size )
{
  static const char* hexStr = "0123456789ABCDEF";

  int bufferSize = size * 2 + 1;
  char* hex = new char[bufferSize];
  memset( hex, 0, size * 2 + 1 );

  for ( int x = 0; x < size; x++ )
  {
    unsigned char chr = ( (unsigned char*)data )[x];
    hex[x * 2] = hexStr[chr >> 4];
    hex[x * 2 + 1] = hexStr[chr & 0x0f];
  }

  FString result = FString( (ANSICHAR*)hex );

  delete[] hex;

  return result;
}

FString BinToHexWithFrameID( const void* data, int size, int frameID )
{
  FString frame = BinToHex( &frameID, 4 );
  return frame + BinToHex( data, size );
}

FString KeyToHex( Ventuz::UE4::AncID id, uint hash )
{
  Ventuz::UE4::DataKey key( id, hash );
  return BinToHex( &key, sizeof( Ventuz::UE4::DataKey ) );
}

FString KeyToHex( Ventuz::UE4::AncID id, const GUID& guid )
{
  Ventuz::UE4::DataKey key( id, guid );
  return BinToHex( &key, sizeof( Ventuz::UE4::DataKey ) );
}

class OscParser
{
public:
  uint8* Scan;
  uint8* End;

  OscParser()
  {
    End = Scan = (uint8*)"";
  }
  void Start( uint8* ptr, int bytes )
  {
    Scan = ptr;
    End = Scan + bytes;
  }

  char* String()
  {
    char* result = (char*)Scan;
    int n = 0;
    while ( Scan[n++] != 0 );
    Scan += ( n + 3 ) & ~3;

    return result;
  }
  uint UInt()
  {
    uint r = ( Scan[0] << 24 ) | ( Scan[1] << 16 ) | ( Scan[2] << 8 ) | ( Scan[3] << 0 );
    Scan += 4;
    return r;
  }
  int Int()
  {
    int r = ( Scan[0] << 24 ) | ( Scan[1] << 16 ) | ( Scan[2] << 8 ) | ( Scan[3] << 0 );
    Scan += 4;
    return r;
  }
  double Float()
  {
    uint32 data = ( Scan[0] << 24 ) | ( Scan[1] << 16 ) | ( Scan[2] << 8 ) | ( Scan[3] << 0 );
    float r = *( (float*)&data );
    Scan += 4;
    return r;
  }
  double Double()
  {
    uint8 data[8];
    for ( int x = 0; x < 8; x++ )
      data[7 - x] = Scan[x];
    double r = *( (double*)data );
    Scan += 8;
    return r;
  }
};

static const FName LevelEditorName( TEXT( "LevelEditor" ) );

void FindSceneViewport( TWeakPtr<FSceneViewport>& OutSceneViewport )
{
#if WITH_EDITOR
  if ( GIsEditor )
  {
    for ( const FWorldContext& Context : GEngine->GetWorldContexts() )
    {
      if ( Context.WorldType == EWorldType::Editor )
      {
        if ( FModuleManager::Get().IsModuleLoaded( LevelEditorName ) )
        {
          FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>( LevelEditorName );
          TSharedPtr<IAssetViewport> ActiveLevelViewport = LevelEditorModule.GetFirstActiveViewport();
          if ( ActiveLevelViewport.IsValid() )
          {
            OutSceneViewport = ActiveLevelViewport->GetSharedActiveViewport();
          }
        }
      }
      else if ( Context.WorldType == EWorldType::PIE )
      {
        FSlatePlayInEditorInfo* SlatePlayInEditorSession = GEditor->SlatePlayInEditorMap.Find( Context.ContextHandle );
        if ( SlatePlayInEditorSession )
        {
          if ( SlatePlayInEditorSession->DestinationSlateViewport.IsValid() )
          {
            TSharedPtr<IAssetViewport> DestinationLevelViewport = SlatePlayInEditorSession->DestinationSlateViewport.Pin();
            OutSceneViewport = DestinationLevelViewport->GetSharedActiveViewport();
          }
          else if ( SlatePlayInEditorSession->SlatePlayInEditorWindowViewport.IsValid() )
          {
            OutSceneViewport = SlatePlayInEditorSession->SlatePlayInEditorWindowViewport;
          }
        }
      }
    }
  }
  else
#endif
  {
    UGameEngine* GameEngine = Cast<UGameEngine>( GEngine );
    OutSceneViewport = GameEngine->SceneViewport;
  }
}

FVentuzInputDevice::FVentuzInputDevice( const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler/*, TArray<UPixelStreamerInputComponent*>& InInputComponents*/ )
  : appWrapper( MakeShareable( new FVentuzApplicationWrapper( FSlateApplication::Get().GetPlatformApplication() ) ) )
  , MessageHandler( InMessageHandler )
{
  for ( int x = 0; x < 10; x++ )
    touchIndexMap[x] = -1;
}

void FVentuzInputDevice::ParseOSCBundle( std::string data )
{
  OscParser osc;
  osc.Start( (uint8*)data.data(), data.length() );

  if ( strcmp( osc.String(), "#bundle" ) != 0 )
    return;

  uint64 Timecode;
  Timecode = osc.UInt();
  Timecode = ( Timecode << 32 ) | osc.UInt();

  uint8* pos = osc.Scan;
  uint size = 0;

  GenericApplication* WindowsApplication = FSlateApplication::Get().GetPlatformApplication().Get();


  UGameViewportClient* vp = nullptr;
  if ( GEngine && GEngine->GameViewport )
    vp = GEngine->GameViewport;

  FVector2D viewportSize;

  if ( vp )
    vp->GetViewportSize( viewportSize );

  do
  {
    osc.Scan = pos + size;
    size = osc.UInt();
    pos = osc.Scan;

    if ( !size )
      break;

    const char* addr = osc.String();
    const char* args = *osc.Scan == ',' ? osc.String() : "";

    const char* arguments[10];
    int argCount = 0;

    for ( char* p = (char*)addr; *p; p++ )
    {
      if ( *p == '/' )
      {
        arguments[argCount++] = p + 1;
        *p = 0;
      }
    }

    if ( argCount < 3 )
      continue;

    if ( strcmp( arguments[0], "ventuz" ) )
      continue;

    int receiverID{};
    if ( sscanf_s( arguments[1], "%d", &receiverID ) != 1 )
      continue;

    const char* deviceType = arguments[2];

    if ( !strcmp( deviceType, "InputTimecode" ) )
    {
      double timeCode = osc.Double();
      continue;
    }

    if ( argCount < 6 )
      continue;

    int deviceID;
    int touchGroup;
    const char* id = arguments[4];

    if ( sscanf_s( arguments[3], "%d", &deviceID ) != 1 )
      continue;

    if ( sscanf_s( arguments[5], "%d", &touchGroup ) != 1 )
      continue;

    int boolCount = 0;
    bool boolValues[10]{};
    int intCount = 0;
    int intValues[10]{};
    int floatCount = 0;
    float floatValues[10]{};
    int stringCount = 0;
    char* stringValues[10]{};

    for ( const char* params = args; *params; params++ )
    {
      switch ( *params )
      {
      case 'F':
        boolValues[boolCount++] = false;
        break;
      case 'T':
        boolValues[boolCount++] = true;
        break;
      case 'i':
        intValues[intCount++] = osc.Int();
        break;
      case 'f':
        floatValues[floatCount++] = osc.Float();
        break;
      case 's':
        stringValues[stringCount++] = osc.String();
        break;
      default:
        break;
      }
    }

    if ( !strcmp( deviceType, "Mouse" ) && vp )
    {
      if ( !strcmp( id, "Position" ) )
      {
        int x = floatValues[0] * viewportSize.X;
        int y = floatValues[1] * viewportSize.Y;

        FIntPoint mappedPoint = vp->GetGameViewport()->ViewportToVirtualDesktopPixel( FVector2D( 0, 0 ) );

        FIntPoint p = mappedPoint + FIntPoint( x, y );

        FVector2D oldPos = WindowsApplication->Cursor->GetPosition();

        WindowsApplication->Cursor->SetPosition( p.X, p.Y );
        MessageHandler->OnRawMouseMove( p.X - oldPos.X, p.Y - oldPos.Y );

        //UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC Move event: %d %d (%d %d) %f %f (%f %f) - %d %d"), p.X, p.Y, x, y, viewportSize.X, viewportSize.Y, oldPos.X, oldPos.Y, mappedPoint.X, mappedPoint.Y);

        continue;
      }

      if ( !strcmp( id, "IsInside" ) )
      {
        if ( boolValues[0] )
        {
          FSlateApplication::Get().OverridePlatformApplication( appWrapper );
          FSlateApplication::Get().OnCursorSet();

          // Make sure the viewport is active.
          FSlateApplication::Get().ProcessApplicationActivationEvent( true );
          WindowsApplication = appWrapper.Get();
        }
        else
        {
          FSlateApplication::Get().OverridePlatformApplication( appWrapper->WrappedApplication );
          WindowsApplication = appWrapper->WrappedApplication.Get();
        }

        continue;
      }

      if ( !strcmp( id, "Hover" ) )
      {
        continue;
      }

      if ( !strcmp( id, "ButtonDown" ) )
      {
        if ( !FSlateApplication::Get().IsActive() )
        {
          FSlateApplication::Get().ProcessApplicationActivationEvent( true );
        }

        int button = intValues[0];
        if ( button & 0x100000 )
          MessageHandler->OnMouseDown( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Left );
        if ( button & 0x200000 )
          MessageHandler->OnMouseDown( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Right );
        if ( button & 0x400000 )
          MessageHandler->OnMouseDown( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Middle );
        if ( button & 0x800000 )
          MessageHandler->OnMouseDown( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Thumb01 );
        if ( button & 0x1000000 )
          MessageHandler->OnMouseDown( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Thumb02 );

        /*int x = floatValues[0] * viewportSize.X;
        int y = floatValues[1] * viewportSize.Y;

        FIntPoint p = vp->GetGameViewport()->ViewportToVirtualDesktopPixel(FVector2D()) + FIntPoint(x, y);


        UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC Click event: %x @ %d %d %f %f"), button, p.X, p.Y, viewportSize.X, viewportSize.Y);*/
        continue;
      }

      if ( !strcmp( id, "ButtonUp" ) )
      {
        int button = intValues[0];
        if ( button & 0x100000 )
          MessageHandler->OnMouseUp( EMouseButtons::Left );
        if ( button & 0x200000 )
          MessageHandler->OnMouseUp( EMouseButtons::Right );
        if ( button & 0x400000 )
          MessageHandler->OnMouseUp( EMouseButtons::Middle );
        if ( button & 0x800000 )
          MessageHandler->OnMouseUp( EMouseButtons::Thumb01 );
        if ( button & 0x1000000 )
          MessageHandler->OnMouseUp( EMouseButtons::Thumb02 );
        continue;
      }

      if ( !strcmp( id, "DoubleClick" ) || !strcmp( id, "DoubleClick2" ) )
      {
        int button = 0x100000;

        if ( !strcmp( id, "DoubleClick2" ) )
          button = intValues[0];

        if ( button & 0x100000 )
          MessageHandler->OnMouseDoubleClick( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Left );
        if ( button & 0x200000 )
          MessageHandler->OnMouseDoubleClick( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Right );
        if ( button & 0x400000 )
          MessageHandler->OnMouseDoubleClick( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Middle );
        if ( button & 0x800000 )
          MessageHandler->OnMouseDoubleClick( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Thumb01 );
        if ( button & 0x1000000 )
          MessageHandler->OnMouseDoubleClick( GEngine->GameViewport->GetWindow()->GetNativeWindow(), EMouseButtons::Thumb02 );
        continue;
      }

      if ( !strcmp( id, "Wheel" ) )
      {
        const float SpinFactor = 1 / 120.0f;
        MessageHandler->OnMouseWheel( intValues[0] * SpinFactor );
        continue;
      }

      //UE_LOG(LogVentuzPlugin, Warning, TEXT("Unhandled OSC event: %s %s %s %s %d %d | %f %f %f | %d %d %d"), *FString(addr), *FString(args), *FString(deviceType), *FString(id), deviceID, touchGroup, floatValues[0], floatValues[1], floatValues[2], intValues[0], intValues[1], intValues[2]);

      continue;
    }

    if ( !strcmp( deviceType, "Keyboard" ) )
    {
      int key = intValues[0];

      if ( !strcmp( id, "KeyDown" ) )
      {
        uint32 CharCode = ::MapVirtualKey( key & 0xffff, MAPVK_VK_TO_CHAR );
        //UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC keydown event: %d (%d)"), key, CharCode);
        MessageHandler->OnKeyDown( key, CharCode, false );
        continue;
      }
      if ( !strcmp( id, "KeyUp" ) )
      {
        uint32 CharCode = ::MapVirtualKey( key & 0xffff, MAPVK_VK_TO_CHAR );
        //UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC keyup event: %d (%d)"), key, CharCode);
        MessageHandler->OnKeyUp( key, CharCode, false );
        continue;
      }
      if ( !strcmp( id, "KeyPress" ) )
      {
        //UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC keypress event: %d"), key);
        MessageHandler->OnKeyChar( key, false );
        continue;
      }

      continue;
    }

    if ( !strcmp( deviceType, "MultiTouch" ) )
    {
      int touchType = intValues[0];
      char* touchId = stringValues[0];
      float x = floatValues[0];
      float y = floatValues[1];
      float z = floatValues[2];
      float pressure = floatValues[3];
      float angle = floatValues[4];

      int touchIDint = 0;

      if ( sscanf_s( touchId, "%d", &touchIDint ) != 1 )
        continue;

      FVector2D cursorPos = FVector2D( vp->GetGameViewport()->ViewportToVirtualDesktopPixel( FVector2D() ) ) + FVector2D( floatValues[0] * viewportSize.X, floatValues[1] * viewportSize.Y );

      if ( !strcmp( id, "TouchAdd" ) )
      {
        int touchIndex = -1;
        for ( int i = 0; i < 10; i++ )
          if ( touchIndexMap[i] == -1 )
          {
            touchIndexMap[i] = touchIDint;
            touchIndex = i;
            break;
          }

        if ( touchIndex == -1 )
          continue;

        MessageHandler->OnTouchStarted( GEngine->GameViewport->GetWindow()->GetNativeWindow(), cursorPos, pressure, touchIndex, 0 );
        continue;
      }

      if ( !strcmp( id, "TouchMove" ) )
      {
        int touchIndex = -1;
        for ( int i = 0; i < 10; i++ )
          if ( touchIndexMap[i] == touchIDint )
          {
            touchIndex = i;
            break;
          }

        if ( touchIndex == -1 )
          continue;

        MessageHandler->OnTouchMoved( cursorPos, pressure, touchIndex, 0 );
        continue;
      }

      if ( !strcmp( id, "TouchRemove" ) )
      {
        int touchIndex = -1;
        for ( int i = 0; i < 10; i++ )
          if ( touchIndexMap[i] == touchIDint )
          {
            touchIndexMap[i] = -1;
            touchIndex = i;
            break;
          }

        if ( touchIndex == -1 )
          continue;

        MessageHandler->OnTouchEnded( cursorPos, touchIndex, 0 );
        continue;
      }

      //UE_LOG(LogVentuzPlugin, Warning, TEXT("OSC event: %s %s %s %s | type: %d id: %s x: %f y: %f z: %f pressure: %f angle: %f"), *FString(addr), *FString(args), *FString(deviceType), *FString(id), intValues[0], *FString(stringValues[0]), floatValues[0], floatValues[1], floatValues[2], floatValues[3], floatValues[4]);
      continue;
    }


  } while ( osc.Scan < osc.End );

}

void FVentuzInputDevice::ResetOriginalApplication()
{
  FSlateApplication::Get().OverridePlatformApplication( appWrapper->WrappedApplication );
}

void FVentuzInputDevice::Tick( float deltaTime )
{
}

void FVentuzInputDevice::SetMessageHandler( const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler )
{
  MessageHandler = InMessageHandler;
}

void FVentuzPlugin::OnPostWorldCreation( UWorld* world )
{
  client.ResetStoredValues();
  eventCallbacks.Reset();
}

FVentuzPlugin::FVentuzPlugin()
{
}

void FVentuzPlugin::Tick( float deltaTime )
{
  ITextureShareAPI& TextureShareAPISingletone = ITextureShare::Get().GetTextureShareAPI();
  auto object = TextureShareAPISingletone.GetObject( GuidToText( ventuzLaunchID ) );

  if ( object )
    currentTextureShareFrameIndex = object->GetCoreData().FrameMarker.CustomFrameIndex1;
}

void FVentuzPlugin::OnBeginFrame()
{
  TRACE_CPUPROFILER_EVENT_SCOPE( TEXT( "FVentuzPlugin::OnBeginFrame" ) );

  FString RHIName = GDynamicRHI->GetName();
  if ( !RHIName.Contains( TEXT( "D3D11" ) ) )
    client.SetErrorMessage( "ERROR: Unreal E2E requires using the DirectX 11 rendering backend, please switch your Unreal project to it.\n" );

  if ( GEngine && GEngine->GameViewport && GEngine->GameViewport->GetWorld() )
  {
    GEngine->LocalPlayerClassName = FSoftClassPath( "/Script/VentuzPlugin.VentuzLocalPlayer" );
    DisableEditorCPUThrottling();

    TWeakPtr<FSceneViewport> SceneViewport;
    FindSceneViewport( SceneViewport );
    if ( SceneViewport.IsValid() )
    {
      FindOrAddDisplayConfiguration( SceneViewport.Pin()->GetViewport() );
    }

    SetGameRunning( GEngine->GameViewport->GetWorld() );
  }
  else
  {
    DisplayExtensions.Reset();
    SetGameRunning( nullptr );
  }

  if ( InputDevice && ( !GEngine || !GEngine->GameViewport ) )
  {
    InputDevice->ResetOriginalApplication();
  }

  if ( !client.IsConnected() )
  {
    auto version = FEngineVersion::Current();
    client.SetUnrealVersion( version.GetMajor(), version.GetMinor(), version.GetPatch() );

  }

  Ventuz::UE4::ConnectionError error = client.UpdateConnection( ventuzLaunchID, isLiveLink );

  if ( error != Ventuz::UE4::ConnectionError::OK )
  {
    textureSharesInSync = false;
    return;
  }

  if ( client.IsConnected() )
  {
    if ( !textureSharesInSync )
    {
      SendTextureShareList();
      textureSharesInSync = true;
    }

    {
      TRACE_CPUPROFILER_EVENT_SCOPE( TEXT( "FVentuzPlugin::TickANC" ) );
      TickAnc();
    }
    ProcessVentuzInputs();
    client.ProcessEventsFromVentuz( [this]( uint eventHash, uint argument )
    {
      TArray<FVentuzEventCallback*> callbacks;
      eventCallbacks.MultiFindPointer( eventHash, callbacks );
      for ( auto callback : callbacks )
      {
        callback->ExecuteIfBound( argument );
      }
    } );

    if ( GEngine && GEngine->GameViewport )
    {
      int width;
      int height;
      if ( client.HasResolutionChanged( width, height ) )
      {
        EWindowMode::Type WindowMode = GEngine->GameViewport->GetGameViewport()->GetWindowMode();
        FSystemResolution::RequestResolutionChange( width, height, WindowMode );

#if WITH_EDITOR
        if ( GIsEditor && GEditor->PlayWorld && !GIsPlayInEditorWorld )
        {
          UWorld* OldWorld = SetPlayInEditorWorld( GEditor->PlayWorld );

          ULocalPlayer* Player = GEngine->GetDebugLocalPlayer();
          if ( Player && Player->ViewportClient && Player->ViewportClient->ViewportFrame )
            Player->ViewportClient->ViewportFrame->ResizeFrame( width + 10, height + 34, WindowMode ); // HACK: added values are based on the default 4.26 skin...

          if ( OldWorld )
            RestoreEditorWorld( OldWorld );
        }
#endif
      }
    }
  }
  else
    textureSharesInSync = false;

  // update aspect ratio even when camera matrices haven't been updated
  if ( GEngine && GEngine->GameViewport && GEngine->GameViewport->GetWorld() )
  {
    auto* playerController = UGameplayStatics::GetPlayerController( GEngine->GameViewport->GetWorld(), 0 );

    if ( playerController )
    {
      playerController->DisableComponentsSimulatePhysics();
      if ( playerController->GetPawn() )
        playerController->GetPawn()->DisableComponentsSimulatePhysics();

      auto* viewTarget = playerController->GetViewTarget();
      if ( playerController->PlayerCameraManager && client.IsConnected() )
      {
        float aspect = GetVentuzCameraAspect();

        playerController->PlayerCameraManager->DefaultAspectRatio = aspect;
        playerController->PlayerCameraManager->bDefaultConstrainAspectRatio = true;

      }
    }
  }

  if ( client.sharedData.matricesUpdated )
  {
    client.sharedData.matricesUpdated = false;

    FTransform transform = GetVentuzCameraTransform();

    if ( GEngine && GEngine->GameViewport && GEngine->GameViewport->GetWorld() )
    {
      auto* playerController = UGameplayStatics::GetPlayerController( GEngine->GameViewport->GetWorld(), 0 );

      if ( playerController )
      {
        auto* viewTarget = playerController->GetViewTarget();
        if ( playerController->PlayerCameraManager )
        {
          float fov = GetVentuzCameraFov();

          playerController->PlayerCameraManager->bIsOrthographic = IsVentuzCameraOrtho();
          playerController->PlayerCameraManager->ViewPitchMin = -179.9999;
          playerController->PlayerCameraManager->ViewPitchMax = 180;
          playerController->PlayerCameraManager->ViewYawMin = 0;
          playerController->PlayerCameraManager->ViewYawMax = 359.9999;
          playerController->PlayerCameraManager->ViewRollMin = -179.9999;
          playerController->PlayerCameraManager->ViewRollMax = 180;
          playerController->PlayerCameraManager->SetFOV( fov );
        }

        TInlineComponentArray<UCameraComponent*> Cameras;
        viewTarget->GetComponents<UCameraComponent>(/*out*/ Cameras );

        for ( UCameraComponent* CameraComponent : Cameras )
        {
          if ( CameraComponent->IsActive() )
          {
            CameraComponent->SetWorldTransform( transform );
          }
        }

        if ( viewTarget->IsA<APawn>() )
        {
          auto* pawn = CastChecked<APawn>( viewTarget );
          if ( pawn->Controller )
            pawn->Controller->ClientSetRotation( transform.Rotator() );
          pawn->BaseEyeHeight = 0;
          pawn->SetActorLocation( transform.GetLocation() );
          viewTarget->SetActorLocation( transform.GetLocation() );
        }
        else
          viewTarget->SetActorTransform( transform );
      }
    }
  }
}

void FVentuzPlugin::OnEndFrame()
{
}

void FVentuzPlugin::OnEnginePreExit()
{
  TRACE_CPUPROFILER_EVENT_SCOPE( TEXT( "FVentuzPlugin::OnBeginPreExit" ) );
  if ( client.IsConnected() )
    client.Disconnect();
}

void FVentuzPlugin::ProcessVentuzInputs()
{
  if ( !client.IsConnected() )
    return;

  auto bundles = client.GetInputBundles();

  if ( !isGameRunning )
    return;

  for ( auto& bundle : bundles )
  {
    if ( InputDevice )
      InputDevice->ParseOSCBundle( bundle );
  }
}

void FVentuzPlugin::SendTextureShareList()
{
  std::vector<GUID> guids;
  for ( auto& guid : textureShares )
    guids.emplace_back( guid.Key );

  if ( client.IsConnected() )
    client.SendTextureShareList( guids );
}

void FVentuzPlugin::SetGameRunning( UWorld* world )
{
  bool isRunning = world ? world->IsGameWorld() : false;

  if ( client.IsConnected() )
    client.SetRunStatus( isRunning );

  if ( isRunning != isGameRunning )
  {
    client.MarkResolutionDirty();

    if ( isRunning )
    {
      defaultQualitySettings = Scalability::GetQualityLevels();
      SetRenderQuality( renderQuality );
      ForceFXAA();

      bool isSimulating = false;

#if WITH_EDITOR
      if ( UEditorEngine* editor = Cast<UEditorEngine>( GEngine ) )
        isSimulating = editor->bIsSimulatingInEditor;
#endif
      if ( !isSimulating )
        CreateVentuzTextureShare( world );
    }
    else
    {
      Scalability::SetQualityLevels( defaultQualitySettings );
      DestroyVentuzTextureShare( world );
    }
  }

  isGameRunning = isRunning;
}

void FVentuzPlugin::SetRenderQuality( Ventuz::UE4::RenderQuality quality )
{
  Scalability::FQualityLevels newQuality;

  switch ( quality )
  {
  case Ventuz::UE4::RenderQuality::Default:
    newQuality = defaultQualitySettings;
    break;
  case Ventuz::UE4::RenderQuality::Low:
    newQuality.SetFromSingleQualityLevel( 0 );
    break;
  case Ventuz::UE4::RenderQuality::Medium:
    newQuality.SetFromSingleQualityLevel( 1 );
    break;
  case Ventuz::UE4::RenderQuality::High:
    newQuality.SetFromSingleQualityLevel( 2 );
    break;
  case Ventuz::UE4::RenderQuality::Epic:
    newQuality.SetFromSingleQualityLevel( 3 );
    break;
  case Ventuz::UE4::RenderQuality::Cinematic:
    newQuality.SetFromSingleQualityLevel( 4 );
    break;
  default:
    newQuality.SetFromSingleQualityLevel( 3 );
    break;
  }

  if ( isGameRunning )
    Scalability::SetQualityLevels( newQuality );

  renderQuality = quality;
}

void FVentuzPlugin::DisableEditorCPUThrottling()
{
#if WITH_EDITOR
  UEditorPerformanceSettings* Settings = GetMutableDefault<UEditorPerformanceSettings>();
  check( Settings );
  Settings->bThrottleCPUWhenNotForeground = false;
  Settings->PostEditChange();
  Settings->SaveConfig();
#endif
}

void FVentuzPlugin::ForceFXAA()
{
  if ( !GEngine )
    return;
  UGameUserSettings* UserSettings = GEngine->GetGameUserSettings();
  UserSettings->SetAntiAliasingQuality( 1 );
  UserSettings->ApplySettings( false );
}

void FVentuzPlugin::CreateVentuzTextureShare( UWorld* world )
{
  FString RHIName = GDynamicRHI->GetName();
  if ( !RHIName.Contains( TEXT( "D3D11" ) ) )
  {
    UE_LOG( LogVentuzPlugin, Error, TEXT( "Ventuz texture share requires using the DirectX 11 rendering backend, please switch your project to it." ) );
    return;
  }

  FString realShareName = GuidToText( ventuzLaunchID );

  //realShareName = FString( "VentuzTextureShare333" );// FString( guidText );

  auto Subsystems = world->GetSubsystemArrayCopy<UTickableWorldSubsystem>();

  UWorldSubsystem* TextureShareWorldSubsystem = nullptr;
  for ( UWorldSubsystem* Subsystem : Subsystems )
  {
    auto className = Subsystem->GetClass()->GetName();
    if ( Subsystem && className == "TextureShareWorldSubsystem" )
    {
      TextureShareWorldSubsystem = Subsystem;
      break;
    }
  }

  if ( !TextureShareWorldSubsystem )
  {
    UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem unavailable!" ) );
    return;
  }

  UTextureShare* TextureShare = nullptr;

  UObject* TextureShareSubsystemAsObject = Cast<UObject>( TextureShareWorldSubsystem );
  if ( TextureShareSubsystemAsObject )
  {
    UFunction* GetTextureShareFunction = TextureShareSubsystemAsObject->FindFunction( TEXT( "GetTextureShare" ) );
    if ( GetTextureShareFunction )
    {
      struct FTextureShareResult
      {
        UObject* Result;
      };

      FTextureShareResult TextureShareResult{};
      TextureShareSubsystemAsObject->ProcessEvent( GetTextureShareFunction, &TextureShareResult );

      UObject* shareRaw = TextureShareResult.Result;
      if ( shareRaw )
      {
        if ( shareRaw->IsA<UTextureShare>() )
        {
          TextureShare = Cast<UTextureShare>( shareRaw );
        }
        else
        {
          UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem::GetTextureShare call return a non UTextureShare object!" ) );
          return;
        }
      }
      else
      {
        UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem::GetTextureShare call failed!" ) );
        return;
      }
    }
    else
    {
      UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem doesn't have a GetTextureShare function!" ) );
      return;
    }
  }

  UTextureShareObject* TextureShareObject = nullptr;

  if ( TextureShare )
  {
    TextureShare->RemoveTextureShareObject( TEXT( "DefaultShareName" ) );
    TextureShareObject = TextureShare->GetOrCreateTextureShareObject( realShareName );
    if ( TextureShareObject )
    {
      TextureShareObject->bEnable = true;
    }
    else
    {
      UE_LOG( LogVentuzPlugin, Error, TEXT( "Failed to create Texture Share object by the name of '%s'" ), *realShareName );
      return;
    }
  }
  else
  {
    UE_LOG( LogVentuzPlugin, Error, TEXT( "TextureShare unavailable" ) );
    return;
  }

#if ENGINE_MAJOR_VERSION >= 5 
  int stereoscopicPass = INDEX_NONE;
#else
  int stereoscopicPass = 0;
#endif

  RegisterTextureShare( ventuzLaunchID, TextureShareObject );
  UE_LOG( LogVentuzPlugin, Log, TEXT( "Texture share by the name of '%s' created" ), *realShareName );
}

void FVentuzPlugin::DestroyVentuzTextureShare( UWorld* world )
{
  if ( !world )
  {
    UnRegisterTextureShare( ventuzLaunchID );
    return;
  }

  FString realShareName = GuidToText( ventuzLaunchID );

  //realShareName = FString( "VentuzTextureShare333" );// FString( guidText );

  auto Subsystems = world->GetSubsystemArrayCopy<UTickableWorldSubsystem>();
  UWorldSubsystem* TextureShareWorldSubsystem = nullptr;
  for ( UWorldSubsystem* Subsystem : Subsystems )
  {
    auto className = Subsystem->GetClass()->GetName();
    if ( Subsystem && className == "TextureShareWorldSubsystem" )
    {
      TextureShareWorldSubsystem = Subsystem;
      break;
    }
  }

  if ( !TextureShareWorldSubsystem )
  {
    UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem unavailable!" ) );
    return;
  }

  UTextureShare* TextureShare = nullptr;

  UObject* TextureShareSubsystemAsObject = Cast<UObject>( TextureShareWorldSubsystem );
  if ( TextureShareSubsystemAsObject )
  {
    UFunction* GetTextureShareFunction = TextureShareSubsystemAsObject->FindFunction( TEXT( "GetTextureShare" ) );
    if ( GetTextureShareFunction )
    {
      struct FTextureShareResult
      {
        UObject* Result;
      };

      FTextureShareResult TextureShareResult{};
      TextureShareSubsystemAsObject->ProcessEvent( GetTextureShareFunction, &TextureShareResult );

      UObject* shareRaw = TextureShareResult.Result;
      if ( shareRaw )
      {
        if ( shareRaw->IsA<UTextureShare>() )
        {
          TextureShare = Cast<UTextureShare>( shareRaw );
        }
        else
        {
          UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem::GetTextureShare call return a non UTextureShare object!" ) );
          return;
        }
      }
      else
      {
        UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem::GetTextureShare call failed!" ) );
        return;
      }
    }
    else
    {
      UE_LOG( LogVentuzPlugin, Error, TEXT( "Unreal TextureShareWorldSubsystem doesn't have a GetTextureShare function!" ) );
      return;
    }
  }

  UTextureShareObject* TextureShareObject = nullptr;

  if ( TextureShare )
  {
    TextureShare->RemoveTextureShareObject( realShareName );
  }
  else
  {
    UE_LOG( LogVentuzPlugin, Error, TEXT( "TextureShare unavailable" ) );
    return;
  }

  UnRegisterTextureShare( ventuzLaunchID );
}

bool FVentuzPlugin::IsTickableWhenPaused() const
{
  return true;
}

bool FVentuzPlugin::IsTickableInEditor() const
{
  return true;
}

TStatId FVentuzPlugin::GetStatId() const
{
  RETURN_QUICK_DECLARE_CYCLE_STAT( FVentuzPlugin, STATGROUP_Tickables );
}

void FVentuzPlugin::StartupModule()
{
  GConfig->SetBool( TEXT( "/Script/TextureShare.TextureShareSettings" ), TEXT( "bCreateDefaults" ), false, GEngineIni );
  GConfig->Flush( false, GEngineIni );

  IModularFeatures::Get().RegisterModularFeature( GetModularFeatureName(), this );
  FWorldDelegates::OnPostWorldCreation.AddRaw( this, &FVentuzPlugin::OnPostWorldCreation );

  FString commandLine = FString( GetCommandLineA() );
  TArray<FString> tokens;
  commandLine.ParseIntoArrayWS( tokens );

  for ( int x = 0; x < tokens.Num(); x++ )
  {
    if ( tokens[x].Find( TEXT( "-VentuzLaunchID=" ) ) == 0 )
    {
      FGuid cmdLineGuid = FGuid( tokens[x].Mid( 16 ) ); // 16 = length of "-VentuzLaunchID="
      ventuzLaunchID.Data1 = cmdLineGuid.A;
      ventuzLaunchID.Data2 = ( cmdLineGuid.B >> 16 ) & 0xffff;
      ventuzLaunchID.Data3 = cmdLineGuid.B & 0xffff;
      ventuzLaunchID.Data4[3] = ( cmdLineGuid.C ) & 0xff;
      ventuzLaunchID.Data4[2] = ( cmdLineGuid.C >> 8 ) & 0xff;
      ventuzLaunchID.Data4[1] = ( cmdLineGuid.C >> 16 ) & 0xff;
      ventuzLaunchID.Data4[0] = ( cmdLineGuid.C >> 24 ) & 0xff;
      ventuzLaunchID.Data4[7] = ( cmdLineGuid.D ) & 0xff;
      ventuzLaunchID.Data4[6] = ( cmdLineGuid.D >> 8 ) & 0xff;
      ventuzLaunchID.Data4[5] = ( cmdLineGuid.D >> 16 ) & 0xff;
      ventuzLaunchID.Data4[4] = ( cmdLineGuid.D >> 24 ) & 0xff;

      isLiveLink = false;
    }
    else
    {
      CoCreateGuid( &ventuzLaunchID );
      isLiveLink = true;
    }
  }

  FCoreDelegates::OnBeginFrame.AddRaw( this, &FVentuzPlugin::OnBeginFrame );
  FCoreDelegates::OnEndFrame.AddRaw( this, &FVentuzPlugin::OnEndFrame );
  FCoreDelegates::OnEnginePreExit.AddRaw( this, &FVentuzPlugin::OnEnginePreExit );

  FCoreDelegates::OnPostEngineInit.AddLambda( [this]()
  {
    DisableEditorCPUThrottling();
    ForceFXAA();
  } );
}

void FVentuzPlugin::ShutdownModule()
{
  if ( client.IsConnected() )
    client.Disconnect();

  IModularFeatures::Get().UnregisterModularFeature( IInputDeviceModule::GetModularFeatureName(), this );

  FWorldDelegates::OnPostWorldCreation.RemoveAll( this );

  FCoreDelegates::OnBeginFrame.RemoveAll( this );
  FCoreDelegates::OnEnginePreExit.RemoveAll( this );
}

TSharedPtr<class IInputDevice> FVentuzPlugin::CreateInputDevice( const TSharedRef<FGenericApplicationMessageHandler>& InMessageHandler )
{
  InputDevice = MakeShareable( new FVentuzInputDevice( InMessageHandler ) );
  return InputDevice;
}

void FVentuzPlugin::RegisterEvent( uint eventNameHash, const FVentuzEventCallback& event )
{
  eventCallbacks.Add( eventNameHash, event );
}

void FVentuzPlugin::TriggerEvent( uint eventNameHash, int argument )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::Event, eventNameHash );

  struct EventData
  {
    int triggerID;
    int argument;
  } outData;

  static int eventTriggerID = 0;

  outData.triggerID = eventTriggerID++;
  outData.argument = argument;

  auto data = BinToHexWithFrameID( &outData, sizeof( outData ), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::TickAnc()
{
  if ( client.UpdateConnection( ventuzLaunchID, isLiveLink ) != Ventuz::UE4::ConnectionError::OK )
    return;

  if ( !client.IsConnected() )
    return;

  client.ParseANC( []( void* targetObject, const std::vector<Ventuz::UE4::FieldDescriptor>& fieldList )
  {
    ( (IVentuzNodeFieldSync*)targetObject )->UpdateNodeFields( fieldList );
  }, fromVentuzFieldListRequests,
                   [this]( Ventuz::UE4::RenderQuality quality )
  {
    SetRenderQuality( quality );
  },
    [this]( const Ventuz::UE4::DataKeyValuePair& kvp)
  {
    // incoming hash changed callback
    auto syncHash = ExpandChecksumCRC32( kvp.key.hash, (const uint8*)"_synced", 7 );

    auto kvpSynced = kvp;
    if ( kvp.key.hash )
      kvpSynced.key.hash = syncHash;

    auto key = BinToHex( &kvpSynced.key, sizeof( Ventuz::UE4::DataKey ) );
    auto data = BinToHexWithFrameID( kvpSynced.data, kvpSynced.length, currentTextureShareFrameIndex );

    for ( auto& share : textureShares )
      share.Value->CustomData.SendParameters.Add( key, data );
  });
}

FTransform FVentuzPlugin::GetVentuzCameraTransform()
{
  FMatrix view;
  for ( int x = 0; x < 16; x++ )
    view.M[x / 4][x % 4] = client.sharedData.cameraMatrix[x];

  // camera's world matrix
  FMatrix camWorld = view.Inverse();

  // this matrix transforms the Ventuz camera space to UE camera space
  FMatrix adjustMatrix;
  adjustMatrix.SetIdentity();
  adjustMatrix.M[1][1] = 0;
  adjustMatrix.M[2][2] = 0;
  adjustMatrix.M[1][2] = 1;
  adjustMatrix.M[2][1] = -1;

  FMatrix vtzToUE = adjustMatrix * FScaleMatrix( 100.f );
  camWorld = camWorld * vtzToUE;

  FVector xAxis_Right;
  FVector yAxis_Up;
  FVector zAxis_forward;
  camWorld.GetUnitAxes( xAxis_Right, yAxis_Up, zAxis_forward );
  auto eye = camWorld.GetOrigin();

  FMatrix rotation( zAxis_forward, xAxis_Right, yAxis_Up, FVector::ZeroVector );
  return FTransform( rotation.Rotator(), eye, FVector::OneVector );
}

float FVentuzPlugin::GetVentuzCameraFov()
{
  return atan( 1.0f / client.sharedData.projectionMatrix[0] ) * 2.0 * 180.0 / PI;
}

float FVentuzPlugin::GetVentuzCameraAspect()
{
  return client.GetCameraAspect();
}

FMatrix FVentuzPlugin::GetVentuzProjectionMatrix()
{
  FMatrix result;
  for ( int x = 0; x < 4; x++ )
    for ( int y = 0; y < 4; y++ )
      result.M[x][y] = client.sharedData.projectionMatrix[x * 4 + y];
  return result;
}

bool FVentuzPlugin::IsVentuzCameraOrtho()
{
  return client.sharedData.projectionMatrix[15] == 1;
}

FVector2D FVentuzPlugin::GetVentuzProjectionOffset()
{
  return FVector2D( client.sharedData.projectionMatrix[8], client.sharedData.projectionMatrix[9] );
}

int FVentuzPlugin::GetIntValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.intValues.find( hash ) != client.sharedData.intValues.end();
  if ( !isAvailable )
    return 0;

  return client.sharedData.intValues[hash];
}

float FVentuzPlugin::GetFloatValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.floatValues.find( hash ) != client.sharedData.floatValues.end();
  if ( !isAvailable )
    return 0;

  return client.sharedData.floatValues[hash];
}

FString FVentuzPlugin::GetStringValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.stringValues.find( hash ) != client.sharedData.stringValues.end();
  if ( !isAvailable )
    return FString();

  std::string& string = client.sharedData.stringValues[hash];
  return FString( UTF8_TO_TCHAR( string.data() ) );
}

bool FVentuzPlugin::GetBoolValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.boolValues.find( hash ) != client.sharedData.boolValues.end();
  if ( !isAvailable )
    return 0;

  return client.sharedData.boolValues[hash];
}

TArray<int> FVentuzPlugin::GetIntArray( uint hash, bool& isAvailable )
{
  TArray<int> result;

  auto findResult = client.sharedData.intArrays.find( hash );
  isAvailable = findResult != client.sharedData.intArrays.end();

  if ( !isAvailable )
    return result;

  for ( int x = 0; x < findResult->second.size(); x++ )
    result.Add( findResult->second[x] );

  return result;
}

TArray<float> FVentuzPlugin::GetFloatArray( uint hash, bool& isAvailable )
{
  TArray<float> result;

  auto findResult = client.sharedData.floatArrays.find( hash );
  isAvailable = findResult != client.sharedData.floatArrays.end();

  if ( !isAvailable )
    return result;

  for ( int x = 0; x < findResult->second.size(); x++ )
    result.Add( findResult->second[x] );

  return result;
}

TArray<FString> FVentuzPlugin::GetStringArray( uint hash, bool& isAvailable )
{
  TArray<FString> result;

  auto findResult = client.sharedData.stringArrays.find( hash );
  isAvailable = findResult != client.sharedData.stringArrays.end();

  if ( !isAvailable )
    return result;

  int length = findResult->second.length();
  auto* strings = findResult->second.c_str();
  auto* end = strings + length;
  while ( strings < end )
  {
    result.Add( UTF16_TO_TCHAR( strings ) );
    while ( *strings )
      strings++;
    strings++;
  }

  return result;
}

TArray<bool> FVentuzPlugin::GetBoolArray( uint hash, bool& isAvailable )
{
  TArray<bool> result;

  auto findResult = client.sharedData.boolArrays.find( hash );
  isAvailable = findResult != client.sharedData.boolArrays.end();

  if ( !isAvailable )
    return result;

  for ( int x = 0; x < findResult->second.size(); x++ )
    result.Add( findResult->second[x] != 0 );

  return result;
}

FColor FVentuzPlugin::GetColorValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.colorValues.find( hash ) != client.sharedData.colorValues.end();
  if ( !isAvailable )
    return FColor();

  auto color = client.sharedData.colorValues[hash];

  return FColor( color );
}

FTransform FVentuzPlugin::GetTransformValue( uint hash, bool& isAvailable )
{
  isAvailable = client.sharedData.transformValues.find( hash ) != client.sharedData.transformValues.end();
  if ( !isAvailable )
    return FTransform();

  FMatrix matrix;
  auto& mx = client.sharedData.transformValues[hash];

  for ( int x = 0; x < 16; x++ )
    matrix.M[x / 4][x % 4] = mx[x];

  return FTransform( matrix );
}

uint32 GetTypeHash( const GUID& k )
{
  RPC_STATUS status;
  return UuidHash( (UUID*)&k, &status );
}

void FVentuzPlugin::RegisterTextureShare( const GUID& textureShareName, UTextureShareObject* textureShareObject )
{
  bool bIsAlreadyInSetPtr = textureShares.Find( textureShareName ) != nullptr;

  textureShares.Add( textureShareName, textureShareObject );

  if ( !bIsAlreadyInSetPtr )
    SendTextureShareList();
}

void FVentuzPlugin::UnRegisterTextureShare( const GUID& textureShareName )
{
  if ( textureShares.Remove( textureShareName ) > 0 )
    SendTextureShareList();
}

void FVentuzPlugin::SendFloatValue( uint hash, const float& value )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::FloatValue, hash );
  auto data = BinToHexWithFrameID( &value, sizeof( value ), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendIntValue( uint hash, const int& value )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::IntValue, hash );
  auto data = BinToHexWithFrameID( &value, sizeof( value ), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendStringValue( uint hash, const FString& value )
{
  auto utf8String = std::string( TCHAR_TO_UTF8( *value ) );

  auto key = KeyToHex( Ventuz::UE4::AncID::StringValue, hash );
  auto data = BinToHexWithFrameID( utf8String.data(), utf8String.length(), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendBoolValue( uint hash, const bool value )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::BoolValue, hash );
  auto data = BinToHexWithFrameID( &value, sizeof( value ), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendIntArray( uint hash, const TArray<int>& array )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::IntArray, hash );
  auto data = BinToHexWithFrameID( array.GetData(), array.Num() * sizeof( int ), currentTextureShareFrameIndex );
  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendFloatArray( uint hash, const TArray<float>& array )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::FloatArray, hash );
  auto data = BinToHexWithFrameID( array.GetData(), array.Num() * sizeof( float ), currentTextureShareFrameIndex );
  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendStringArray( uint hash, const TArray<FString>& array )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::StringArray, hash );

  std::vector<std::string> stringArray;
  for ( int x = 0; x < array.Num(); x++ )
    stringArray.emplace_back( std::string( TCHAR_TO_UTF8( *array[x] ) ) );

  int compositeSize = 0;
  for ( int x = 0; x < stringArray.size(); x++ )
    compositeSize += (int)( stringArray[x].length() + 1 );

  unsigned char* composite = new unsigned char[compositeSize];
  unsigned char* copyTarget = composite;

  for ( int x = 0; x < stringArray.size(); x++ )
  {
    size_t length = stringArray[x].length();
    memcpy( copyTarget, stringArray[x].c_str(), length );
    copyTarget[length] = 0;
    copyTarget += length + 1;
  }

  auto data = BinToHexWithFrameID( composite, compositeSize, currentTextureShareFrameIndex );

  delete[] composite;

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendBoolArray( uint hash, const TArray<bool>& array )
{
  auto key = KeyToHex( Ventuz::UE4::AncID::BoolArray, hash );
  auto data = BinToHexWithFrameID( array.GetData(), array.Num() * sizeof( bool ), currentTextureShareFrameIndex );
  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendTransformValue( uint hash, const FTransform& value )
{
  auto matrix = value.ToMatrixWithScale();

  auto key = KeyToHex( Ventuz::UE4::AncID::TransformValue, hash );
  auto data = BinToHexWithFrameID( matrix.M, sizeof( matrix.M ), currentTextureShareFrameIndex );

  for ( auto& share : textureShares )
    share.Value->CustomData.SendParameters.Add( key, data );
}

void FVentuzPlugin::SendFieldList( const GUID& requestID, const std::vector<Ventuz::UE4::FieldDescriptor>& fieldsToSend )
{
  if ( client.IsConnected() )
    client.SendFieldList( requestID, fieldsToSend );
}

void FVentuzPlugin::RequestFieldListUpdate( const FString& nodeName, IVentuzNodeFieldSync* node )
{
  auto requestedName = std::string( TCHAR_TO_UTF8( *nodeName.ToLower() ) );
  if ( client.IsConnected() )
    client.RequestFieldListUpdate( requestedName, node );
}

std::unordered_map<GUID, std::string> FVentuzPlugin::GetFieldListRequests()
{
  auto copy = fromVentuzFieldListRequests;
  fromVentuzFieldListRequests.clear();
  return copy;
}

bool FVentuzPlugin::IsConnected()
{
  return client.IsConnected();
}

Ventuz::UE4::FromVentuzClock FVentuzPlugin::GetClusterClock()
{
  return client.GetClusterClock();
}

GUID FVentuzPlugin::GetVentuzLaunchID()
{
  return ventuzLaunchID;
}

void FVentuzPlugin::SetViewports( int x1, int y1, int x2, int y2, int zWidth, int zHeight )
{
  if ( client.IsConnected() )
    client.SetViewports( x1, y1, x2, y2, zWidth, zHeight );
}

void FVentuzPlugin::SetAlphaPropagation( int propagation )
{
  if ( client.IsConnected() )
    client.SetAlphaType( propagation == 2 ? 2 : 0 );
}

UTextureShareObject* FVentuzPlugin::GetRegisteredTextureShare( const GUID& guid )
{
  return (UTextureShareObject*)textureShares.Find( guid );
}

TSharedPtr<FVentuzViewExtension, ESPMode::ThreadSafe> FVentuzPlugin::FindOrAddDisplayConfiguration( FViewport* InViewport )
{
  const TSharedPtr<FVentuzViewExtension, ESPMode::ThreadSafe>* Extension = DisplayExtensions.FindByPredicate( [InViewport]( const TSharedPtr<FVentuzViewExtension, ESPMode::ThreadSafe>& Other ) { return  Other.IsValid() && InViewport == Other->GetAssociatedViewport(); } );
  if ( Extension )
  {
    return *Extension;
  }

  // Extension not found, create it and return its config
  TSharedPtr<FVentuzViewExtension, ESPMode::ThreadSafe> DisplayExtension = FSceneViewExtensions::NewExtension<FVentuzViewExtension>( InViewport );
  DisplayExtensions.Add( DisplayExtension );
  return DisplayExtension;
}

FVentuzViewExtension::FVentuzViewExtension( const FAutoRegister& AutoRegister, FViewport* AssociatedViewport )
  : LinkedViewport( AssociatedViewport )
{
}

void FVentuzViewExtension::BeginRenderViewFamily( FSceneViewFamily& ViewFamily )
{
  TRACE_CPUPROFILER_EVENT_SCOPE( TEXT( "FVentuzPlugin::BeginRenderViewFamily" ) );

  if ( !ViewFamily.Views.Num() )
    return;

  if ( !IVentuzPlugin::IsAvailable() )
    return;

  auto& plugin = FModuleManager::LoadModuleChecked<FVentuzPlugin>( "VentuzPlugin" );

  const auto& view = ViewFamily.Views[0];
  //static const auto AlphaPropagation = IConsoleManager::Get().FindTConsoleVariableDataInt( TEXT( "r.PostProcessing.PropagateAlpha" ) );

  plugin.SetViewports( 0, 0, view->UnconstrainedViewRect.Width(), view->UnconstrainedViewRect.Height(), view->UnscaledViewRect.Width(), view->UnscaledViewRect.Height() );
  //plugin.SetAlphaPropagation( AlphaPropagation->GetValueOnAnyThread() );
}

bool FVentuzViewExtension::IsActiveThisFrame( const FSceneViewExtensionContext& Context ) const
{
  return ( LinkedViewport == Context.Viewport );
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE( FVentuzPlugin, VentuzPlugin )
