// Copyright (c) 2022 Dominic Masters
// 
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

#include "Texture.hpp"

using namespace Dawn;

Texture::Texture() : ITexture() {
  useEffect([&]{
    this->texturePropertiesNeedUpdating = true;
  }, { 
    &this->wrapModeX, &this->wrapModeY,
    &this->filterModeMin, &this->filterModeMag,
    &this->mipmapFilterModeMin, &this->mipmapFilterModeMag
  });
}

void Texture::bind(textureslot_t slot) {
  assertTrue(this->id != -1);
  glActiveTexture(GL_TEXTURE0 + slot);
  glBindTexture(GL_TEXTURE_2D, this->id);

  if(this->texturePropertiesNeedUpdating) {
    this->updateTextureProperties();
    this->texturePropertiesNeedUpdating = false;
  }
}

int32_t Texture::getWidth() {
  return this->width;
}

int32_t Texture::getHeight() {
  return this->height;
}

void Texture::setSize(int32_t width, int32_t height, enum TextureFormat format) {
  if(this->id != -1) {
    glDeleteTextures(1, &this->id);
    this->id = -1;
  }

  this->width = width;
  this->height = height;
  this->format = format;

  glGenTextures(1, &this->id);
  if(this->id <= 0) throw "Texture generation failed!";
  
  // Initialize the texture to blank
  glActiveTexture(GL_TEXTURE0);
  this->bufferRaw(NULL);
}

void Texture::fill(struct Color color) {
  struct Color *pixels = (struct Color *)memoryAllocate(
    sizeof(struct Color) * this->width * this->height
  );
  this->buffer(pixels);
  memoryFree(pixels);
}

bool_t Texture::isReady() {
  return this->id != -1;
}

void Texture::updateTextureProperties() {
  auto setWrapMode = [&](GLenum axis, enum TextureWrapMode wm) {
    switch(wm) {
      case TEXTURE_WRAP_MODE_REPEAT:
        glTexParameteri(GL_TEXTURE_2D, axis, GL_REPEAT);
        break;
      
      case TEXTURE_WRAP_MODE_MIRRORED_REPEAT:
        glTexParameteri(GL_TEXTURE_2D, axis, GL_MIRRORED_REPEAT);
        break;

      case TEXTURE_WRAP_MODE_CLAMP_TO_EDGE:
        glTexParameteri(GL_TEXTURE_2D, axis, GL_CLAMP_TO_EDGE);
        break;

      case TEXTURE_WRAP_MODE_CLAMP_TO_BORDER:
        glTexParameteri(GL_TEXTURE_2D, axis, GL_CLAMP_TO_BORDER);
        break;

      default:
        assertUnreachable();
    }
  };

  setWrapMode(GL_TEXTURE_WRAP_S, this->wrapModeX);
  setWrapMode(GL_TEXTURE_WRAP_T, this->wrapModeY);

  auto setFilterMode = [&](
    GLenum minMag,
    enum TextureFilterMode filter,
    enum TextureFilterMode mapFilterMode
  ) {
    switch(filter) {
      case TEXTURE_FILTER_MODE_NEAREST: {
        switch(mapFilterMode) {
          case TEXTURE_FILTER_MODE_NEAREST:
            glTexParameteri(GL_TEXTURE_2D, minMag, GL_NEAREST_MIPMAP_NEAREST);
            break;

          case TEXTURE_FILTER_MODE_LINEAR:
            glTexParameteri(GL_TEXTURE_2D, minMag, GL_LINEAR_MIPMAP_NEAREST);
            break;

          default:
            assertUnreachable();
        }
        break;
      }
      
      case TEXTURE_FILTER_MODE_LINEAR: {
        switch(mapFilterMode) {
          case TEXTURE_FILTER_MODE_NEAREST:
            glTexParameteri(GL_TEXTURE_2D, minMag, GL_NEAREST_MIPMAP_LINEAR);
            break;

          case TEXTURE_FILTER_MODE_LINEAR:
            glTexParameteri(GL_TEXTURE_2D, minMag, GL_LINEAR_MIPMAP_LINEAR);
            break;

          default:
            assertUnreachable();
        }
        break;
      }

      default: {
        assertUnreachable();
      }
    }
  };

  setFilterMode(GL_TEXTURE_MIN_FILTER, this->filterModeMin, this->mipmapFilterModeMin);
  setFilterMode(GL_TEXTURE_MAG_FILTER, this->filterModeMag, this->mipmapFilterModeMag);
}

void Texture::bufferRaw(void *data) {
  assertTrue(this->isReady());
  glBindTexture(GL_TEXTURE_2D, this->id);

  GLenum format;
  switch(this->format) {
    case TEXTURE_FORMAT_R:
      format = GL_RED;
      break;

    case TEXTURE_FORMAT_RG:
      format = GL_RG;
      break;

    case TEXTURE_FORMAT_RGB:
      format = GL_RGB;
      break;

    case TEXTURE_FORMAT_RGBA:
      format = GL_RGBA;
      break;
    
    default:
      assertUnreachable();
  }

  glTexImage2D(
    GL_TEXTURE_2D, 0, format,
    this->width, this->height,
    0, format, GL_UNSIGNED_BYTE, data
  );
  glGenerateMipmap(GL_TEXTURE_2D);
  this->texturePropertiesNeedUpdating = true;
}

void Texture::buffer(struct Color pixels[]) {
  this->bufferRaw((void*)pixels);
}

void Texture::buffer(uint8_t pixels[]) {
  this->bufferRaw((void*)pixels);
}

Texture::~Texture() {
  if(this->id != -1) glDeleteTextures(1, &this->id);
}