#include "playstate.hpp"

#include "main.hpp"

#include "stage.hpp"
//#include "playerbody.hpp"
//#include "controllers/humanplayercontroller.hpp"
//#include "sequencelibrary.hpp"
#include "character/character.hpp"

#include "kungflowutils.hpp" // for fmod

#include <SDL_image.h> // for light surface loading

PlayState::PlayState(KungFlowGame *game) : State(game) {

  // music

  printf("latency: %i\n", game->GetAudioLatency_ms());
  int laggyFeel_ms = 0;
  //std::string filename = "res/music/test_095bpm.ogg";
  //std::string filename = "res/music/asiavibe2_130bpm_tmp.wav";
  //std::string filename = "res/music/angrydrums_130bpm.wav";
  std::string filename = "res/music/powerhouse_130bpm.wav";
  //std::string filename = "res/music/arpstorm_135bpm.wav";
  //std::string filename = "res/music/speed_160bpm.wav";
  //std::string filename = "res/music/dnb_180bpm.wav";
  int bpm = 130; // default
  // extract bpm from filename, so players can hax in own tracks
  std::size_t bpmPos = filename.find("bpm") - 3;
  std::size_t bpmLen = 3;
  if (bpmPos != std::string::npos && bpmLen != std::string::npos) {
    bpm = atoi(filename.substr(bpmPos, bpmLen).c_str());
    printf("found: %s (= %i)\n", filename.substr(bpmPos, bpmLen).c_str(), bpm);
  }
  //bpm = 160;
  testMusic = new Music(game, this, filename, bpm, -game->GetAudioLatency_ms() - laggyFeel_ms);
  // test printf("%i, %i, %f\n", 1000 / int((float)bpm / 60.0f), int(1000.0f / ((float)bpm / 60.0f)), 1000.0f / ((float)bpm / 60.0f));

  // kung fu punch: https://freesound.org/people/nextmaking/sounds/86019/

  FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/hit01.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_Hit01]));

  FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/plop.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_SoundHit]));
  //FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/223966__dav0r__d0-meatslap-on-glass-2.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_SoundHit]));
  FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/344669__daymonjlong__pillow-smash.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_SoundHit]));
  //FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/186950__readeonly__toy-cannon-shot.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_SoundHit]));

  FMOD_CHECK(FMOD_System_CreateSound(GetAudioSystem(), "res/sfx/piw.wav", FMOD_DEFAULT, 0, &sounds[e_GlobalSound_SoundBlast]));


  // display

  viewOrientation = -0.25f * pi;//-0.25f * pi;
  defaultCameraDistance = _CAMBASEDISTANCE + 2.0f * _PHYSICSSCALE;

  const Viewport &mainViewport = game->GetMainViewport();

  camera.spatial.position = Vector2(0, 0);
  camera.spatial.scale = Vector2(1.0f, 0.5f);
  camera.spatial.z = defaultCameraDistance;
  camera.spatial.orientation = viewOrientation;//-0.25f * pi;//0.0f;
  camera.fov = 0.5f * pi;

  //float history_seconds = 0.4f;
  float history_seconds = 1.0f * GetCurrentMusic()->GetBeatDuration_ms() * 0.001f;
  camSpatialHistory.set_capacity(int(round(1000.0f / game->GetPhysicsTimeStep_ms()) * history_seconds));

  hudDarkenSprite = new Sprite();
  hudDarkenSprite->LoadFile("res/gfx/darken_effect.png", e_FilterMode_Linear);
  //hudSprite->SetAlpha(0.5f);
  hudDarkenObject = new Object(0, hudDarkenSprite);
  hudDarkenObject->spatial.position = Vector2(mainViewport.w / 2, mainViewport.h / 2);
  hudDarkenObject->spatial.scale = Vector2(mainViewport.w, mainViewport.h);

  hudSprite = new Sprite();
  hudSprite->LoadFile("res/gfx/hud.png", e_FilterMode_Linear);
  //hudSprite->SetAlpha(0.5f);
  hudObject = new Object(0, hudSprite);
  hudObject->spatial.position = Vector2(mainViewport.w / 2, mainViewport.h / 2);
  hudObject->spatial.scale = Vector2(mainViewport.w, mainViewport.h);

  lightingLayerResolutionDivider = 8;
  int lighting_bpp = 32;
  lightingLayer = SDL_CreateRGBSurface(0, mainViewport.w / lightingLayerResolutionDivider, mainViewport.h / lightingLayerResolutionDivider, lighting_bpp, 0, 0, 0, 0);

  lightSprite = new Sprite();
  lightSprite->LoadFile("res/gfx/highlight.png", e_FilterMode_Linear);

  lightSurface = IMG_Load("res/gfx/light.png");
  if (lightSurface == nullptr) {
    printf("could not load light texture\n");
    exit(EXIT_FAILURE);
  }

  //SDL_FillRect(lightSurface, nullptr, SDL_MapRGBA(lightSurface->format, 255, 0, 0, 127));

  if (SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1") == SDL_FALSE) printf("could not set render scale quality hint\n");
  lightingLayerTexture = SDL_CreateTexture(GetSDLRenderer(), lighting_bpp == 32 ? SDL_PIXELFORMAT_ARGB8888 : SDL_PIXELFORMAT_ARGB4444, SDL_TEXTUREACCESS_STREAMING, lightingLayer->w, lightingLayer->h);
  SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");

  lighting = true;


  stage = new Stage(game, this);
  stage->InitLevel(0);

  bazkie = new HumanGamer(game); // test gamer ^_^ he's so hawt right now
  stage->SpawnHumanCharacter(0, stage->GetStadium()->GetGoodGuySpawnPoint(0), bazkie);

  stateRunTime_ms = 0;
  timeStepMultiplier = 1.0f;
  runState = e_RunState_Running;
}

