/*
 * This is a GUI for example programs that require GUI-style interaction.
 * It's intended to be as simple and transparent as possible (simplistic,
 * even).
 */

#include <iostream>
#include <string>
#include <algorithm>

#define ALLEGRO_UNSTABLE
#include <allegro5/allegro.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>

#include "nihgui.hpp"

#define CLAMP(x,y,z) (std::max)(x, (std::min)(y, z))

namespace {
   
class SaveState
{
   ALLEGRO_STATE state;

public:
   SaveState(int save=ALLEGRO_STATE_ALL)
   {
      al_store_state(&state, save);
   }
   ~SaveState()
   {
      al_restore_state(&state);
   }
};

class UString
{
   ALLEGRO_USTR_INFO info;
   const ALLEGRO_USTR *ustr;

public:
   UString(const ALLEGRO_USTR *s, int first, int end = -1)
   {
      if (end == -1)
         end = al_ustr_size(s);
      ustr = al_ref_ustr(&info, s, first, end);
   }

   // Conversion
   operator const ALLEGRO_USTR *() const
   {
      return ustr;
   }
};

};

/*---------------------------------------------------------------------------*/

Theme::Theme(const ALLEGRO_FONT *font)
{
   this->bg = al_map_rgb(255, 255, 255);
   this->fg = al_map_rgb(0, 0, 0);
   this->highlight = al_map_rgb(128, 128, 255);
   this->font = font;
}

/*---------------------------------------------------------------------------*/

Widget::Widget():
   grid_x(0),
   grid_y(0),
   grid_w(0),
   grid_h(0),
   dialog(NULL),
   x1(0),
   y1(0),
   x2(0),
   y2(0),
   disabled(false)
{
}

void Widget::configure(int xsize, int ysize, int x_padding, int y_padding)
{
   this->x1 = xsize * this->grid_x + x_padding;
   this->y1 = ysize * this->grid_y + y_padding;
   this->x2 = xsize * (this->grid_x + this->grid_w) - x_padding - 1;
   this->y2 = ysize * (this->grid_y + this->grid_h) - y_padding - 1;
}

bool Widget::contains(int x, int y)
{
   return (x >= this->x1 && y >= this->y1 && x <= this->x2 && y <= this->y2);
}

/*---------------------------------------------------------------------------*/

Dialog::Dialog(const Theme & theme, ALLEGRO_DISPLAY *display,
      int grid_m, int grid_n):
   theme(theme),
   display(display),
   grid_m(grid_m),
   grid_n(grid_n),
   x_padding(1),
   y_padding(1),

   draw_requested(true),
   quit_requested(false),
   mouse_over_widget(NULL),
   mouse_down_widget(NULL),
   key_widget(NULL),

   event_handler(NULL)
{
   this->event_queue = al_create_event_queue();
   al_register_event_source(this->event_queue, al_get_keyboard_event_source());
   al_register_event_source(this->event_queue, al_get_mouse_event_source());
   al_register_event_source(this->event_queue, al_get_display_event_source(display));
   if (al_is_touch_input_installed()) {
      al_register_event_source(this->event_queue,
         al_get_touch_input_mouse_emulation_event_source());
   }
}

Dialog::~Dialog()
{
   this->display = NULL;

   al_destroy_event_queue(this->event_queue);
   this->event_queue = NULL;
}

void Dialog::set_padding(int x_padding, int y_padding)
{
   this->x_padding = x_padding;
   this->y_padding = y_padding;
}

void Dialog::add(Widget & widget, int grid_x, int grid_y, int grid_w,
   int grid_h)
{
   widget.grid_x = grid_x;
   widget.grid_y = grid_y;
   widget.grid_w = grid_w;
   widget.grid_h = grid_h;

   this->all_widgets.push_back(&widget);
   widget.dialog = this;
}

void Dialog::prepare()
{
   this->configure_all();

   /* XXX this isn't working right in X.  The mouse position is reported as
    * (0,0) initially, until the mouse pointer is moved.
    */
   ALLEGRO_MOUSE_STATE mst;
   al_get_mouse_state(&mst);
   this->check_mouse_over(mst.x, mst.y);
}

void Dialog::configure_all()
{
   const int xsize = al_get_display_width(display) / this->grid_m;
   const int ysize = al_get_display_height(display) / this->grid_n;

   for (std::list<Widget*>::iterator it = this->all_widgets.begin();
         it != this->all_widgets.end();
         ++it)
   {
      (*it)->configure(xsize, ysize, this->x_padding, this->y_padding);
   }
}

