/// local includes
#include "GameSWFHelper.h"
#include "GameSWFLocalization.h"
#include "Utils/RangeFor.h"

/// library includes
#include <CasualCore.h>
#include <RKDevice.h>
#include <RKCamera.h>
#include "RKEngine/Private/Include/RKRender.h"  //!< For RKRender_SetRenderLayerCustomViewport / RKRender_SetRenderLayerCustomCamera

#include "Utils/SoundEmulator.h"

namespace gameswf
{
  static RKList<RKString> s_preserved(4);
  static RKList<RKString> s_preservedCopy;
  static bool s_visible = true;

  FlashFX* load(const char* filename, bool background /*= false*/) {
    FlashFX* fx = CGAME->GetFlashManager()->GetFlashFX(filename);
    if (fx == nullptr) {
      fx = CGAME->GetFlashManager()->LoadFlashFX(filename, background);

#if defined(CC_DEFAULT_SWF_SCALE)
      CasualCore::GameSettings* settings = CGAME->GetSettings();
      float scale = (settings->m_defaultSWFCreationHeight > settings->m_defaultSWFCreationWidth)
        ? (static_cast<float>(RKDevice_GetWidth()) / settings->m_defaultSWFCreationWidth)
        : (static_cast<float>(RKDevice_GetHeight()) / 960.f); // settings->m_defaultSWFCreationHeight;
      CGAME->GetFlashManager()->SetFlashScale(fx, scale);
#endif // CC_DEFAULT_SWF_SCALE
    }
    else
    {
      fx = CGAME->GetFlashManager()->LoadFlashFX(filename, background);
    }
    if (s_visible)
      return fx;
    
    for (const RKString& name : s_preserved) {
      if (name == filename) return fx;
    }

    CGAME->GetFlashManager()->SetVisible(filename, false);

    return fx;
  }

  void unload(FlashFX* swf) {
    if (swf == nullptr) return;
    if (GameSWFLocalization::IsValid())
      GameSWFLocalization::GetInstance().RemoveLocalizedElements(swf);
    CGAME->GetFlashManager()->UnloadFlashFX(swf);
  }

  void preserveVisibility(const char* name) {
    s_preserved.AppendUnique(name);
  }

  void pushVisibiliyList() {
    s_preservedCopy = s_preserved;
    s_preserved.Clear();
  }

  void popVisibiliyList() {
    s_preserved = s_preservedCopy;
    s_preservedCopy.Clear();
  }

} // namespace gameswf

GameSWFHelper::GameSWFHelper()
{
  gameswf::registerNativeFunction("PlaySWFSound", &GameSWFHelper::PlaySound);
  gameswf::registerNativeFunction("ButtonPressedToScript", &GameSWFHelper::ButtonPressedToScript);
  gameswf::registerNativeFunction("GetDeviceWidth", &GameSWFHelper::GetDeviceWidth);
  gameswf::registerNativeFunction("GetDeviceHeight", &GameSWFHelper::GetDeviceHeight);
  gameswf::registerNativeFunction("GetDeviceAspect", &GameSWFHelper::GetDeviceAspect);
  gameswf::registerNativeFunction("IsOnFrameWithLabel", &GameSWFHelper::IsOnFrameWithLabel);
}

void GameSWFHelper::ClearCustomCamera(RKRenderLayer* a_pRenderLayer)
{
  RKRender_SetRenderLayerCustomCamera(a_pRenderLayer, nullptr);
}

void GameSWFHelper::SetCameraViewportToCharacterHandle(const RKRect &viewport, RKCamera* a_pCamera, RKRenderLayer* a_pRenderLayer)
{
  RKRender_SetRenderLayerCustomCamera(a_pRenderLayer, a_pCamera);
  RKIntRect viewportRect;
  viewportRect.x = static_cast<int>(viewport.x);
  viewportRect.y = static_cast<int>(viewport.y);
  viewportRect.w = static_cast<int>(viewport.w);
  viewportRect.h = static_cast<int>(viewport.h);
  RKRender_SetRenderLayerCustomViewport(a_pRenderLayer, &viewportRect);
}

gameswf::Point GameSWFHelper::WorldToSWFCoordinates(const gameswf::FlashFX* a_pFX, const RKVector& a_worldPosition)
{
  RKVector screenCoords(RKVector::Zero);

  RKCamera* camera = RKCamera_GetCurrent();
  if (camera != nullptr)
  {
    // Does projection transformation.
    RKMatrix viewProjectionMatrix = camera->GetViewProjection();
    RKVector transformedPoint = viewProjectionMatrix.Transform(a_worldPosition);
    screenCoords = transformedPoint / transformedPoint.w;

    // Adjusts if from (x,y) = (-1 to 1, -1 to 1) to screen (x,y) = (0 to 1, 1 to 0)
    screenCoords.x = 0.5f + screenCoords.x / 2.0f;
    screenCoords.y = 0.5f - screenCoords.y / 2.0f;

    float deviceRatio = RKDevice_GetAspect();
    float flashRatio = static_cast<float>(a_pFX->getBoundWidth()) / static_cast<float>(a_pFX->getBoundHeight());
    float ratio = deviceRatio / flashRatio;

    //Adjusts to the screen size.
    screenCoords.x *= a_pFX->getBoundWidth() * ratio;
    screenCoords.y *= a_pFX->getBoundHeight();
  }

  return gameswf::Point(screenCoords.x, screenCoords.y);
}

