// SPDX-License-Identifier: Unlicense

#include "stage.h"

#ifdef SHR3D_ENVIRONMENT_STAGE

#include "file.h"
#include "global.h"
#include "opengl.h"
#include "shader.h"
#include "string_.h"
#include "texture.h"

#include "geometry.h"

void Stage::init()
{
  const std::vector<std::u8string> modelFilepaths = File::filesInDirectory(Settings::pathStage.c_str());

  for (const std::u8string& filepath : modelFilepaths)
  {
    if (File::extension(filepath.c_str()) != std::u8string(
#ifdef __ANDROID__
      u8".stage_a"
#else // __ANDROID__
      u8".stage"
#endif //  __ANDROID__
    ))
      continue;

    {
      const std::u8string modelName = File::filename(filepath.c_str());
      Global::environmentStageNames.push_back(modelName);
    }
  }
}

void Stage::tick()
{
  static std::u8string activeModel;

  if (activeModel == Settings::environmentStage)
    return;

  activeModel = Settings::environmentStage;

  if (Global::environmentStageIndexData.size() != 0)
  {
    Global::environmentStageIndexData.clear();
    Global::environmentStageVertexData.clear();
    GL(glDeleteTextures(Global::modelTexture.size(), Global::modelTexture.data()));
    Global::modelTexture.clear();
  }

  if (!Settings::environmentStage.empty())
  {
    const std::u8string filepath = Settings::pathStage + Settings::environmentStage;

    if (File::exists(filepath.c_str()))
    {
      FILE* file = File::fopen_u8(filepath.c_str(), false);
      ASSERT(file != nullptr);

      struct
      {
        char magic[4];
        u32 version;
        u32 objectCount;
        u32 totalVertexDataSize;
        u32 totalIndexDataSize;
        //u32 totalTextureDataSize;

        uint32_t vertexDataOffset;
        uint32_t indexDataOffset;
      } header;
      fread(&header, sizeof(header), 1, file);

      ASSERT(header.magic[0] == 'S');
      ASSERT(header.magic[1] == '3');
      ASSERT(header.magic[2] == 'D');
      ASSERT(header.magic[3] == 'S');
      ASSERT(header.version == 2);

      struct TextureHeader
      {
        u32 textureDataOffset;
        u32 textureDataSize;
      };
      std::vector<TextureHeader> textureHeader(header.objectCount);

      for (i32 i = 0; i < i32(header.objectCount); ++i)
      {
        Global::environmentStageModels.resize(header.objectCount);
        fread(&Global::environmentStageModels[i], sizeof(StageModel), 1, file);
        fread(&textureHeader[i], sizeof(TextureHeader), 1, file);
      }

      GL(glGenVertexArrays(1, &Global::stageDrawVao));
      GL(glBindVertexArray(Global::stageDrawVao));

      { // Vertex
        fseek(file, header.vertexDataOffset, SEEK_SET);
        Global::environmentStageVertexData.resize(header.totalVertexDataSize / sizeof(f32));
        fread(Global::environmentStageVertexData.data(), header.totalVertexDataSize, 1, file);


        GLuint vbo;
        GL(glGenBuffers(1, &vbo));
        GL(glBindBuffer(GL_ARRAY_BUFFER, vbo));
        GL(glBufferData(GL_ARRAY_BUFFER, header.totalVertexDataSize, Global::environmentStageVertexData.data(), GL_STATIC_DRAW));
      }


      GL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), nullptr)); // vertex coords
      GL(glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)))); // uv coords
      GL(glEnableVertexAttribArray(0));
      GL(glEnableVertexAttribArray(1));

      { // Indicies
        fseek(file, header.indexDataOffset, SEEK_SET);
        Global::environmentStageIndexData.resize(header.totalIndexDataSize / sizeof(u32));
        fread(Global::environmentStageIndexData.data(), header.totalIndexDataSize, 1, file);

        GLuint ebo;
        GL(glGenBuffers(1, &ebo));
        GL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo));
        GL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, header.totalIndexDataSize, Global::environmentStageIndexData.data(), GL_STATIC_DRAW));
      }

      Global::modelTexture.resize(textureHeader.size());
      for (u32 j = 0; j < textureHeader.size(); ++j)
      {
        { // Texture
          fseek(file, textureHeader[j].textureDataOffset, SEEK_SET);
          std::vector<u8> textureData(textureHeader[j].textureDataSize);
          fread(textureData.data(), textureHeader[j].textureDataSize, 1, file);

          Global::modelTexture[j] = Texture::openGlLoadTexture(textureData.data(), textureHeader[j].textureDataSize);
          ASSERT(Global::modelTexture[j] != 0);
        }
      }
      fclose(file);

      GL(glBindVertexArray(Global::dynamicDrawVao));
      GL(glBindBuffer(GL_ARRAY_BUFFER, Global::vbo));
    }
  }
}

void Stage::render(const mat4& viewProjectionMat)
{
  ASSERT(!Settings::environmentStage.empty());

  GL(glBindVertexArray(Global::stageDrawVao));
  GL(glUseProgram(Shader::modelUvNotFlipped));

  for (i32 i = 0; i < i32(Global::environmentStageModels.size()); ++i)
  {
    if (Global::environmentStageVertexData.size() == 0)
      continue;

    mat4 trans{
      .m03 = Global::environmentStageModels[i].translation.x + Settings::environmentStageX,
      .m13 = Global::environmentStageModels[i].translation.y + Settings::environmentStageY,
      .m23 = Global::environmentStageModels[i].translation.z + Settings::environmentStageZ,
    };
#ifndef PLATFORM_OPENXR_ANDROID
#ifdef SHR3D_OPENXR_PCVR
    if (!Global::xrInitialized)
#endif // SHR3D_OPENXR_PCVR
    {
      trans.m13 -= Settings::environmentStagePlayerHeight;
    }
#endif // PLATFORM_OPENXR_ANDROID
    const mat4 rot = vec::createMatFromQuaternion(Global::environmentStageModels[i].rotation);
    const f32 rotation = Settings::environmentStageRotation * 2.0f * PI_;
    const mat4 rot2{
      .m00 = cosf(rotation),
      .m20 = sinf(rotation),
      .m02 = -sinf(rotation),
      .m22 = cosf(rotation)
    };
    const mat4 scale{
      .m00 = Global::environmentStageModels[i].scale.x * Settings::environmentStageScale,
      .m11 = Global::environmentStageModels[i].scale.y * Settings::environmentStageScale,
      .m22 = Global::environmentStageModels[i].scale.z * Settings::environmentStageScale
    };

    const mat4 model = vec::multiply(vec::multiply(trans, vec::multiply(rot, rot2)), scale);

    const mat4 mvp = vec::multiply(viewProjectionMat, model);

    GL(glUniformMatrix4fv(Shader::modelUvNotFlippedUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    GL(glBindTexture(GL_TEXTURE_2D, Global::modelTexture[i]));
    GL(glDrawElements(GL_TRIANGLES, Global::environmentStageModels[i].indexCount, GL_UNSIGNED_INT, reinterpret_cast<void*>(Global::environmentStageModels[i].startIndex * sizeof(GLuint))));
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

#endif // SHR3D_ENVIRONMENT_STAGE
