#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <ostream>

#if defined(__APPLE__)
#include <SDL2/SDL.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL2/SDL_opengles.h>
#else
#include <SDL2/SDL_opengl.h>
#endif
#else
#include <SDL.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL_opengles2.h>
#else
#include <SDL_opengl.h>
#endif
#endif

#include "../ImNodeFlow.h"
#include "../image_model.h"
#include "../imfilebrowser.h"
#include "../stb_image.h"
#include "../uuid.h"

#ifdef __EMSCRIPTEN__
#include "../emscripten_browser_file.h"
#endif

struct Texture {
  ImTextureID texture;
  ImVec2 size;
};

struct LoadCallback {
  std::string filename;
  std::string mime_type;
  std::string_view buffer;
};

class Exposure : public ImFlow::BaseNode {
public:
  ImageBundle returnBundle;
  Exposure() {
    setTitle("Exposure");
    setStyle(ImFlow::NodeStyle::green());
    addIN<ImageBundle>("Image", ImageBundle());
    addOUT<ImageBundle>("Image Out")->behaviour([this]() {
      return returnBundle;
    });
  }

  ~Exposure() {
    if (returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
    }
  }

  void draw() override {
    ImGui::SetNextItemWidth(100);
    ImGui::SliderInt("", &brightnessVal, -255, 255);
    ImageBundle val = getInVal<ImageBundle>("Image");

    // If the input is valid, and out buffers are not
    if (val.loaded && !returnBundle.loaded) {
      int img_size = val.width * val.height * val.channels;
      uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
      memcpy(duplicated_img, val.image, img_size);
      returnBundle.image = duplicated_img;
      returnBundle.height = val.height;
      returnBundle.width = val.width;
      returnBundle.channels = val.channels;
      returnBundle.loaded = true;
      returnBundle.fname = uuid::generate_uuid_v4();
      fname = val.fname;
      compute(val.image, returnBundle.image, img_size, returnBundle.channels);
    }

    // If the input is invalid, and are buffers are valid
    if (!val.loaded && returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
      returnBundle.loaded = false;
    }

    // If the input has changed
    if (val.fname != fname) {
      int img_size = val.width * val.height * val.channels;
      int old_img_size =
          returnBundle.width * returnBundle.height * returnBundle.channels;
      if (img_size == old_img_size) {
        memcpy(returnBundle.image, val.image, img_size);
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      } else {
        stbi_image_free(returnBundle.image);
        uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
        memcpy(duplicated_img, val.image, img_size);
        returnBundle.image = duplicated_img;
        returnBundle.height = val.height;
        returnBundle.width = val.width;
        returnBundle.channels = val.channels;
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      }
    }

    if (returnBundle.loaded) {
      if (brightnessVal != prev_brightness) {
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
        prev_brightness = brightnessVal;
        returnBundle.fname = uuid::generate_uuid_v4();
      }
    }
  }

private:
  static int formula(uint8_t color, int val) {
    float vf = static_cast<float>(val) / 255.0;
    vf = vf + 1.0;
    float cf = static_cast<float>(color) / 255.0;
    float ret_val = (cf * vf) * 255.0f;
    return static_cast<int>(ret_val);
  }

  void compute(uint8_t *input, uint8_t *output, int size, int channels) {
    for (int i = 0; i < size; i += channels) {
      uint8_t r = std::clamp(formula(input[i], brightnessVal), 0, 255);
      uint8_t g = std::clamp(formula(input[i + 1], brightnessVal), 0, 255);
      uint8_t b = std::clamp(formula(input[i + 2], brightnessVal), 0, 255);
      output[i] = r;
      output[i + 1] = g;
      output[i + 2] = b;
    }
  }

  int brightnessVal = 0;
  int prev_brightness = 0;
  std::string fname;
};