void Dialog::run_step(bool block)
{
   ALLEGRO_EVENT event;

   if (block) {
      al_wait_for_event(event_queue, NULL);
   }

   while (al_get_next_event(event_queue, &event)) {
      switch (event.type) {
         case ALLEGRO_EVENT_DISPLAY_CLOSE:
            this->request_quit();
            break;

         case ALLEGRO_EVENT_KEY_CHAR:
            on_key_down(event.keyboard);
            break;

         case ALLEGRO_EVENT_MOUSE_AXES:
            on_mouse_axes(event.mouse);
            break;

         case ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
            on_mouse_button_down(event.mouse);
            break;

         case ALLEGRO_EVENT_MOUSE_BUTTON_UP:
            on_mouse_button_up(event.mouse);
            break;

         case ALLEGRO_EVENT_DISPLAY_EXPOSE:
            this->request_draw();
            break;

         default:
            if (event_handler) {
               event_handler->handle_event(event);
            }
            break;
      }
   }
}

void Dialog::on_key_down(const ALLEGRO_KEYBOARD_EVENT & event)
{
   if (event.display != this->display) {
      return;
   }

   // XXX think of something better when we need it
   if (event.keycode == ALLEGRO_KEY_ESCAPE) {
      this->request_quit();
   }

   if (this->key_widget) {
      this->key_widget->on_key_down(event);
   }
}

void Dialog::on_mouse_axes(const ALLEGRO_MOUSE_EVENT & event)
{
   const int mx = event.x;
   const int my = event.y;

   if (event.display != this->display) {
      return;
   }

   if (this->mouse_down_widget) {
      this->mouse_down_widget->on_mouse_button_hold(mx, my);
      return;
   }

   this->check_mouse_over(mx, my);
}

void Dialog::check_mouse_over(int mx, int my)
{
   if (this->mouse_over_widget && this->mouse_over_widget->contains(mx, my)) {
      /* no change */
      return;
   }

   for (std::list<Widget*>::iterator it = this->all_widgets.begin();
         it != this->all_widgets.end();
         ++it)
   {
      if ((*it)->contains(mx, my) && (*it)->want_mouse_focus()) {
         this->mouse_over_widget = (*it);
         this->mouse_over_widget->got_mouse_focus();
         return;
      }
   }

   if (this->mouse_over_widget) {
      this->mouse_over_widget->lost_mouse_focus();
      this->mouse_over_widget = NULL;
   }
}

void Dialog::on_mouse_button_down(const ALLEGRO_MOUSE_EVENT & event)
{
   if (event.button != 1)
      return;

   /* With touch input we may not receive mouse axes event before the touch
    * so we must check which widget the touch is over.
    */
   this->check_mouse_over(event.x, event.y);
   if (!this->mouse_over_widget)
      return;

   this->mouse_down_widget = this->mouse_over_widget;
   this->mouse_down_widget->on_mouse_button_down(event.x, event.y);

   /* transfer key focus */
   if (this->mouse_down_widget != this->key_widget) {
      if (this->key_widget) {
         this->key_widget->lost_key_focus();
         this->key_widget = NULL;
      }
      if (this->mouse_down_widget->want_key_focus()) {
         this->key_widget = this->mouse_down_widget;
         this->key_widget->got_key_focus();
      }
   }
}

void Dialog::on_mouse_button_up(const ALLEGRO_MOUSE_EVENT & event)
{
   if (event.button != 1)
      return;
   if (!this->mouse_down_widget)
      return;

   this->mouse_down_widget->on_mouse_button_up(event.x, event.y);
   if (this->mouse_down_widget->contains(event.x, event.y)) {
      this->mouse_down_widget->on_click(event.x, event.y);
   }
   this->mouse_down_widget = NULL;
}

void Dialog::request_quit()
{
   this->quit_requested = true;
}

bool Dialog::is_quit_requested() const
{
   return this->quit_requested;
}

void Dialog::request_draw()
{
   this->draw_requested = true;
}

bool Dialog::is_draw_requested() const
{
   return this->draw_requested;
}

