//---------------------------------------------------------------------------------------
// Gets memory representing binary for class hierarchy and associated info.
// 
// #See Also:   load_compiled_scripts(), SkCompiler::get_binary_class_group()
// #Modifiers:  virtual - overridden from SkookumRuntimeBase
// #Author(s):  Conan Reis
SkBinaryHandle * SkUERuntime::get_binary_symbol_table()
  {
  FString sym_file = get_compiled_path() / TEXT("classes.sk-sym");

  A_DPRINT("  Loading compiled binary symbol file '%ls'...\n", *sym_file);

  SkBinaryHandleUE * handle_p = SkBinaryHandleUE::create(*sym_file);

  // Ensure symbol table binary exists
  if (!handle_p)
    {
    A_DPRINT("  ...it does not exist!\n\n", *sym_file);
    }

  return handle_p;
  }                                                                  
//---------------------------------------------------------------------------------------
// Gets memory representing binary for class hierarchy and associated info.
// 
// #See Also:   load_compiled_scripts(), SkCompiler::get_binary_class_group()
// #Modifiers:  virtual - overridden from SkookumRuntimeBase
// #Author(s):  Conan Reis
SkBinaryHandle * SkUERuntime::get_binary_hierarchy()
  {
  FString compiled_file = get_compiled_path() / TEXT("classes.sk-bin");

  A_DPRINT("  Loading compiled binary file '%ls'...\n", *compiled_file);

  return SkBinaryHandleUE::create(*compiled_file);
  }
//---------------------------------------------------------------------------------------
//  Setups the auto-parse temporary symbol table. Symbol creation calls will put shared copies of
//  new symbols into the auto-parse symbol table. The auto-parse symbol table with then be used to
//  remove these temporary symbols once the auto-parse terminates.
//  
//  Author(s)  John Stenersen
void ASymbolTable::track_auto_parse_init()
  {
  if (ms_auto_parse_syms_p)
    {
    A_DPRINT(A_SOURCE_STR "ms_auto_parse_syms_p is not null. Forgotten call to track_auto_parse_term()?\n");
    track_auto_parse_term();
    }

  ms_auto_parse_syms_p = this;
  }
//---------------------------------------------------------------------------------------
// Override to add bindings to any custom C++ routines (methods & coroutines).
//
// #See Also   SkBrain::register_bind_atomics_func()
// #Modifiers  virtual
// #Author(s)  Conan Reis
void SkUERuntime::on_bind_routines()
  {
  A_DPRINT(A_SOURCE_STR "\nBind routines for SkUERuntime.\n");

  #if WITH_EDITORONLY_DATA
    SkUEClassBindingHelper::reset_dynamic_class_mappings(); // Start over fresh
  #endif

  SkUEBindings::register_all_bindings();
  m_blueprint_interface.reexpose_all(); // Hook up Blueprint functions and events for static classes
  }
//---------------------------------------------------------------------------------------
void FSkookumScriptRuntime::on_world_cleanup(UWorld * world_p, bool session_ended_b, bool cleanup_resources_b)
  {
  //A_DPRINT("on_world_cleanup: %S %p\n", *world_p->GetName(), world_p);

  if (world_p->IsGameWorld())
    {
    // Keep track of how many game worlds we got
    --m_num_game_worlds;

    // Set world pointer to null if it was pointing to us
    if (m_game_world_p == world_p)
      {
      m_game_world_p->OnTickDispatch().Remove(m_game_tick_handle);
      m_game_world_p = nullptr;
      SkUEClassBindingHelper::set_world(nullptr);
      }

    // Restart SkookumScript if initialized
    if (m_num_game_worlds == 0 && is_skookum_initialized())
      {
      // Simple shutdown
      //SkookumScript::get_world()->clear_coroutines();
      A_DPRINT(
        "SkookumScript resetting session...\n"
        "  cleaning up...\n");
      SkookumScript::deinitialize_gameplay();
      SkookumScript::deinitialize_sim();
      SkookumScript::initialize_sim();
      A_DPRINT("  ...done!\n\n");
      }
    }
  else if (world_p->WorldType == EWorldType::Editor)
    {
    // Set world pointer to null if it was pointing to us
    if (m_editor_world_p == world_p)
      {
      m_editor_world_p->OnTickDispatch().Remove(m_editor_tick_handle);
      m_editor_world_p = nullptr;
      }
    }
  }