class Contrast : public ImFlow::BaseNode {
public:
  ImageBundle returnBundle;
  Contrast() {
    setTitle("Contrast");
    setStyle(ImFlow::NodeStyle::green());
    addIN<ImageBundle>("Image", ImageBundle());
    addOUT<ImageBundle>("Image Out")->behaviour([this]() {
      return returnBundle;
    });
  }

  ~Contrast() {
    if (returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
    }
  }

  void draw() override {
    ImGui::SetNextItemWidth(100);
    ImGui::SliderInt("", &brightnessVal, -255, 255);
    ImageBundle val = getInVal<ImageBundle>("Image");

    // If the input is valid, and out buffers are not
    if (val.loaded && !returnBundle.loaded) {
      int img_size = val.width * val.height * val.channels;
      uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
      memcpy(duplicated_img, val.image, img_size);
      returnBundle.image = duplicated_img;
      returnBundle.height = val.height;
      returnBundle.width = val.width;
      returnBundle.channels = val.channels;
      returnBundle.loaded = true;
      returnBundle.fname = uuid::generate_uuid_v4();
      fname = val.fname;
      compute(val.image, returnBundle.image, img_size, returnBundle.channels);
    }

    // If the input is invalid, and are buffers are valid
    if (!val.loaded && returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
      returnBundle.loaded = false;
    }

    // If the input has changed
    if (val.fname != fname) {
      int img_size = val.width * val.height * val.channels;
      int old_img_size =
          returnBundle.width * returnBundle.height * returnBundle.channels;
      if (img_size == old_img_size) {
        memcpy(returnBundle.image, val.image, img_size);
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      } else {
        stbi_image_free(returnBundle.image);
        uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
        memcpy(duplicated_img, val.image, img_size);
        returnBundle.image = duplicated_img;
        returnBundle.height = val.height;
        returnBundle.width = val.width;
        returnBundle.channels = val.channels;
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      }
    }

    if (returnBundle.loaded) {
      if (brightnessVal != prev_brightness) {
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
        prev_brightness = brightnessVal;
        returnBundle.fname = uuid::generate_uuid_v4();
      }
    }
  }

private:
  static int formula(uint8_t color, int val) {
    float vf = static_cast<float>(val) / 255.0;
    vf = vf + 1.0;
    float cf = static_cast<float>(color) / 255.0;
    float ret_val = (((cf - 0.5f) * vf) + 0.5f) * 255.0f;
    return static_cast<int>(ret_val);
  }

  void compute(uint8_t *input, uint8_t *output, int size, int channels) {
    int nBval = brightnessVal + 256;
    for (int i = 0; i < size; i += channels) {
      uint8_t r = std::clamp(formula(input[i], brightnessVal), 0, 255);
      uint8_t g = std::clamp(formula(input[i + 1], brightnessVal), 0, 255);
      uint8_t b = std::clamp(formula(input[i + 2], brightnessVal), 0, 255);
      output[i] = r;
      output[i + 1] = g;
      output[i + 2] = b;
    }
  }

  int brightnessVal = 0;
  int prev_brightness = 0;
  std::string fname;
};

class SimpleBrightness : public ImFlow::BaseNode {
public:
  ImageBundle returnBundle;
  SimpleBrightness() {
    setTitle("Simple Brightness");
    setStyle(ImFlow::NodeStyle::green());
    addIN<ImageBundle>("Image", ImageBundle());
    addOUT<ImageBundle>("Image Out")->behaviour([this]() {
      return returnBundle;
    });
  }

  ~SimpleBrightness() {
    if (returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
    }
  }