void Dialog::draw()
{
   int cx, cy, cw, ch;
   al_get_clipping_rectangle(&cx, &cy, &cw, &ch);

   for (std::list<Widget *>::iterator it = this->all_widgets.begin();
         it != this->all_widgets.end();
         ++it)
   {
      Widget *wid = (*it);
      al_set_clipping_rectangle(wid->x1, wid->y1, wid->width(), wid->height());
      wid->draw();
   }

   al_set_clipping_rectangle(cx, cy, cw, ch);

   this->draw_requested = false;
}

const Theme & Dialog::get_theme() const
{
   return this->theme;
}

void Dialog::register_event_source(ALLEGRO_EVENT_SOURCE *source)
{
   al_register_event_source(this->event_queue, source);
}

void Dialog::set_event_handler(EventHandler *event_handler)
{
   this->event_handler = event_handler;
}

/*---------------------------------------------------------------------------*/

Label::Label(std::string text, bool centred) :
   text(text),
   centred(centred)
{
}

void Label::draw()
{
   const Theme & theme = this->dialog->get_theme();
   SaveState state;
   ALLEGRO_COLOR fg = theme.fg;

   if (is_disabled()) {
      fg = al_map_rgb(64, 64, 64);
   }

   al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
   if (centred) {
      al_draw_text(theme.font, fg, (this->x1 + this->x2 + 1)/2,
         this->y1, ALLEGRO_ALIGN_CENTRE, this->text.c_str());
   }
   else {
      al_draw_text(theme.font, fg, this->x1, this->y1, 0, this->text.c_str());
   }
}

void Label::set_text(std::string new_text)
{
   this->text = new_text;
}

bool Label::want_mouse_focus()
{
   return false;
}

/*---------------------------------------------------------------------------*/

Button::Button(std::string text):
   text(text),
   pushed(false)
{
}

void Button::on_mouse_button_down(int mx, int my)
{
   (void)mx;
   (void)my;

   if (is_disabled())
      return;

   this->pushed = true;
   dialog->request_draw();
}

void Button::on_mouse_button_up(int mx, int my)
{
   (void)mx;
   (void)my;

   if (is_disabled())
      return;

   this->pushed = false;
   dialog->request_draw();
}

void Button::draw()
{
   const Theme & theme = this->dialog->get_theme();
   ALLEGRO_COLOR fg;
   ALLEGRO_COLOR bg;
   SaveState state;
   double y;

   if (this->pushed) {
      fg = theme.bg;
      bg = theme.fg;
   }
   else {
      fg = theme.fg;
      bg = theme.bg;
   }

   if (is_disabled()) {
      bg = al_map_rgb(64, 64, 64);
   }

   al_draw_filled_rectangle(this->x1, this->y1,
      this->x2, this->y2, bg);
   al_draw_rectangle(this->x1 + 0.5, this->y1 + 0.5,
      this->x2 - 0.5, this->y2 - 0.5, fg, 0);
   al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);

   /* Center the text vertically in the button, taking the font size
    * into consideration.
    */
   y = (this->y1 + this->y2 - al_get_font_line_height(theme.font) - 1) / 2;

   al_draw_text(theme.font, fg, (this->x1 + this->x2 + 1)/2,
      y, ALLEGRO_ALIGN_CENTRE, this->text.c_str());
}

bool Button::get_pushed()
{
   return pushed;
}

/*---------------------------------------------------------------------------*/

ToggleButton::ToggleButton(std::string text) :
   Button(text)
{
}

void ToggleButton::on_mouse_button_down(int mx, int my)
{
   (void)mx;
   (void)my;

   if (is_disabled())
      return;

   set_pushed(!this->pushed);
}

void ToggleButton::on_mouse_button_up(int mx, int my)
{
   (void)mx;
   (void)my;

   if (is_disabled())
      return;
}

void ToggleButton::set_pushed(bool pushed)
{
   if (this->pushed != pushed) {
      this->pushed = pushed;
      if (dialog)
         dialog->request_draw();
   }
}

/*---------------------------------------------------------------------------*/

const std::string List::empty_string;

List::List(int initial_selection) :
   selected_item(initial_selection)
{
}

bool List::want_key_focus()
{
   return !is_disabled();
}

void List::on_key_down(const ALLEGRO_KEYBOARD_EVENT & event)
{
   if (is_disabled())
      return;

   switch (event.keycode) {
      case ALLEGRO_KEY_DOWN:
         if (selected_item < items.size() - 1) {
            selected_item++;
            dialog->request_draw();
         }
         break;

      case ALLEGRO_KEY_UP:
         if (selected_item > 0) {
            selected_item--;
            dialog->request_draw();
         }
         break;
   }
}