//---------------------------------------------------------------------------------------
// This code will execute after your module is loaded into memory (but after global
// variables are initialized, of course.)
void FSkookumScriptRuntime::StartupModule()
  {
  #if WITH_EDITORONLY_DATA
    // In editor builds, don't activate SkookumScript if there's no project (project wizard mode)
    if (!FApp::GetGameName() || !FApp::GetGameName()[0] || FPlatformString::Strcmp(FApp::GetGameName(), TEXT("None")) == 0)
      {
      m_is_skookum_disabled = true;
      return;
      }
  #else
    // In cooked builds, stay inert when there's no compiled binaries
    if (!m_runtime.is_binary_hierarchy_existing())
      {
      m_is_skookum_disabled = true;
      return;
      }
  #endif

  A_DPRINT("Starting up SkookumScript plug-in modules\n");

  // Note that FWorldDelegates::OnPostWorldCreation has world_p->WorldType set to None
  // Note that FWorldDelegates::OnPreWorldFinishDestroy has world_p->GetName() set to "None"

  m_on_world_init_pre_handle  = FWorldDelegates::OnPreWorldInitialization.AddRaw(this, &FSkookumScriptRuntime::on_world_init_pre);
  m_on_world_init_post_handle = FWorldDelegates::OnPostWorldInitialization.AddRaw(this, &FSkookumScriptRuntime::on_world_init_post);
  m_on_world_cleanup_handle   = FWorldDelegates::OnWorldCleanup.AddRaw(this, &FSkookumScriptRuntime::on_world_cleanup);

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Start up SkookumScript
  // Originally, the compiled binaries were loaded with a delay when in UE4Editor to provide the user with a smoother startup sequence
  // However this caused issues with the proper initialization of Skookum Blueprint nodes
  // So to avoid glitches, SkookumScript is always initialized right away right here
  //#if WITH_EDITORONLY_DATA
  //  if (!GIsEditor)
  //#endif
      {
      // Initialize right away
      ensure_runtime_initialized();
      }

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Send off connect request to IDE
  // Come back later to check on it
  #ifdef SKOOKUM_REMOTE_UNREAL
    if (!IsRunningCommandlet())
      {
      m_remote_client.set_mode(SkLocale_runtime);
      }
  #endif
  }
//---------------------------------------------------------------------------------------
// This function may be called during shutdown to clean up your module.  For modules that
// support dynamic reloading, we call this function before unloading the module.
void FSkookumScriptRuntime::ShutdownModule()
  {
  A_DPRINT(A_SOURCE_STR " Shutting down SkookumScript plug-in modules\n");

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Clean up SkookumScript
  m_runtime.shutdown();

  #ifdef SKOOKUM_REMOTE_UNREAL
    // Remote communication to and from SkookumScript IDE
    m_remote_client.disconnect();
  #endif

  // Clear out our registered delegates
  FWorldDelegates::OnPreWorldInitialization.Remove(m_on_world_init_pre_handle);
  FWorldDelegates::OnPostWorldInitialization.Remove(m_on_world_init_post_handle);
  FWorldDelegates::OnWorldCleanup.Remove(m_on_world_cleanup_handle);
  }
//---------------------------------------------------------------------------------------
// One-time initialization of SkookumScript
// See Also    shutdown()
// Author(s)   Conan Reis
void SkUERuntime::startup()
  {
  SK_ASSERTX(!m_is_initialized, "Tried to initialize SkUERuntime twice in a row.");

  A_DPRINT("\nSkookumScript starting up.\n");

  // Let scripting system know that the game engine is present and is being hooked-in
  SkDebug::enable_engine_present();

  #ifdef SKOOKUM_REMOTE_UNREAL
    SkDebug::register_print_with_agog();
  #endif

  SkBrain::register_bind_atomics_func(SkookumRuntimeBase::bind_routines);
  SkClass::register_raw_resolve_func(SkUEClassBindingHelper::resolve_raw_data);

  m_is_initialized = true;
  }
