#include "utils.hpp"

#include <SDL_image.h>

namespace gaia {

  SDL_Window *_SDLWindow;
  SDL_Renderer *_SDLRenderer;

  int _contextWidth;
  int _contextHeight;

  Spatial SpatialNode::GetDerivedSpatial() const {
    Spatial derivedSpatial = spatial;

    if (parent) derivedSpatial *= parent->GetDerivedSpatial();

    return derivedSpatial;
  }


  SDL_Window *GetSDLWindow() {
    return _SDLWindow;
  }

  SDL_Renderer *GetSDLRenderer() {
    return _SDLRenderer;
  }

  int GetContextWidth() {
    return _contextWidth;
  }

  int GetContextHeight() {
    return _contextHeight;
  }

  int Modulo(int m, int n) { return m >= 0 ? m % n : ( n - abs(m % n) ) % n; } // because negative numbers are treated dependent on implementation, *sigh*
  float ModuloFloat(float m, float n) { return m - n * floor(m / n); } // because negative numbers are treated dependent on implementation, *sigh*

  float ModulateIntoRange(float value, float min, float max) {
    float step = max - min;
    float newValue = value;
    while (newValue < min) newValue += step;
    while (newValue > max) newValue -= step;
    return newValue;
  }

  float Clamp(float value, float min, float max) {
    assert(max >= min);
    if (min > value) return min;
    if (max < value) return max;
    return value;
  }

  float NormalizedClamp(float value, float min, float max) {
    assert(max > min);
    float banana = Clamp(value, min, max);
    banana = (banana - min) / (max - min);
    return banana;
  }

  float Curve(float source, float bias) { // make linear / into sined _/-
    return (sin((source - 0.5f) * pi) * 0.5f + 0.5f) * bias +
           source * (1.0f - bias);
  }

  signed int SignSide(float n) {
    return n >= 0 ? 1 : -1;
  }

  float Random(float min, float max) {
    return (rand() / (float)RAND_MAX) * (max - min) + min;
  }

  inline
  Vector2 PointOnLine(float u, const Vector2 &p1, const Vector2 &p2) {
    return p1 + (p2 - p1) * u;
  }

  Vector2 GetCubicBezierPosition(float u, const Vector2 &p1, const Vector2 &p2, const Vector2 &c1, const Vector2 &c2) {
    Vector2 ab = PointOnLine(u, p1, c1);
    Vector2 bc = PointOnLine(u, c1, c2);
    Vector2 cd = PointOnLine(u, c2, p2);

    Vector2 abbc = PointOnLine(u, ab, bc);
    Vector2 bccd = PointOnLine(u, bc, cd);

    return PointOnLine(u, abbc, bccd);
  }


  int InitSDL(int w, int h, bool vsync, e_WindowMode windowMode, const std::string windowTitle, int renderContextResolutionMultiplier) {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
      printf("SDL_Init error: %s", SDL_GetError());
      return 1;
    }

    _SDLWindow = SDL_CreateWindow(windowTitle.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, SDL_WINDOW_SHOWN);
    if (_SDLWindow == nullptr) {
      printf("SDL_CreateWindow error: %s", SDL_GetError());
      SDL_Quit();
      return 1;
    }
    _contextWidth = w * renderContextResolutionMultiplier;
    _contextHeight = h * renderContextResolutionMultiplier;

    Uint32 windowFlags = 0;
    switch (windowMode) {
      case e_WindowMode_BorderlessFullscreen:
        windowFlags = SDL_WINDOW_FULLSCREEN_DESKTOP;
      case e_WindowMode_Fullscreen:
        windowFlags = SDL_WINDOW_FULLSCREEN;
        break;
      case e_WindowMode_Windowed:
        windowFlags = 0;
        break;
      default:
        windowFlags = 0;
        break;
    }

    int errorCode = SDL_SetWindowFullscreen(_SDLWindow, windowFlags);
    if (errorCode != 0) {
      //printf("could not set fullscreen mode to %i", mode;)
      SDL_Quit();
      return 1;
    }

    _SDLRenderer = SDL_CreateRenderer(_SDLWindow, -1, SDL_RENDERER_ACCELERATED | (vsync ? SDL_RENDERER_PRESENTVSYNC : 0) );
    if (_SDLRenderer == nullptr) {
      SDL_DestroyWindow(_SDLWindow);
      printf("SDL_CreateRenderer error: %s", SDL_GetError());
      SDL_Quit();
      return 1;
    }
    SDL_RenderSetLogicalSize(_SDLRenderer, _contextWidth, _contextHeight);

    IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG);

    int initError = SDL_InitSubSystem(SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
    if (initError < 0) {
      printf("SDL_InitSubSystem: %s\n", SDL_GetError());
      SDL_Quit();
      return 1;
    }

    if (TTF_Init() == -1) {
      printf("TTF_Init: %s\n", TTF_GetError());
      exit(2);
    }

    return 0;
  }

  int ExitSDL() {

    IMG_Quit();

    SDL_QuitSubSystem(SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER);

    TTF_Quit();

    SDL_DestroyRenderer(GetSDLRenderer());
    SDL_DestroyWindow(GetSDLWindow());
    SDL_Quit();

    return 0;
  }

  Uint32 GetPixel(SDL_Surface *surface, int x, int y) {
    int bpp = surface->format->BytesPerPixel;
    assert(bpp == 4);
    return *((Uint8 *)surface->pixels + y * surface->pitch + x * bpp);
  }

  SDL_Texture *LoadSDLTexture(const std::string &filename, e_FilterMode filterMode, int *w, int *h) {

    //printf("loading %s..\n", filename.c_str());
    SDL_Surface *surface = IMG_Load(filename.c_str());
    if (surface == nullptr) {
      printf("could not load texture file %s\n", filename.c_str());
      exit(EXIT_FAILURE);
    }
    if (w) *w = surface->w;
    if (h) *h = surface->h;

    std::string filterStr = "0";
    if (filterMode == e_FilterMode_Nearest) filterStr = "0";
    else if (filterMode == e_FilterMode_Linear) filterStr = "1";
    else if (filterMode == e_FilterMode_Anisotropic) filterStr = "2";
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, filterStr.c_str());

    SDL_Texture *texture = SDL_CreateTextureFromSurface(GetSDLRenderer(), surface);
    if (texture == nullptr) {
      printf("could not convert surface to texture\n");
      exit(EXIT_FAILURE);
    }
    SDL_FreeSurface(surface);

    return texture;
  }

  SDL_Texture *RenderText(TTF_Font *font, const std::string &text, const SDL_Color &color, e_FilterMode filterMode, int *w, int *h) {

    SDL_Surface *surface = nullptr;
    if (filterMode == e_FilterMode_Nearest) {
      surface = TTF_RenderUTF8_Solid(font, text.c_str(), color);
    } else if (filterMode == e_FilterMode_Linear) {
      surface = TTF_RenderUTF8_Blended(font, text.c_str(), color);
    }
    if (surface == nullptr) {
      printf("could not render text \"%s\"\n", text.c_str());
      exit(EXIT_FAILURE);
    }
    if (w) *w = surface->w;
    if (h) *h = surface->h;

    SDL_Texture *texture = SDL_CreateTextureFromSurface(GetSDLRenderer(), surface);
    if (texture == nullptr) {
      printf("could not convert surface to texture\n");
      exit(EXIT_FAILURE);
    }
    SDL_FreeSurface(surface);

    return texture;
  }

  Vector2 WorldToScreenVector(const Vector2 &worldVector, const Viewport &viewport, const Camera &camera) {
    Vector2 screenVector = worldVector;
    float aspectRatio = viewport.w / (float)viewport.h;
    screenVector -= camera.spatial.position;
    screenVector.Rotate(-camera.spatial.orientation); // new
    screenVector /= camera.spatial.z * tan(camera.fov / 2.0f);
    screenVector *= camera.spatial.scale; // new
    screenVector *= Vector2(1.0f / aspectRatio, 1.0f);
    screenVector += 0.5f;
    screenVector *= Vector2(viewport.w, viewport.h);
    return screenVector;
  }

  Spatial WorldToScreenSpatial(const Spatial &worldSpatial, const Viewport &viewport, const Camera &camera) {
    Spatial screenSpatial = worldSpatial;

    float aspectRatio = viewport.w / (float)viewport.h;
/*
    screenSpatial.position = -worldSpatial.rotationCenter; // to rotation center space
    screenSpatial.position.Rotate(worldSpatial.orientation);
    screenSpatial.position += worldSpatial.rotationCenter; // and restore

    screenSpatial.position += worldSpatial.position;
*/
    // to camera view space
    screenSpatial.position -= camera.spatial.position;
    screenSpatial.position.Rotate(-camera.spatial.orientation);
    screenSpatial.orientation -= camera.spatial.orientation;

    // to camera projection
    //printf("%f /= %f * %f\n", screenSpatial.position.coords[0], camera.distance, tan(camera.fov / 2.0f));
    screenSpatial.position /= (camera.spatial.z + -worldSpatial.z) * tan(camera.fov / 2.0f);
    screenSpatial.position *= camera.spatial.scale; // new
    screenSpatial.scale /= (camera.spatial.z + -worldSpatial.z) * tan(camera.fov / 2.0f);

    //screenSpatial.scale *= camera.spatial.scale; // new

    // to viewport space
    screenSpatial.position *= Vector2(1.0f / aspectRatio, 1.0f);
    screenSpatial.position += 0.5f;
    screenSpatial.position *= Vector2(viewport.w, viewport.h);
    screenSpatial.scale *= Vector2(viewport.w / aspectRatio, viewport.h); // scale by viewport height; width can be variable, depending on aspect ratio

    //screenSpatial.scale *= camera.spatial.scale; // new

    //screenSpatial.position.Print();
    return screenSpatial;
  }

}