void List::on_click(int mx, int my)
{
   if (is_disabled())
      return;

   const Theme & theme = dialog->get_theme();
   unsigned int i = (my - this->y1) / al_get_font_line_height(theme.font);
   if (i < this->items.size()) {
      this->selected_item = i;
      dialog->request_draw();
   }

   (void)mx;
   (void)my;
}

void List::draw()
{
   const Theme & theme = dialog->get_theme();
   SaveState state;
   ALLEGRO_COLOR bg = theme.bg;

   if (is_disabled()) {
      bg = al_map_rgb(64, 64, 64);
   }

   al_draw_filled_rectangle(x1 + 1, y1 + 1, x2 - 1, y2 - 1, bg);

   al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
   const int font_height = al_get_font_line_height(theme.font);
   for (unsigned i = 0; i < items.size(); i++) {
      int yi = y1 + i * font_height;

      if (i == selected_item) {
         al_draw_filled_rectangle(x1 + 1, yi, x2 - 1, yi + font_height - 1,
            theme.highlight);
      }

      al_draw_text(theme.font, theme.fg, x1, yi, 0, items.at(i).c_str());
   }
}

void List::clear_items()
{
   this->items.clear();
   this->selected_item = 0;
}

void List::append_item(std::string text)
{
   this->items.push_back(text);
}

const std::string & List::get_selected_item_text() const
{
   if (this->selected_item < this->items.size())
      return this->items.at(this->selected_item);
   else
      return List::empty_string;
}

int List::get_cur_value() const
{
   return this->selected_item;
}

/*---------------------------------------------------------------------------*/

VSlider::VSlider(int cur_value, int max_value) :
   cur_value(cur_value),
   max_value(max_value)
{
}

void VSlider::on_mouse_button_down(int mx, int my)
{
   if (is_disabled())
      return;

   this->on_mouse_button_hold(mx, my);
}

void VSlider::on_mouse_button_hold(int mx, int my)
{
   if (is_disabled())
      return;

   double r = (double) (this->y2 - 1 - my) / (this->height() - 2);
   r = CLAMP(0.0, r, 1.0);
   cur_value = (int) (r * max_value);
   dialog->request_draw();

   (void)mx;
}

void VSlider::draw()
{
   const Theme & theme = dialog->get_theme();
   ALLEGRO_COLOR bg = theme.fg;
   float left = x1 + 0.5, top = y1 + 0.5;
   float right = x2 + 0.5, bottom = y2 + 0.5;
   SaveState state;

   if (is_disabled()) {
      bg = al_map_rgb(64, 64, 64);
   }

   al_draw_rectangle(left, top, right, bottom, bg, 1);

   double ratio = (double) this->cur_value / (double) this->max_value;
   int ypos = (int) (bottom - 0.5 - (int) (ratio * (height() - 7)));
   al_draw_filled_rectangle(left + 0.5, ypos - 5, right - 0.5, ypos, theme.fg);
}

int VSlider::get_cur_value() const
{
   return this->cur_value;
}

int VSlider::get_max_value() const
{
   return this->max_value;
}

void VSlider::set_cur_value(int v)
{
   this->cur_value = v;
}

/*---------------------------------------------------------------------------*/

HSlider::HSlider(int cur_value, int max_value) :
   cur_value(cur_value),
   max_value(max_value)
{
}

void HSlider::on_mouse_button_down(int mx, int my)
{
   if (is_disabled())
      return;

   this->on_mouse_button_hold(mx, my);
}

void HSlider::on_mouse_button_hold(int mx, int my)
{
   if (is_disabled())
      return;

   double r = (double) (mx - 1 - this->x1) / (this->width() - 2);
   r = CLAMP(0.0, r, 1.0);
   cur_value = (int) (r * max_value);
   dialog->request_draw();

   (void)my;
}

void HSlider::draw()
{
   const Theme & theme = dialog->get_theme();
   const int cy = (y1 + y2) / 2;
   SaveState state;
   ALLEGRO_COLOR bg = theme.bg;

   if (is_disabled()) {
      bg = al_map_rgb(64, 64, 64);
   }

   al_draw_filled_rectangle(x1, y1, x2, y2, bg);
   al_draw_line(x1, cy, x2, cy, theme.fg, 0);

   double ratio = (double) this->cur_value / (double) this->max_value;
   int xpos = x1 + (int) (ratio * (width() - 2));
   al_draw_filled_rectangle(xpos - 2, y1, xpos + 2, y2, theme.fg);
}