void GameSWFHelper::PlaySound(const gameswf::FunctionCall& a_fn)
{
  if (a_fn.nargs != 0)
  {
    const char* arg = a_fn.arg(0).toCStr();
    if (arg != nullptr) {
      SOUND->Play(arg);
    }
    else {
      RKASSERT(false, "Attempting to play NULL sound");
    }
  }
}

void GameSWFHelper::ButtonPressedToScript(const gameswf::FunctionCall& a_fn)
{
  if (a_fn.nargs > 0)
  {
    const char* handleName = a_fn.arg(0).toCStr();
    if (handleName != nullptr) {
      const char* functionName      = a_fn.nargs > 1 ? a_fn.arg(1).toCStr() : "";
      const char* functionParameter = a_fn.nargs > 2 ? a_fn.arg(2).toCStr() : "";
      SCRIPT->CallScene("ButtonPressed", "sss", handleName, functionName, functionParameter);
    }
    else {
      RKLOGt("ui", "Attempting to pass scene script a button press with no handle name");
    }
  }
}

void GameSWFHelper::GetDeviceWidth(const gameswf::FunctionCall& a_fn)
{
  a_fn.result->setInt(RKDevice_GetWidth());
}

void GameSWFHelper::GetDeviceHeight(const gameswf::FunctionCall& a_fn)
{
  a_fn.result->setInt(RKDevice_GetHeight());
}

void GameSWFHelper::GetDeviceAspect(const gameswf::FunctionCall& a_fn)
{
  a_fn.result->setDouble((static_cast<double>(RKDevice_GetWidth()) / RKDevice_GetHeight()));
}

void GameSWFHelper::IsOnFrameWithLabel(const gameswf::FunctionCall& a_fn)
{
  gameswf::CharacterHandle obj = gameswf::CharacterHandle(a_fn.this_ptr);
  if (obj.isValid())
  {
    const bool bIsOnFrame = obj.getCurrentFrame() == obj.getFrameIdFromLabel(a_fn.arg(0).toCStr());
    a_fn.result->setBool(bIsOnFrame);
  }
}

bool GameSWFHelper::IsHandleValidLog(gameswf::CharacterHandle a_hHandle, const char* a_sErrorMessage /*= nullptr*/)
{
  if (!a_hHandle.isValid())
  {
    RKString errorMessage = "gameswf::CharacterHandle invalid";
    if (a_sErrorMessage != nullptr)
    {
      RKString extraMessage; 
      extraMessage.MakeFormatted(": %s", a_sErrorMessage);
      errorMessage.Append(extraMessage);
    }

    RKLOGt_ERROR("ui", errorMessage.GetString());
    return false;
  }
  return true;
}

bool GameSWFHelper::SetHandleText(const char* a_sHandleName, const char* a_sText, gameswf::FlashFX* a_pFX, const char* a_sErrorMessage /*= nullptr*/)
{
  gameswf::CharacterHandle hHandle = a_pFX->find(a_sHandleName);
  if (GameSWFHelper::IsHandleValidLog(hHandle, a_sErrorMessage)) {
    hHandle.setText(a_sText);
    return true;
  }
  return false;
}

bool GameSWFHelper::AreAllAncestorsVisible(gameswf::CharacterHandle& a_hHandle)
{
  if (!a_hHandle.isValid()) return true;  // If the handle is invalid and we haven't yet returned false, everything is visible.
  if (!a_hHandle.isVisible()) return false;
  gameswf::CharacterHandle hParent = a_hHandle.getParent();
  return AreAllAncestorsVisible(hParent);
}

void GameSWFHelper::SetAllChildrenVisible(gameswf::CharacterHandle& a_hHandle, bool a_bVisible)
{
  if (a_hHandle.isValid()) {
    gameswf::array<gameswf::CharacterHandle> pChildren;
    a_hHandle.getChildren(pChildren);

    int iSize = pChildren.size();
    for (int i = 0; i < iSize; ++i)
    {
      const char* sName = pChildren[i].getName().c_str();
      pChildren[i].setVisible(a_bVisible);
      pChildren[i].setEnabled(a_bVisible);
    }
  }
  return;
}

void GameSWFHelper::SetRelativeSWFOrder(gameswf::FlashFX* a_pSWFBeingReordered, gameswf::FlashFX* a_pRelativeSWF, int a_order)
{
  CGAME->GetFlashManager()->SetFlashOrder(a_pSWFBeingReordered, CGAME->GetFlashManager()->GetFlashOrder(a_pRelativeSWF) + a_order);
}

