// Copyright 2021 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "uitools.h"

#include <stdio.h>
#include <string.h>

#include <GLFW/glfw3.h>
#include "glfw_dispatch.h"

namespace {
using ::mujoco::Glfw;
}

//------------------------------------ internal GLFW callbacks -------------------------------------

// update state
static void uiUpdateState(GLFWwindow* wnd) {
  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // mouse buttons
  state->left =   (Glfw().glfwGetMouseButton(wnd, GLFW_MOUSE_BUTTON_LEFT)==GLFW_PRESS);
  state->right =  (Glfw().glfwGetMouseButton(wnd, GLFW_MOUSE_BUTTON_RIGHT)==GLFW_PRESS);
  state->middle = (Glfw().glfwGetMouseButton(wnd, GLFW_MOUSE_BUTTON_MIDDLE)==GLFW_PRESS);

  // keyboard modifiers
  state->control = (Glfw().glfwGetKey(wnd, GLFW_KEY_LEFT_CONTROL)==GLFW_PRESS ||
                    Glfw().glfwGetKey(wnd, GLFW_KEY_RIGHT_CONTROL)==GLFW_PRESS);
  state->shift =   (Glfw().glfwGetKey(wnd, GLFW_KEY_LEFT_SHIFT)==GLFW_PRESS ||
                    Glfw().glfwGetKey(wnd, GLFW_KEY_RIGHT_SHIFT)==GLFW_PRESS);
  state->alt =     (Glfw().glfwGetKey(wnd, GLFW_KEY_LEFT_ALT)==GLFW_PRESS ||
                    Glfw().glfwGetKey(wnd, GLFW_KEY_RIGHT_ALT)==GLFW_PRESS);

  // swap left and right if Alt
  if (state->alt) {
    int tmp = state->left;
    state->left = state->right;
    state->right = tmp;
  }

  // get mouse position, scale by buffer-to-window ratio
  double x, y;
  Glfw().glfwGetCursorPos(wnd, &x, &y);
  x *= ptr->buffer2window;
  y *= ptr->buffer2window;

  // invert y to match OpenGL convention
  y = state->rect[0].height - y;

  // save
  state->dx = x - state->x;
  state->dy = y - state->y;
  state->x = x;
  state->y = y;

  // find mouse rectangle
  state->mouserect = mjr_findRect(mju_round(x), mju_round(y), state->nrect-1, state->rect+1) + 1;
}



// keyboard
static void uiKeyboard(GLFWwindow* wnd, int key, int scancode, int act, int mods) {
  // release: nothing to do
  if (act==GLFW_RELEASE) {
    return;
  }

  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // update state
  uiUpdateState(wnd);

  // set key info
  state->type = mjEVENT_KEY;
  state->key = key;
  state->keytime = Glfw().glfwGetTime();

  // application-specific processing
  ptr->uiEvent(state);
}



// mouse button
static void uiMouseButton(GLFWwindow* wnd, int button, int act, int mods) {
  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // update state
  uiUpdateState(wnd);

  // translate button
  if (button==GLFW_MOUSE_BUTTON_LEFT) {
    button = mjBUTTON_LEFT;
  } else if (button==GLFW_MOUSE_BUTTON_RIGHT) {
    button = mjBUTTON_RIGHT;
  } else {
    button = mjBUTTON_MIDDLE;
  }

  // swap left and right if Alt
  if (Glfw().glfwGetKey(wnd, GLFW_KEY_LEFT_ALT)==GLFW_PRESS ||
      Glfw().glfwGetKey(wnd, GLFW_KEY_RIGHT_ALT)==GLFW_PRESS) {
    if (button==mjBUTTON_LEFT) {
      button = mjBUTTON_RIGHT;
    } else if (button==mjBUTTON_RIGHT) {
      button = mjBUTTON_LEFT;
    }
  }

  // press
  if (act==GLFW_PRESS) {
    // detect doubleclick: 250 ms
    if (button==state->button && Glfw().glfwGetTime()-state->buttontime<0.25) {
      state->doubleclick = 1;
    } else {
      state->doubleclick = 0;
    }

    // set info
    state->type = mjEVENT_PRESS;
    state->button = button;
    state->buttontime = Glfw().glfwGetTime();

    // start dragging
    if (state->mouserect) {
      state->dragbutton = state->button;
      state->dragrect = state->mouserect;
    }
  }

  // release
  else {
    state->type = mjEVENT_RELEASE;
  }

  // application-specific processing
  ptr->uiEvent(state);

  // stop dragging after application processing
  if (state->type==mjEVENT_RELEASE) {
    state->dragrect = 0;
    state->dragbutton = 0;
  }
}



// mouse move
static void uiMouseMove(GLFWwindow* wnd, double xpos, double ypos) {
  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // no buttons down: nothing to do
  if (!state->left && !state->right && !state->middle) {
    return;
  }

  // update state
  uiUpdateState(wnd);

  // set move info
  state->type = mjEVENT_MOVE;

  // application-specific processing
  ptr->uiEvent(state);
}