int HSlider::get_cur_value() const
{
   return this->cur_value;
}

int HSlider::get_max_value() const
{
   return this->max_value;
}

void HSlider::set_cur_value(int v)
{
   this->cur_value = v;
}

/*---------------------------------------------------------------------------*/

TextEntry::TextEntry(const char *initial_text) :
   focused(false),
   cursor_pos(0),
   left_pos(0)
{
   text = al_ustr_new(initial_text);
}

TextEntry::~TextEntry()
{
   al_ustr_free(text);
}

bool TextEntry::want_key_focus()
{
   return !is_disabled();
}

void TextEntry::got_key_focus()
{
   this->focused = true;
   dialog->request_draw();
}

void TextEntry::lost_key_focus()
{
   this->focused = false;
   dialog->request_draw();
}

void TextEntry::on_key_down(const ALLEGRO_KEYBOARD_EVENT & event)
{
   if (is_disabled())
      return;

   switch (event.keycode) {
      case ALLEGRO_KEY_LEFT:
         al_ustr_prev(text, &cursor_pos);
         break;

      case ALLEGRO_KEY_RIGHT:
         al_ustr_next(text, &cursor_pos);
         break;

      case ALLEGRO_KEY_HOME:
         cursor_pos = 0;
         break;

      case ALLEGRO_KEY_END:
         cursor_pos = al_ustr_size(text);
         break;

      case ALLEGRO_KEY_DELETE:
         al_ustr_remove_chr(text, cursor_pos);
         break;

      case ALLEGRO_KEY_BACKSPACE:
         if (al_ustr_prev(text, &cursor_pos))
            al_ustr_remove_chr(text, cursor_pos);
         break;

      default:
         if (event.unichar >= ' ') {
            al_ustr_insert_chr(text, cursor_pos, event.unichar);
            cursor_pos += al_utf8_width(event.unichar);
         }
         break;
   }

   maybe_scroll();
   dialog->request_draw();
}

void TextEntry::maybe_scroll()
{
   const Theme & theme = dialog->get_theme();

   if (cursor_pos < left_pos + 3) {
      if (cursor_pos < 3)
         left_pos = 0;
      else
         left_pos = cursor_pos - 3;
   }
   else {
      for (;;) {
         const int tw = al_get_ustr_width(theme.font,
            UString(text, left_pos, cursor_pos));
         if (x1 + tw + CURSOR_WIDTH < x2) {
            break;
         }
         al_ustr_next(text, &left_pos);
      }
   }
}

void TextEntry::draw()
{
   const Theme & theme = dialog->get_theme();
   SaveState state;
   ALLEGRO_COLOR bg = theme.bg;

   if (is_disabled()) {
      bg = al_map_rgb(64, 64, 64);
   }

   al_draw_filled_rectangle(x1, y1, x2, y2, bg);

   al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);

   if (!focused) {
      al_draw_ustr(theme.font, theme.fg, x1, y1, 0, UString(text, left_pos));
   }
   else {
      int x = x1;

      if (cursor_pos > 0) {
         UString sub(text, left_pos, cursor_pos);
         al_draw_ustr(theme.font, theme.fg, x1, y1, 0, sub);
         x += al_get_ustr_width(theme.font, sub);
      }

      if ((unsigned) cursor_pos == al_ustr_size(text)) {
         al_draw_filled_rectangle(x, y1, x + CURSOR_WIDTH,
            y1 + al_get_font_line_height(theme.font), theme.fg);
      }
      else {
         int post_cursor = cursor_pos;
         al_ustr_next(text, &post_cursor);

         UString sub(text, cursor_pos, post_cursor);
         int subw = al_get_ustr_width(theme.font, sub);
         al_draw_filled_rectangle(x, y1, x + subw,
            y1 + al_get_font_line_height(theme.font), theme.fg);
         al_draw_ustr(theme.font, theme.bg, x, y1, 0, sub);
         x += subw;

         al_draw_ustr(theme.font, theme.fg, x, y1, 0,
            UString(text, post_cursor));
      }
   }
}

const char *TextEntry::get_text()
{
   return al_cstr(text);
}


/* vim: set sts=3 sw=3 et: */