PlayState::~PlayState() {

  delete hudObject;
  delete hudSprite;

  delete hudDarkenObject;
  delete hudDarkenSprite;

  delete lightSprite;
  SDL_FreeSurface(lightSurface);
  SDL_DestroyTexture(lightingLayerTexture);
  SDL_FreeSurface(lightingLayer);

  //stage->UnregisterAllHumanGamers();
  delete stage;
  delete bazkie;

  FMOD_CHECK(FMOD_Sound_Release(sounds[e_GlobalSound_Hit01]));
  FMOD_CHECK(FMOD_Sound_Release(sounds[e_GlobalSound_SoundHit]));
  FMOD_CHECK(FMOD_Sound_Release(sounds[e_GlobalSound_SoundBlast]));
  testMusic->Play(false);
  delete testMusic;

}

void PlayState::OnActivate() {
}

void PlayState::OnDeactivate() {
}

void PlayState::OnStepPhysicsTime() {

  //printf("%i\n", game->GetPhysicsTimeStepsDone());

  if (!GetCurrentMusic()->IsPlaying()) GetCurrentMusic_rw()->Play(true);

  GetCurrentMusic_rw()->OnStepPhysicsTime();

/*
  // accelerate/decelerate level flow depending on joystick (test hax)
  bazkie->GetJoyInput()
  Vector2 inputDirection = game->GetJoyInput().stick1_processed;
  SetFlowTilesPerBeat(1.0f + inputDirection.coords[GetFlowAxis()] * 0.5f);
*/

//  float beatFactor = cos(GetCurrentMusic()->GetBeatOffset() * 2.0f * pi) * 0.5f + 0.5f;
//  SetTimeStepMultiplier(std::min(1.0f, 0.5f + 0.5f * beatFactor));

  stage->OnStepPhysicsTime();

  Spatial camSpatial;
  camSpatial.position = stage->GetCameraFocusPositionProj();
  camSpatial.z = defaultCameraDistance;
  // dynamic camera distance

/* buggy 'distance to mate' test
  Character *human = stage->GetAllCharactersRef().back();
  if (human) {
    Character *mate = (*--(--(stage->GetAllCharactersRef().end())));
    if (mate) {
      float distance = ( human->GetPosition() - mate->GetPosition() ).GetLength();
      camSpatial.z += distance * 0.5f;
    }
  }
*/
/* distance to enemy, only works for p1 right now
  Character *goodGuy = stage->GetHumanCharactersRef().back(); // todo: this is a hax
  if (goodGuy) {
    Character *badGuy = stage->GetClosestCharacter(1, goodGuy->GetPosition());
    if (badGuy) {
      float distance = ( goodGuy->GetPosition() - badGuy->GetPosition() ).GetLength();
      camSpatial.z += distance * 0.5f;
    }
  }
*/
  float largestMateDistance = 0.0f;
  const std::vector<Character*> &humanTeamCharacters = stage->GetCharacters(0);
  for (unsigned int i = 0; i < humanTeamCharacters.size(); i++) {
    for (unsigned int j = i + 1; j < humanTeamCharacters.size(); j++) {
      float distance = humanTeamCharacters.at(i)->GetPosition().GetDistance(humanTeamCharacters.at(j)->GetPosition());
      if (distance > largestMateDistance) largestMateDistance = distance;
    }
  }
  camSpatial.z += largestMateDistance * 0.2f;

  camSpatialHistory.push_back(camSpatial);
  //float history_seconds = 0.5f;
  //if (camSpatialHistory.size() > (1000.0f / game->GetPhysicsTimeStep_ms()) * history_seconds) camSpatialHistory.pop_front();

  boost::circular_buffer<Spatial>::iterator camSpatialIter = camSpatialHistory.begin();
  Vector2 averagePos;
  float averageHeight = 0.0f;//defaultCameraDistance;
  while (camSpatialIter != camSpatialHistory.end()) {
    averagePos += camSpatialIter->position;
    averageHeight += camSpatialIter->z;
    camSpatialIter++;
  }
  averagePos /= camSpatialHistory.size();
  averageHeight /= camSpatialHistory.size();

  camera.spatial.position = averagePos + Vector2(-0.5, -0.5); // compensation for character height
  camera.spatial.z = averageHeight;

  if (runState == e_RunState_Running) {
    stateRunTime_ms += game->GetPhysicsTimeStep_ms();
  }
}