//---------------------------------------------------------------------------------------
//  Setups the auto-parse temporary symbol table. Symbol creation calls will put shared copies of
//  new symbols into the auto-parse symbol table. The auto-parse symbol table with then be used to
//  remove these temporary symbols once the auto-parse terminates.
//  
//  Author(s)  John Stenersen
void ASymbolTable::track_auto_parse_term()
  {
  if (!ms_auto_parse_syms_p)
    {
    A_DPRINT(A_SOURCE_STR "ms_auto_parse_syms_p is null (terminated) already.?\n");
    return;
    }

  //  Remove any symbols found in the auto-parse symbol table from the main symbol table.
  uint32_t length = ms_auto_parse_syms_p->get_length();
  for (uint32_t i = 0; i < length; i++)
    {
    ASymbolRef * sym_ref = ms_auto_parse_syms_p->m_sym_refs.get_at(i);
    ms_main_p->m_sym_refs.remove(sym_ref->m_uid, AMatch_first_found);

    //A_DPRINT(A_SOURCE_STR "Removing symbol = %ld\n", sym_ref->m_uid);
    }

  ms_auto_parse_syms_p = nullptr;
  }
//---------------------------------------------------------------------------------------
// Load the Skookum class hierarchy scripts in compiled binary form.
// 
// #Params
//   ensure_atomics:
//     If set makes sure all atomic (C++) scripts that were expecting a C++ function to be
//     bound/hooked up to them do actually have a binding.
//   ignore_classes_pp:
//     array of classes to ignore when ensure_atomics is set.
//     This allows some classes with optional or delayed bindings to be skipped such as
//     bindings to a in-game world editor.
//   ignore_count:  number of class pointers in ignore_classes_pp
//
// #Returns
//   true if compiled scrips successfully loaded, false if not
// 
// #See:        load_compiled_class_group(), SkCompiler::compiled_load()
// #Modifiers:  static
// #Author(s):  Conan Reis
bool SkUERuntime::load_compiled_scripts(
  bool       ensure_atomics,     // = true
  SkClass ** ignore_classes_pp,  // = nullptr
  uint32_t   ignore_count        // = 0u
  )
  {
  A_DPRINT("\nSkookumScript loading previously parsed compiled binary...\n");

  if (load_compiled_hierarchy() != SkLoadStatus_ok)
    {
    return false;
    }

  A_DPRINT("  ...done!\n\n");


  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Bind atomics
  A_DPRINT("SkookumScript binding with C++ routines...\n");

  // Registers/connects Generic SkookumScript atomic classes, stimuli, coroutines, etc.
  // with the compiled binary that was just loaded.
  SkookumScript::initialize_post_load();

  #if (SKOOKUM & SK_DEBUG)
    // Ensure atomic (C++) methods/coroutines are properly bound to their C++ equivalents
    if (ensure_atomics)
      {
      SkBrain::ensure_atomics_registered(ignore_classes_pp, ignore_count);
      }
  #endif

  A_DPRINT("  ...done!\n\n");


  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Enable SkookumScript evaluation
  SkookumScript::enable_flag(SkookumScript::Flag_evaluate);

  A_DPRINT("SkookumScript initializing session...\n");
  SkookumScript::initialize_session();
  A_DPRINT("  ...done!\n\n");

  return true;
  }