  void draw() override {
    ImGui::SetNextItemWidth(100);
    ImGui::SliderInt("", &brightnessVal, -255, 255);
    ImageBundle val = getInVal<ImageBundle>("Image");

    // If the input is valid, and out buffers are not
    if (val.loaded && !returnBundle.loaded) {
      int img_size = val.width * val.height * val.channels;
      uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
      memcpy(duplicated_img, val.image, img_size);
      returnBundle.image = duplicated_img;
      returnBundle.height = val.height;
      returnBundle.width = val.width;
      returnBundle.channels = val.channels;
      returnBundle.loaded = true;
      returnBundle.fname = uuid::generate_uuid_v4();
      fname = val.fname;
      compute(val.image, returnBundle.image, img_size, returnBundle.channels);
    }

    // If the input is invalid, and are buffers are valid
    if (!val.loaded && returnBundle.loaded) {
      stbi_image_free(returnBundle.image);
      returnBundle.loaded = false;
    }

    // If the input has changed
    if (val.fname != fname) {
      int img_size = val.width * val.height * val.channels;
      int old_img_size =
          returnBundle.width * returnBundle.height * returnBundle.channels;
      if (img_size == old_img_size) {
        memcpy(returnBundle.image, val.image, img_size);
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      } else {
        stbi_image_free(returnBundle.image);
        uint8_t *duplicated_img = (uint8_t *)malloc(img_size);
        memcpy(duplicated_img, val.image, img_size);
        returnBundle.image = duplicated_img;
        returnBundle.height = val.height;
        returnBundle.width = val.width;
        returnBundle.channels = val.channels;
        returnBundle.fname = uuid::generate_uuid_v4();
        fname = val.fname;
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
      }
    }

    if (returnBundle.loaded) {
      if (brightnessVal != prev_brightness) {
        int img_size =
            returnBundle.width * returnBundle.height * returnBundle.channels;
        compute(val.image, returnBundle.image, img_size, returnBundle.channels);
        prev_brightness = brightnessVal;
        returnBundle.fname = uuid::generate_uuid_v4();
      }
    }
  }

private:
  void compute(uint8_t *input, uint8_t *output, int size, int channels) {
    for (int i = 0; i < size; i += channels) {
      uint8_t r = std::clamp(input[i] + brightnessVal, 0, 255);
      uint8_t g = std::clamp(input[i + 1] + brightnessVal, 0, 255);
      uint8_t b = std::clamp(input[i + 2] + brightnessVal, 0, 255);
      output[i] = r;
      output[i + 1] = g;
      output[i + 2] = b;
    }
  }

  int brightnessVal = 0;
  int prev_brightness = 0;
  std::string fname;
};

class PreviewImage : public ImFlow::BaseNode {
public:
  PreviewImage() {
    setTitle("Output");
    setStyle(ImFlow::NodeStyle::cyan());
    addIN<ImageBundle>("Image", ImageBundle());
  }

  ~PreviewImage() {
    if (loaded) {
      glDeleteTextures(1, &txHandle);
    }
  }

  void draw() override {
    ImageBundle val = getInVal<ImageBundle>("Image");
    if (val.loaded && !loaded) {
      GLuint textureHandle;
      glGenTextures(1, &textureHandle);
      glBindTexture(GL_TEXTURE_2D, textureHandle);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
                      GL_CLAMP_TO_EDGE); // This is required on WebGL for non
                                         // power-of-two textures
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
                      GL_CLAMP_TO_EDGE); // Same
#if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
      glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
#endif
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, val.width, val.height, 0, GL_RGBA,
                   GL_UNSIGNED_BYTE, val.image);

      txHandle = textureHandle;
      id = val.fname;
      loaded = true;
      prev_h = val.height;
      prev_w = val.width;
    }
    if (!val.loaded && loaded) {
      glDeleteTextures(1, &txHandle);
      loaded = false;
    }

    if ((val.fname != id) && loaded) {
      if (prev_w != val.width || prev_h != val.height) {
        glDeleteTextures(1, &txHandle);
        GLuint textureHandle;
        glGenTextures(1, &textureHandle);
        glBindTexture(GL_TEXTURE_2D, textureHandle);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
                        GL_CLAMP_TO_EDGE); // This is required on WebGL for non
                                           // power-of-two textures
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
                        GL_CLAMP_TO_EDGE); // Same