// scroll
static void uiScroll(GLFWwindow* wnd, double xoffset, double yoffset) {
  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // update state
  uiUpdateState(wnd);

  // set scroll info, scale by buffer-to-window ratio
  state->type = mjEVENT_SCROLL;
  state->sx = xoffset * ptr->buffer2window;
  state->sy = yoffset * ptr->buffer2window;

  // application-specific processing
  ptr->uiEvent(state);
}



// resize
static void uiResize(GLFWwindow* wnd, int width, int height) {
  // extract data from user pointer
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;

  // set layout
  ptr->uiLayout(state);

  // update state
  uiUpdateState(wnd);

  // set resize info
  state->type = mjEVENT_RESIZE;

  // stop dragging
  state->dragbutton = 0;
  state->dragrect = 0;

  // application-specific processing (unless called with 0,0 from uiModify)
  if (width && height) {
    ptr->uiEvent(state);
  }
}


static void uiRender(GLFWwindow* wnd) {
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;
  ptr->uiRender(state);
}

static void uiDrop(GLFWwindow* wnd, int count, const char** paths) {
  uiUserPointer* ptr = (uiUserPointer*)Glfw().glfwGetWindowUserPointer(wnd);
  mjuiState* state = ptr->state;
  ptr->uiDrop(state, count, paths);
}

//------------------------------------------- public API -------------------------------------------

// Compute suitable font scale.
int uiFontScale(GLFWwindow* wnd) {
  // compute framebuffer-to-window ratio
  int width_win, width_buf, height;
  Glfw().glfwGetWindowSize(wnd, &width_win, &height);
  Glfw().glfwGetFramebufferSize(wnd, &width_buf, &height);
  double b2w = (double)width_buf / (double)width_win;

  // compute PPI
  int width_MM, height_MM;
  Glfw().glfwGetMonitorPhysicalSize(Glfw().glfwGetPrimaryMonitor(), &width_MM, &height_MM);
  int width_vmode = Glfw().glfwGetVideoMode(Glfw().glfwGetPrimaryMonitor())->width;
  double PPI = 25.4 * b2w * (double)width_vmode / (double)width_MM;

  // estimate font scaling, guard against unrealistic PPI
  int fs;
  if (width_buf>width_win) {
    fs = mju_round(b2w * 100);
  } else if (PPI>50 && PPI<350) {
    fs = mju_round(PPI);
  } else {
    fs = 150;
  }
  fs = mju_round(fs * 0.02) * 50;
  fs = mjMIN(300, mjMAX(100, fs));

  return fs;
}



// Set internal and user-supplied UI callbacks in GLFW window.
void uiSetCallback(GLFWwindow* wnd, mjuiState* state,
                   uiEventFn uiEvent, uiLayoutFn uiLayout,
                   uiRenderFn uiUserRender, uiDropFn uiUserDrop) {
  // make container with user-supplied objects and set window pointer
  uiUserPointer* ptr = (uiUserPointer*) mju_malloc(sizeof(uiUserPointer));
  ptr->state = state;
  ptr->uiEvent = uiEvent;
  ptr->uiLayout = uiLayout;
  ptr->uiRender = uiUserRender;
  ptr->uiDrop = uiUserDrop;
  Glfw().glfwSetWindowUserPointer(wnd, ptr);

  // compute framebuffer-to-window pixel ratio
  int width_win, width_buf, height;
  Glfw().glfwGetWindowSize(wnd, &width_win, &height);
  Glfw().glfwGetFramebufferSize(wnd, &width_buf, &height);
  ptr->buffer2window = (double)width_buf / (double)width_win;

  // set internal callbacks
  Glfw().glfwSetKeyCallback(wnd, uiKeyboard);
  Glfw().glfwSetCursorPosCallback(wnd, uiMouseMove);
  Glfw().glfwSetMouseButtonCallback(wnd, uiMouseButton);
  Glfw().glfwSetScrollCallback(wnd, uiScroll);
  Glfw().glfwSetWindowSizeCallback(wnd, uiResize);
  Glfw().glfwSetWindowRefreshCallback(wnd, uiRender);
  Glfw().glfwSetDropCallback(wnd, uiDrop);
}



// Clear UI callbacks in GLFW window.
void uiClearCallback(GLFWwindow* wnd) {
  // clear container
  if (Glfw().glfwGetWindowUserPointer(wnd)) {
    mju_free(Glfw().glfwGetWindowUserPointer(wnd));
    Glfw().glfwSetWindowUserPointer(wnd, NULL);
  }

  // clear internal callbacks
  Glfw().glfwSetKeyCallback(wnd, NULL);
  Glfw().glfwSetCursorPosCallback(wnd, NULL);
  Glfw().glfwSetMouseButtonCallback(wnd, NULL);
  Glfw().glfwSetScrollCallback(wnd, NULL);
  Glfw().glfwSetWindowSizeCallback(wnd, NULL);
  Glfw().glfwSetWindowRefreshCallback(wnd, NULL);
  Glfw().glfwSetDropCallback(wnd, NULL);
}



// Modify UI structure.
void uiModify(GLFWwindow* wnd, mjUI* ui, mjuiState* state, mjrContext* con) {
  mjui_resize(ui, con);
  mjr_addAux(ui->auxid, ui->width, ui->maxheight, ui->spacing.samples, con);
  uiResize(wnd, 0, 0);
  mjui_update(-1, -1, ui, state, con);
}