//---------------------------------------------------------------------------------------
// This code will execute after your module is loaded into memory (but after global
// variables are initialized, of course.)
void FSkookumScriptRuntime::StartupModule()
  {
  A_DPRINT(A_SOURCE_STR " Starting up SkookumScript plug-in modules\n");

  // Note that FWorldDelegates::OnPostWorldCreation has world_p->WorldType set to None
  // Note that FWorldDelegates::OnPreWorldFinishDestroy has world_p->GetName() set to "None"

  m_on_world_init_pre_handle  = FWorldDelegates::OnPreWorldInitialization.AddRaw(this, &FSkookumScriptRuntime::on_world_init_pre);
  m_on_world_init_post_handle = FWorldDelegates::OnPostWorldInitialization.AddRaw(this, &FSkookumScriptRuntime::on_world_init_post);
  m_on_world_cleanup_handle   = FWorldDelegates::OnWorldCleanup.AddRaw(this, &FSkookumScriptRuntime::on_world_cleanup);

  // Hook up Unreal memory allocator
  AMemory::override_functions(&Agog::malloc_func, &Agog::free_func, &Agog::req_byte_size_func);

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Start up SkookumScript
  #if !WITH_EDITOR
    // If no editor, initialize right away
    // otherwise wait until map is loaded
    m_runtime.startup();

    #ifndef SKOOKUM_REMOTE_UNREAL
      bool success_b = m_runtime.load_compiled_scripts();
      SK_ASSERTX(success_b, AErrMsg("Unable to load SkookumScript compiled binaries!", AErrLevel_notify));
    #endif
  #endif

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Send off connect request to IDE
  // Come back later to check on it
  #ifdef SKOOKUM_REMOTE_UNREAL
    if (!IsRunningCommandlet())
      {
      m_remote_client.set_mode(SkLocale_runtime);
      }
  #endif
  }
//---------------------------------------------------------------------------------------
// Converts any occurrences of symbol ids in the form |#12345678#| to their string
// equivalents.
// 
// Returns:  
//   `true` if completely converted and `false` if there were some ids that the string
//   equivalent could not be found for.
//   
// Params:  
//   str_p: pointer to string to convert
//   
// Author(s):   Conan Reis
bool ASymbolTable::translate_ids(AString * str_p) const
  {
  uint32_t length = str_p->get_length();

  const uint32_t ASymbol_id_str_length = 12u;

  if (length < ASymbol_id_str_length)
    {
    return true;
    }

  bool     sym_replaced;
  bool     translated = true;
  uint32_t sym_id;
  uint32_t str_length;
  uint32_t find_idx;
  uint32_t end_idx;
  uint32_t start_idx = 0u;
  uint32_t max_idx   = length - ASymbol_id_str_length;

  AString sym_str;

  const char * cstr_p = str_p->as_cstr();

  // If found special |#12345678#| pattern
  while ((start_idx <= max_idx)
    && str_p->find('|', 1u, &find_idx, start_idx)
    && (find_idx <= max_idx)
    && (cstr_p[find_idx + 1u] == '#')
    && (cstr_p[find_idx + ASymbol_id_str_length - 2u] == '#')
    && (cstr_p[find_idx + ASymbol_id_str_length - 1u] == '|'))
    {
    sym_replaced = false;

    // $Revisit - CReis Ensure that ids with leading zeros 00123 are converted properly
    sym_id = str_p->as_uint(find_idx + 1u, &end_idx, 16u);

    if (end_idx == find_idx + ASymbol_id_str_length - 1u)
      {
      if (sym_id != ASymbol_id_null)
        {
        if (translate_known_id(sym_id, &sym_str))
          {
          // Replace symbol id with corresponding string
          str_p->replace(sym_str, find_idx, ASymbol_id_str_length);
          str_length   = sym_str.get_length();
          max_idx      = max_idx + str_length - ASymbol_id_str_length;
          start_idx    = find_idx + str_length;
          sym_replaced = true;
          }
        else
          {
          // Unable to translate all of supplied string
          translated = false;
          }
        }
      else
        {
        // It is the empty symbol "" - remove symbol id
        str_p->remove_all(find_idx, ASymbol_id_str_length);
        max_idx -= ASymbol_id_str_length;
        start_idx = find_idx;
        sym_replaced = true;
        }
      }
    else
      {
      translated = false;
      A_DPRINT("Bad symbol id!\n");
      }

    if (!sym_replaced)
      {
      start_idx = find_idx + ASymbol_id_str_length;
      }
    }  // while

  return translated;
  }
//---------------------------------------------------------------------------------------
// Called before the module has been unloaded
void FSkookumScriptRuntime::PreUnloadCallback()
  {
  A_DPRINT(A_SOURCE_STR " SkookumScript - about to unload.\n");
  }
//---------------------------------------------------------------------------------------
// Called after the module has been reloaded
void FSkookumScriptRuntime::PostLoadCallback()
  {
  A_DPRINT(A_SOURCE_STR " SkookumScript - loaded.\n");
  }