#if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
        glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
#endif
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, val.width, val.height, 0,
                     GL_RGBA, GL_UNSIGNED_BYTE, val.image);

        txHandle = textureHandle;

      } else {
        glBindTexture(GL_TEXTURE_2D, txHandle);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, val.width, val.height, GL_RGBA,
                        GL_UNSIGNED_BYTE, val.image);
        glBindTexture(GL_TEXTURE_2D, 0);
      }
      prev_h = val.height;
      prev_w = val.width;
      id = val.fname;
    }

    if (loaded) {
      if (val.height < val.width) {
        ImGui::Image((void *)(intptr_t)txHandle,
                     ImVec2(256, val.calcResizedWidth(256)));
      } else {
        ImGui::Image((void *)(intptr_t)txHandle,
                     ImVec2(val.calcResizedHeight(256), 256));
      }
    }
  }

private:
  int prev_h = 0;
  int prev_w = 0;
  bool loaded = false;
  GLuint txHandle;
  std::string id;
};

class LoadImage : public ImFlow::BaseNode {
public:
  ImageBundle bundle;

  LoadImage() {
    setTitle("Load Image");
    setStyle(ImFlow::NodeStyle::brown());
    addOUT<ImageBundle>("Image")->behaviour([this]() { return bundle; });
  }

  ~LoadImage() {
    if (bundle.loaded) {
      stbi_image_free(bundle.image);
    }
  }

  void draw() override {
    LoadCallback lc = LoadCallback();
#ifdef __EMSCRIPTEN__
    if (ImGui::Button("Select File")) {
      emscripten_browser_file::upload(".png,.jpg,.jpeg", handle_upload_file,
                                      this);
    }
#else
    if (bundle.fname == "") {
      handle_upload_file_local(
          "/Users/tanishqdubey/Downloads/Odd-eyed_cat_by_ihasb33r.jpg", this);
    }
#endif
    if (bundle.fname != "") {
      bundle.loaded = true;
      ImGui::Text("File Loaded");
    } else {
      ImGui::Text("No image loaded");
    }
  }

private:
  bool showPreview = false;

  static void handle_upload_file_local(const char *filename, void *callback) {
    auto lc{reinterpret_cast<LoadImage *>(callback)};
    const int channelCount = 4;
    int imageFileChannelCount;
    int width, height;

    uint8_t *image = (uint8_t *)stbi_load(filename, &width, &height,
                                          &imageFileChannelCount, channelCount);

    if (image == NULL) {
      fprintf(stderr, "%s\nFailed to open %s\n", stbi_failure_reason(),
              lc->bundle.fname.c_str());
      lc->bundle.fname = "";
    }

    lc->bundle.fname = uuid::generate_uuid_v4();
    lc->bundle.width = width;
    lc->bundle.height = height;
    lc->bundle.channels = channelCount;
    lc->bundle.image = image;
    lc->bundle.loaded = true;
  }

  static void handle_upload_file(std::string const &filename,
                                 std::string const &mime_type,
                                 std::string_view buffer, void *callback) {
    auto lc{reinterpret_cast<LoadImage *>(callback)};
    const int channelCount = 4;
    int imageFileChannelCount;
    int width, height;

    auto d = buffer.data();
    stbi_uc *image = (stbi_uc *)stbi_load_from_memory(
        (const stbi_uc *)buffer.data(), buffer.size(), &width, &height,
        &imageFileChannelCount, channelCount);

    if (image == NULL) {
      fprintf(stderr, "%s\nFailed to open %s - %s\n", stbi_failure_reason(),
              filename.c_str(), mime_type.c_str());
      lc->bundle.fname = "";
    }

    lc->bundle.fname = uuid::generate_uuid_v4();
    lc->bundle.width = width;
    lc->bundle.height = height;
    lc->bundle.channels = channelCount;
    lc->bundle.image = image;
    lc->bundle.loaded = true;
  }
};