bool GameSWFHelper::IsOnFrame(gameswf::CharacterHandle& a_hHandle, const char* a_sLabelName)
{
  return a_hHandle.getCurrentFrame() == a_hHandle.getFrameIdFromLabel(a_sLabelName);
}

gameswf::Point GameSWFHelper::GetHandleWorldPosition(gameswf::CharacterHandle& a_hHandle)
{
  gameswf::Point result;

  static const float ratio_4_3 = (4.0f / 3.0f) * 1.05f;  // Give us a 5% buffer - some 4:3 aspects are a touch larger than 4/3.
  bool bScaleForLargerAspect = RKDevice_GetAspect() >= ratio_4_3;

  while (a_hHandle.isValid())
  {
    if (bScaleForLargerAspect)  // Check if we need to scale so we don't unecessarily query for scale on each element in the hierarchy
    {
      gameswf::ASValue ASScale = a_hHandle.getMember("Scale_for_larger_aspect_ratio");
      if (ASScale.isNumber())
      {
        float scale = ASScale.toFloat();
        if (scale != 0.0f)
        {
          // If we are an AnchorNode with a scale applied, we want to factor that scale into our children's positions 
          result.m_x *= scale;
          result.m_y *= scale;
        }
      }
    }

    result.m_x += a_hHandle.getMember("_x").toFloat();
    result.m_y += a_hHandle.getMember("_y").toFloat();

    a_hHandle = a_hHandle.getParent();
  }
  return result;
}

void GameSWFHelper::PrintLeafNames(gameswf::CharacterHandle& a_hHandle)
{
  if (a_hHandle.isValid()) 
  {
    gameswf::array<gameswf::CharacterHandle> pChildren;
    a_hHandle.getChildren(pChildren);

    int iSize = pChildren.size();
    for (int i = 0; i < iSize; ++i)
    {
      PrintLeafNames(pChildren[i]);
    }

    // Leaf element, print path
    if (iSize == 0)
    {
      // Recurse up the hierarchy to determine the full handle path
      RKString sFullName = "";
      gameswf::CharacterHandle hCurrentHandle = a_hHandle;
      const char* MISSING_IDENTIFIER = "NAME_MISSING";
      while (hCurrentHandle.isValid())
      {
        RKString sPrevName = sFullName;
        const char* sCurrentName = hCurrentHandle.getName().size() > 0 ? hCurrentHandle.getName().c_str() : MISSING_IDENTIFIER; // If the name is missing, the handle path cannot be relied upon. a name of "_instanceX" will be generated.
        sFullName = RKString::MakeFormatted("%s.%s", sCurrentName, sPrevName.GetString());
        hCurrentHandle = hCurrentHandle.getParent();
      }
      // Clean up the string
      sFullName.StripLeft(MISSING_IDENTIFIER);  // what should be "_level0" is displaying as empty. Remove it.
      sFullName.StripLeft(".");
      sFullName.StripRight(".");

      RKLOGt_INFO("ui", sFullName.GetString());
    }
  }
  return;
}

void GameSWFHelper::SetLocalizationID(gameswf::CharacterHandle& a_hHandle, const char* a_sLocalizationID)
{
  gameswf::ASValue sLocalizationParamater = a_sLocalizationID;
  a_hHandle.invokeMethod("setTranslationID", &sLocalizationParamater, 1);
}

void GameSWFHelper::ClearAutomaticLocalization(gameswf::CharacterHandle& a_hHandle)
{
  a_hHandle.setMember("untranslated",   gameswf::ASValue());  // Clear untranslated string
  a_hHandle.setMember("translationID",  gameswf::ASValue());  // Clear loca ID       
  a_hHandle.invokeMethod("TextUnload");                       // Unregister it as auto-loca so we don't override custom messages
}

RKString GameSWFHelper::GetHtmlText(const char* a_sFontName, const char* a_sText)
{
  return RKString::MakeFormatted("<font face='%s'>%s</font>", a_sFontName, a_sText);
}

void GameSWFHelper::SetButtonEnabled(gameswf::CharacterHandle& a_hButton, bool a_bIsEnabled)
{
  gameswf::ASValue bDisabled = !a_bIsEnabled;
  a_hButton.invokeMethod("setDisabled", &bDisabled, 1);
}

bool GameSWFHelper::IsBetweenAnimations(gameswf::CharacterHandle& a_hElement, const char* a_sStartLabelName, const char* a_sEndLabelName, bool a_bInclusiveOfEndLabel/* = false*/)
{
  int currentFrame  = a_hElement.getCurrentFrame();
  int startFrame    = a_hElement.getFrameIdFromLabel(a_sStartLabelName);
  int endFrame      = a_hElement.getFrameIdFromLabel(a_sEndLabelName);

  // Reduce final frame test if required (checking bounds)
  if (!a_bInclusiveOfEndLabel && endFrame > 0)
  {
    --endFrame;
  }

  return currentFrame >= startFrame && currentFrame <= endFrame;
}