void PlayState::OnStepGraphicsTime() {

  if (static_cast<KungFlowGame*>(game)->GetKeyInput().actionkeys[0] && lighting) lighting = false;
  if (static_cast<KungFlowGame*>(game)->GetKeyInput().actionkeys[2] && !lighting) lighting = true;

  // clear lighting buffer (make black: black == not transparent later on)
  //SDL_SetSurfaceAlphaMod(lightingLayer, 127);
  //SDL_FillRect(lightingLayer, nullptr, SDL_MapRGBA(lightingLayer->format, 0, 0, 0, 127));
  // motion blur experiment SDL_Surface *prevLightingLayer = SDL_ConvertSurface(lightingLayer, lightingLayer->format, SDL_SWSURFACE);
  //float ambientStrength = 0.0f;//0.03f; // need just a little for the ambient lighting to grab hold in places where there is no light
  if (lighting) {

    lightQueue.clear();

    Uint8 *pixeldata = static_cast<Uint8*>(lightingLayer->pixels);
    memset(pixeldata, 0, lightingLayer->w * lightingLayer->h * 4 * sizeof(Uint8));

    /*
    for (int x = 0; x < lightingLayer->w * lightingLayer->h * 4; x += 4) {
      //pixeldata[x + 0] = ambientStrength * 255;
      //pixeldata[x + 1] = ambientStrength * 255;
      //pixeldata[x + 2] = ambientStrength * 255;
      pixeldata[x + 3] = 0xff;
      //memset(pixeldata, 0, lightingLayer->w * lightingLayer->h * 4 * sizeof(Uint8));
    }
    */

  }

  stage->Draw(game->GetMainViewport(), GetCamera());

  if (lighting) {

    SDL_UpdateTexture(lightingLayerTexture, nullptr, lightingLayer->pixels, lightingLayer->w * sizeof(Uint32));

    // this is faster (WIP) *update: not sure.. using updatetexture may actually be faster, see https://forums.libsdl.org/viewtopic.php?t=9728
    //SDL_LockTexture(texture, 0, (void**)&texture_data, &texture_pitch));
    //int pitch = lightingLayer->w * sizeof(Uint32);
    //SDL_LockTexture(lightingLayerTexture, nullptr, (void**)&lightingLayer->pixels, &pitch);
    //zzz memcpy(lightingLayer->pixels, frame_data, w*h*Bpp);
    //SDL_UnlockTexture(lightingLayerTexture);

    float r, g, b;

    // fullscreen quad for color grading and such
    float gradingBias = 0.0f;
    if (gradingBias > 0.0f) {
      r = (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(0);
      g = (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(1);
      b = (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(2);
      SDL_SetRenderDrawBlendMode(GetSDLRenderer(), SDL_BLENDMODE_MOD);
      SDL_SetRenderDrawColor(GetSDLRenderer(), r * 255, g * 255, b * 255, 255);
      SDL_RenderFillRect(GetSDLRenderer(), nullptr);
      // end quad layer fun
      SDL_SetRenderDrawBlendMode(GetSDLRenderer(), SDL_BLENDMODE_NONE);
    }

    // now draw the lighting layer
    SDL_Rect rect;
    rect.x = 0;//game->GetMainViewport().x;
    rect.y = 0;//game->GetMainViewport().y;
    rect.w = game->GetMainViewport().w;
    rect.h = game->GetMainViewport().h;
    //rect.x += lightingLayerResolutionDivider

/*
    // second, add the light layer (a little) for overexposure - 'bloom' highlights
    SDL_SetTextureBlendMode(lightingLayerTexture, SDL_BLENDMODE_ADD);
    SDL_SetTextureAlphaMod(lightingLayerTexture, 127);
    SDL_RenderCopyEx(GetSDLRenderer(), lightingLayerTexture, nullptr, &rect, 0, nullptr, SDL_FLIP_NONE);
    SDL_SetTextureAlphaMod(lightingLayerTexture, SDL_ALPHA_OPAQUE);
*/

    // add ambient color to lighting layer before we modulate
    float ambientBias = 0.5f; // minimum brightness
    float ambientGreynessBias = 0.0f;
    r = ambientGreynessBias + (1.0f - ambientGreynessBias) * stage->GetStadium()->GetAmbientColor().at(0);
    g = ambientGreynessBias + (1.0f - ambientGreynessBias) * stage->GetStadium()->GetAmbientColor().at(1);
    b = ambientGreynessBias + (1.0f - ambientGreynessBias) * stage->GetStadium()->GetAmbientColor().at(2);
    Uint8 *pixeldata = static_cast<Uint8*>(lightingLayer->pixels);
    for (int x = 0; x < lightingLayer->w * lightingLayer->h * 4; x += 4) {
//      pixeldata[x + 0] = pixeldata[x + 0] * (1.0f - ambientBias) + b * 255 * ambientBias;
//      pixeldata[x + 1] = pixeldata[x + 1] * (1.0f - ambientBias) + g * 255 * ambientBias;
//      pixeldata[x + 2] = pixeldata[x + 2] * (1.0f - ambientBias) + r * 255 * ambientBias;

      // only 'ambientize' dark pixels (basically fake hdri)
      float intensity = (pixeldata[x + 0] + pixeldata[x + 1] + pixeldata[x + 2]) / (float)(3 * UINT8_MAX);
      //intensity = Curve(intensity, 1.0f); too cpu intensive
      //intensity *= intensity;//pow(intensity, 2.5f);
      //intensity = Curve(intensity, 1.0f);
      float adaptedAmbientBias = ambientBias * (1.0f - intensity);
      pixeldata[x + 0] = pixeldata[x + 0] * (1.0f - adaptedAmbientBias) + b * 255 * adaptedAmbientBias;
      pixeldata[x + 1] = pixeldata[x + 1] * (1.0f - adaptedAmbientBias) + g * 255 * adaptedAmbientBias;
      pixeldata[x + 2] = pixeldata[x + 2] * (1.0f - adaptedAmbientBias) + r * 255 * adaptedAmbientBias;

      /* motion blur experiment
      float motionBlurFactor = 0.96f;
      Uint8 *prevPixeldata = static_cast<Uint8*>(prevLightingLayer->pixels);
      pixeldata[x + 0] = pixeldata[x + 0] * (1.0f - motionBlurFactor) + prevPixeldata[x + 0] * motionBlurFactor;
      pixeldata[x + 1] = pixeldata[x + 1] * (1.0f - motionBlurFactor) + prevPixeldata[x + 1] * motionBlurFactor;
      pixeldata[x + 2] = pixeldata[x + 2] * (1.0f - motionBlurFactor) + prevPixeldata[x + 2] * motionBlurFactor;
      */
    }
    SDL_UpdateTexture(lightingLayerTexture, nullptr, lightingLayer->pixels, lightingLayer->w * sizeof(Uint32));

    // modulate by the light layer
    SDL_SetTextureBlendMode(lightingLayerTexture, SDL_BLENDMODE_MOD);
    SDL_RenderCopyEx(GetSDLRenderer(), lightingLayerTexture, nullptr, &rect, 0, nullptr, SDL_FLIP_NONE);

    // end light texture layer fun
    SDL_SetTextureBlendMode(lightingLayerTexture, SDL_BLENDMODE_NONE);

    // now add highlights using the normal SDL texture render function (so we can use full resolution instead of lightingLayer resolution)
    float bloomFactor = 0.8f;//0.65f;//0.75f;
    SDL_SetTextureBlendMode(lightSprite->GetTexture(), SDL_BLENDMODE_ADD);
    for (unsigned int i = 0; i < lightQueue.size(); i++) {

      const LightQueueEntry &entry = lightQueue.at(i);

      SDL_Rect lightRect;
      lightRect.x = 0;
      lightRect.y = 0;
      lightRect.w = lightSprite->GetWidth();
      lightRect.h = lightSprite->GetHeight();

      SDL_Rect viewportRect;
      //game->GetMainViewport().x +
      //game->GetMainViewport().y +
      float highlightScale = 0.8f;//0.5f;//0.75f; // making the highlight smaller than the base light creates a wider band of 'medium light', which may be unrealistic, but has its merits
      viewportRect.x = entry.screenPosition.coords[0] - entry.radius.coords[0] * highlightScale;
      viewportRect.y = entry.screenPosition.coords[1] - entry.radius.coords[1] * highlightScale;
      viewportRect.w = entry.radius.coords[0] * 2 * highlightScale;
      viewportRect.h = entry.radius.coords[1] * 2 * highlightScale;

      //SDL_RenderSetClipRect(GetSDLRenderer(), &game->GetMainViewport());

      // only highlight the higher end of the intensity spectrum; after all, we don't want dim lights to do ANY highlighting
      //float highlightIntensity = (entry.r + entry.g + entry.b) / 3.0f;

      /*
      // only highlight the brightest 1/3rd of lights
      float highlightIntensityR = Clamp(entry.r * 3.0f - 2.0f, 0.0f, 1.0f);
      float highlightIntensityG = Clamp(entry.g * 3.0f - 2.0f, 0.0f, 1.0f);
      float highlightIntensityB = Clamp(entry.b * 3.0f - 2.0f, 0.0f, 1.0f);
      */
      /*
      // only highlight the brightest 1/2nd of lights
      float highlightIntensityR = Clamp(entry.r * 0.5f + 0.5f, 0.0f, 1.0f);
      float highlightIntensityG = Clamp(entry.g * 0.5f + 0.5f, 0.0f, 1.0f);
      float highlightIntensityB = Clamp(entry.b * 0.5f + 0.5f, 0.0f, 1.0f);
      */
      // emphasis on higher range (cut off bottom, then pow() the remainder)
      float highlightIntensityR = pow(Clamp(entry.r * 1.1f - 0.1f, 0.0f, 1.0f), 1.8f - stage->GetStadium()->GetGradingColor().at(0) * 0.6f);
      float highlightIntensityG = pow(Clamp(entry.g * 1.1f - 0.1f, 0.0f, 1.0f), 1.8f - stage->GetStadium()->GetGradingColor().at(1) * 0.6f);
      float highlightIntensityB = pow(Clamp(entry.b * 1.1f - 0.1f, 0.0f, 1.0f), 1.8f - stage->GetStadium()->GetGradingColor().at(2) * 0.6f);

/*
      // color grading
      float gradingBias = 1.0f;
      if (gradingBias > 0.0f) {
        highlightIntensityR = highlightIntensityR * (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(0);
        highlightIntensityG = highlightIntensityG * (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(1);
        highlightIntensityB = highlightIntensityB * (1.0f - gradingBias) + gradingBias * stage->GetStadium()->GetGradingColor().at(2);
      }
*/

      //highlightIntensity = Curve(highlightIntensity, 1.0f);
      //highlightIntensity = Curve(highlightIntensity, 1.0f);
      //highlightIntensity = pow(highlightIntensity, 2.0f);
      //SDL_SetTextureColorMod(lightSprite->GetTexture(), entry.r * 255 * bloomFactor * highlightIntensityR, entry.g * 255 * bloomFactor * highlightIntensityG, entry.b * 255 * bloomFactor * highlightIntensityB);
      SDL_SetTextureColorMod(lightSprite->GetTexture(), 255 * bloomFactor * highlightIntensityR, 255 * bloomFactor * highlightIntensityG, 255 * bloomFactor * highlightIntensityB);
      //SDL_SetRenderDrawColor(GetSDLRenderer(), entry.r * 255, entry.g * 255, entry.b * 255, SDL_ALPHA_OPAQUE);
      SDL_RenderCopyEx(GetSDLRenderer(), lightSprite->GetTexture(), &lightRect, &viewportRect, 0, nullptr, SDL_FLIP_NONE);
    }
    SDL_SetTextureBlendMode(lightSprite->GetTexture(), SDL_BLENDMODE_NONE);
    SDL_SetTextureColorMod(lightSprite->GetTexture(), 255, 255, 255);

  }

  if (!lighting) {
    DrawDebugCoordCross(Vector2(0, 0), e_CoordinateSystem_World, 0.3f);
    DrawDebugCoordCross(Vector2(1, 0), e_CoordinateSystem_World, 0.1f);
  }

  float intensity = cos(GetCurrentMusic()->GetBeatOffset() * 2.0f * pi) * 0.5f + 0.5f;
  intensity = intensity * 0.3f + 0.7f * (1.0f - GetCurrentMusic()->GetBeatOffset());
  float darkness = 1.0f - intensity;
  //darkness = pow(darkness, 2.0f);
  darkness = darkness * 0.9f;
  //intensity = 0.5f + intensity;
  hudDarkenObject->GetSprite()->SetAlpha(darkness);
  hudDarkenObject->Draw();

  //if (lighting) {
  hudObject->Draw();
  //}

  FlushDebugGraphics();
}

void PlayState::DrawLight(const Vector2 &screenPosition, const Vector2 &radius, float r, float g, float b) const {

  LightQueueEntry lightQueueEntry;
  lightQueueEntry.screenPosition = screenPosition;
  lightQueueEntry.radius = radius;
  lightQueueEntry.r = r;
  lightQueueEntry.g = g;
  lightQueueEntry.b = b;
  lightQueue.push_back(lightQueueEntry);

  SDL_Rect rect; // lightingLayer rect
  rect.x = screenPosition.coords[0] - radius.coords[0];//round(radius.coords[0] * 0.707f);
  rect.y = screenPosition.coords[1] - radius.coords[1];//round(radius.coords[1] * 0.707f);
  //rect.w = round(radius.coords[0] * 0.707f * 2.0f);
  //rect.h = round(radius.coords[1] * 0.707f * 2.0f);
  rect.w = round(radius.coords[0] * 2.0f);
  rect.h = round(radius.coords[1] * 2.0f);

  rect.x /= lightingLayerResolutionDivider;
  rect.y /= lightingLayerResolutionDivider;
  rect.w /= lightingLayerResolutionDivider;
  rect.h /= lightingLayerResolutionDivider;

  SDL_Rect lightRect;
  lightRect.x = 0;
  lightRect.y = 0;
  lightRect.w = lightSurface->w;
  lightRect.h = lightSurface->h;

  // out of bounds completely?
  if (rect.x > lightingLayer->w) return;
  if (rect.x + rect.w < 0) return;
  if (rect.y > lightingLayer->h) return;
  if (rect.y + rect.h < 0) return;


  // if rect is larger than the target surface, it will be automatically clamped by SDL_BlitScaled.
  // therefore, we need to adapt the source rect to correct for this behaviour.
  // this auto-clamping doesn't seem to happen anymore since SDL 2.0.4
  SDL_version linked;
  SDL_GetVersion(&linked);
  if (linked.major == 2 && linked.minor == 0 && linked.patch < 4) {

    int lightRect_w_cache = lightRect.w;
    int lightRect_h_cache = lightRect.h;
    if (rect.x < 0) {
      //rect.x = 0;
      lightRect.x += -rect.x / (float)rect.w * lightRect.w;
      lightRect.w -= lightRect.x;
    }
    if (rect.y < 0) {
      //rect.y = 0;
      lightRect.y += -rect.y / (float)rect.h * lightRect.h;
      lightRect.h -= lightRect.y;
    }
    if (rect.x + rect.w >= lightingLayer->w) {
      //rect.w -= (rect.x + rect.w) - lightingLayer->w;
      lightRect.w -= std::min(lightRect.w, (int)( ((rect.x + rect.w) - lightingLayer->w + 1) / (float)rect.w * lightRect_w_cache ));
    }
    if (rect.y + rect.h >= lightingLayer->h) {
      //rect.h -= (rect.y + rect.h) - lightingLayer->h;
      lightRect.h -= std::min(lightRect.h, (int)( ((rect.y + rect.h) - lightingLayer->h + 1) / (float)rect.h * lightRect_h_cache ));
    }

  }

  //SDL_RenderSetClipRect(GetSDLRenderer(), nullptr);
  //SDL_SetClipRect(lightSurface, &lightRect);
  //SDL_SetClipRect(lightingLayer, &rect);

  /*if (rect.x + rect.w > (game->GetMainViewport().x + game->GetMainViewport().w) / lightingLayerResolutionDivider) {
    lightRect.w -= std::min(lightRect.w, (int)(( ((rect.x + rect.w) - (game->GetMainViewport().x + game->GetMainViewport().w) / lightingLayerResolutionDivider) / (float)rect.w) * lightRect.w));
  }
  if (rect.y + rect.h > (game->GetMainViewport().y + game->GetMainViewport().h) / lightingLayerResolutionDivider) {
    lightRect.h -= std::min(lightRect.h, (int)(( ((rect.y + rect.h) - (game->GetMainViewport().y + game->GetMainViewport().h) / lightingLayerResolutionDivider) / (float)rect.h) * lightRect.h));
  }*/

  // now render onto lighting layer surface
  SDL_SetSurfaceBlendMode(lightSurface, SDL_BLENDMODE_ADD);
  //SDL_SetAlpha(lightSurface, false);
  SDL_SetSurfaceBlendMode(lightingLayer, SDL_BLENDMODE_NONE);
//  SDL_SetSurfaceAlphaMod(lightSurface, 127);
//  SDL_SetSurfaceAlphaMod(lightingLayer, 127);

  // overpower the lights by 50%, so that all lights over 66.6% intensity will look similar (100% source image color).
  // the brightest 1/3rd or lights will be differentiated later by the bloom highlights
  //SDL_SetSurfaceColorMod(lightSurface, std::min(255, int(r * 255 * 1.5f)), std::min(255, int(g * 255 * 1.5f)), std::min(255, int(b * 255 * 1.5f)));
  // version with 'compression' instead of cutoff (hard knee)
  SDL_SetSurfaceColorMod(lightSurface, int(std::min(powf(r, 0.5f), r * 1.5f) * 255), int(std::min(powf(g, 0.5f), g * 1.5f) * 255), int(std::min(powf(b, 0.5f), b * 1.5f) * 255));

  // overpower the lights by 100%, so that all lights over 50.0% intensity will look similar (100% source image color).
  // the brightest 1/2nd or lights will be differentiated later by the bloom highlights
  //SDL_SetSurfaceColorMod(lightSurface, std::min(255, int(r * 255 * 2.0f)), std::min(255, int(g * 255 * 2.0f)), std::min(255, int(b * 255 * 2.0f)));

//  SDL_SetSurfaceColorMod(lightingLayer, r * 255, g * 255, b * 255);

  //SDL_RenderCopyEx(GetSDLRenderer(), lightSprite->GetTexture(), &lightRect, &rect, 0, nullptr, SDL_FLIP_NONE);
  //return;

  SDL_BlitScaled(lightSurface, &lightRect, lightingLayer, &rect);
  //SDL_BlitSurface(lightSurface, nullptr, lightingLayer, rect);

/* we can now ignore alpha layer, because the highlight ADD mode that was used at first is now replaced by a postprocess 'bloom' render
  Uint8 *pixeldata = static_cast<Uint8*>(lightingLayer->pixels);
  for (int x = 0; x < lightingLayer->w; x++) {
    for (int y = 0; y < lightingLayer->h; y++) {

      int address = (x + y * lightingLayer->w) * 4;

      // silly SDL_BlitScaled seems to like to overwrite alpha values to zero. So put them back to 0xff :p
      //pixeldata[address + 3] = 0xff;

      // use alpha layer to denote bloom highlights
      float intensity = (pixeldata[address + 0] + pixeldata[address + 1] + pixeldata[address + 2]) / (float)(3 * UINT8_MAX);
      pixeldata[address + 3] = intensity * intensity * 255;

    }
  }
*/

}

void PlayState::FlushDebugGraphics() const {

  SDL_SetRenderDrawBlendMode(GetSDLRenderer(), SDL_BLENDMODE_BLEND);
  for (unsigned int i = 0; i < debugLines.size(); i++) {

    Vector2 p1screen, p2screen;
    if (debugLines.at(i).coordinateSystem == e_CoordinateSystem_World) {
      p1screen = WorldToScreenVector(debugLines.at(i).position1, game->GetMainViewport(), GetCamera());
      p2screen = WorldToScreenVector(debugLines.at(i).position2, game->GetMainViewport(), GetCamera());
    } else {
      p1screen = debugLines.at(i).position1;
      p2screen = debugLines.at(i).position2;
    }

    SDL_SetRenderDrawColor(GetSDLRenderer(), debugLines.at(i).r * 255.0f, debugLines.at(i).g * 255.0f, debugLines.at(i).b * 255.0f, debugLines.at(i).a * 255.0f);
    SDL_RenderDrawLine(GetSDLRenderer(), p1screen.coords[0], p1screen.coords[1], p2screen.coords[0], p2screen.coords[1]);
  }

  debugLines.clear();

  for (unsigned int i = 0; i < debugTexts.size(); i++) {
    Sprite *textSprite = new Sprite();
    SDL_Color color = { Uint8(debugTexts.at(i).r * 255), Uint8(debugTexts.at(i).g * 255), Uint8(debugTexts.at(i).b * 255) };
    textSprite->CreateText(game->GetFont(e_FontID_Debug), debugTexts.at(i).text, color, e_FilterMode_Linear);
    Spatial spatial;
    if (debugTexts.at(i).coordinateSystem == e_CoordinateSystem_World) {
      spatial.position = WorldToScreenVector(debugTexts.at(i).position, game->GetMainViewport(), GetCamera());
    } else {
      spatial.position = debugTexts.at(i).position;
    }
    spatial.scale.coords[0] = textSprite->GetWidth();
    spatial.scale.coords[1] = textSprite->GetHeight();
    SDL_SetTextureAlphaMod(textSprite->GetTexture(), Uint8(debugTexts.at(i).a * 255));
    textSprite->Draw(spatial);
    delete textSprite;
  }

  debugTexts.clear();

}

void PlayState::DrawDebugLine(const Vector2 &position1, const Vector2 &position2, e_CoordinateSystem coordinateSystem, float r, float g, float b, float a) const {
  DebugLine debugLine;
  debugLine.position1 = position1;
  debugLine.position2 = position2;
  debugLine.coordinateSystem = coordinateSystem;
  debugLine.r = r;
  debugLine.g = g;
  debugLine.b = b;
  debugLine.a = a;
  debugLines.push_back(debugLine);
}

void PlayState::DrawDebugCoordCross(const Vector2 &center, e_CoordinateSystem coordinateSystem, float intensity) const {
  Vector2 debugP1 = Vector2(center.coords[0] + 100, center.coords[1] + 100);
  Vector2 debugP2 = Vector2(center.coords[0] + -100, center.coords[1] + -100);
  DrawDebugLine(debugP1, debugP2, coordinateSystem, 1.0, 1.0, 0.0f, intensity);
  debugP1 = Vector2(center.coords[0] + -100, center.coords[1] + 0);
  debugP2 = Vector2(center.coords[0] + 100, center.coords[1] + 0);
  DrawDebugLine(debugP1, debugP2, coordinateSystem, 1.0, 0.0, 1.0f, intensity);
  debugP1 = Vector2(center.coords[0] + 0, center.coords[1] + -100);
  debugP2 = Vector2(center.coords[0] + 0, center.coords[1] + 100);
  DrawDebugLine(debugP1, debugP2, coordinateSystem, 0.0, 1.0, 1.0f, intensity);
}

void PlayState::DrawDebugText(const std::string &text, const Vector2 &position, e_CoordinateSystem coordinateSystem, float r, float g, float b, float a) const {
  DebugText debugText;
  debugText.text = text;
  debugText.position = position;
  debugText.coordinateSystem = coordinateSystem;
  debugText.r = r;
  debugText.g = g;
  debugText.b = b;
  debugText.a = a;
  debugTexts.push_back(debugText);
}

void PlayState::DrawLine(const Vector2 &position1, const Vector2 &position2, e_CoordinateSystem coordinateSystem, float r, float g, float b, float a) const {
  Vector2 p1screen, p2screen;
  if (coordinateSystem == e_CoordinateSystem_World) {
    p1screen = WorldToScreenVector(position1, game->GetMainViewport(), GetCamera());
    p2screen = WorldToScreenVector(position2, game->GetMainViewport(), GetCamera());
  } else {
    p1screen = position1;
    p2screen = position2;
  }
  SDL_SetRenderDrawBlendMode(GetSDLRenderer(), SDL_BLENDMODE_BLEND);
  SDL_SetRenderDrawColor(GetSDLRenderer(), r * 255.0f, g * 255.0f, b * 255.0f, a * 255.0f);
  SDL_RenderDrawLine(GetSDLRenderer(), p1screen.coords[0], p1screen.coords[1], p2screen.coords[0], p2screen.coords[1]);
}

void PlayState::SoundPlay(e_GlobalSound sound, float volume) const {
  FMOD_CHANNEL *channel = nullptr;
  FMOD_CHECK(FMOD_System_PlaySound(GetAudioSystem(), sounds[sound], static_cast<KungFlowGame*>(game)->GetAudioSFXChannelGroup(), true, &channel));
  //FMOD_CHECK(FMOD_Channel_SetChannelGroup(channel, static_cast<KungFlowGame*>(game)->GetAudioSFXChannelGroup()));
  FMOD_CHECK(FMOD_Channel_SetVolume(channel, volume * _AUDIOVOLUME_SFX));
  //FMOD_CHECK(FMOD_Channel_SetPitch(channel, 0.95f + Random(0.0f, 0.1f))); todo
  FMOD_CHECK(FMOD_Channel_SetPitch(channel, 1.0f));
  FMOD_CHECK(FMOD_Channel_SetPaused(channel, false));
}


// --- MUSIC ------------------------------------------------------------------------------------------------------------------------

Music::Music(const KungFlowGame *game, const PlayState *playState, const std::string &filename, float bpm, signed int syncOffset_ms) : game(game), playState(playState), bpm(bpm), isPlaying(false), currentTime_ms(0), syncOffset_ms(syncOffset_ms) {
  FMOD_CHECK(FMOD_System_CreateSound(playState->GetAudioSystem(), filename.c_str(), FMOD_CREATECOMPRESSEDSAMPLE, 0, &theSoundOfMusic));
  channel = nullptr;
  FMOD_CHECK(FMOD_System_PlaySound(playState->GetAudioSystem(), theSoundOfMusic, game->GetAudioMusicChannelGroup(), true, &channel));
  FMOD_CHECK(FMOD_Channel_SetVolume(channel, 1.0f * _AUDIOVOLUME_MUSIC));
  FMOD_CHECK(FMOD_Channel_SetPaused(channel, true));
  FMOD_CHECK(FMOD_Channel_SetMode(channel, FMOD_LOOP_NORMAL));
}

Music::~Music() {
  FMOD_CHECK(FMOD_Sound_Release(theSoundOfMusic));
}

void Music::OnStepPhysicsTime() {
  if (isPlaying) currentTime_ms += playState->GetPhysicsTimeStep_ms();

  //printf("%lu vs %lu\n", currentTime_ms, game->GetPhysicsTime_ms());
}

void Music::Play(bool well) {
  //FMOD_CHECK(FMOD_Channel_SetVolume(channel, volume * 0.5f + 0.5f));
  //FMOD_CHECK(FMOD_Channel_SetPitch(channel, 0.95f + Random(0.0f, 0.1f)));
  FMOD_CHECK(FMOD_Channel_SetPaused(channel, !well)); // unwell? :p

  isPlaying = well;
}

float Music::GetBPM() const {
  return bpm;
}

int Music::GetBeat() const {
  return int( (float(currentTime_ms * 0.001f) + (syncOffset_ms * 0.001f)) * (bpm / 60.0f) );
}

float Music::GetBeatOffset() const {
  return ModuloFloat((float(currentTime_ms * 0.001f) + (syncOffset_ms * 0.001f)) * (bpm / 60.0f), 1.0f);
}


// --- HUMANGAMER -------------------------------------------------------------------------------------------------------------------

HumanGamer::HumanGamer(const KungFlowGame *game) : game(game) {
}

HumanGamer::~HumanGamer() {
}

const KeyInput &HumanGamer::GetKeyInput() const {
  return game->GetKeyInput();
}

const JoyInput &HumanGamer::GetJoyInput() const {
  return game->GetJoyInput();
}
