/* ASEPRITE
 * Copyright (C) 2001-2012  David Capello
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"

#include "app/widget_loader.h"

#include "app.h"
#include "app/widget_not_found.h"
#include "base/bind.h"
#include "base/memory.h"
#include "modules/gui.h"
#include "resource_finder.h"
#include "ui/gui.h"
#include "widgets/color_button.h"
#include "xml_exception.h"

#include "tinyxml.h"

#include <allegro.h>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#define TRANSLATE_ATTR(a) a

using namespace ui;

static int convert_align_value_to_flags(const char *value);
static bool bool_attr_is_true(const TiXmlElement* elem, const char* attribute_name);

namespace app {

WidgetLoader::WidgetLoader()
  : m_tooltipManager(NULL)
{
}

WidgetLoader::~WidgetLoader()
{
  for (TypeCreatorsMap::iterator
         it=m_typeCreators.begin(), end=m_typeCreators.end(); it != end; ++it)
    it->second->dispose();
}

void WidgetLoader::addWidgetType(const char* tagName, IWidgetTypeCreator* creator)
{
  m_typeCreators[tagName] = creator;
}

Widget* WidgetLoader::loadWidget(const char* fileName, const char* widgetId)
{
  Widget* widget;
  std::string buf;
  bool found = false;

  ResourceFinder rf;

  rf.addPath(fileName);

  buf = "widgets/";
  buf += fileName;
  rf.findInDataDir(buf.c_str());

  while (const char* path = rf.next()) {
    if (exists(path)) {
      buf = path;
      found = true;
      break;
    }
  }

  if (!found)
    throw WidgetNotFound(widgetId);

  widget = loadWidgetFromXmlFile(buf.c_str(), widgetId);
  if (!widget)
    throw WidgetNotFound(widgetId);

  return widget;
}

Widget* WidgetLoader::loadWidgetFromXmlFile(const char* xmlFilename, const char* widgetId)
{
  Widget* widget = NULL;
  m_tooltipManager = NULL;

  TiXmlDocument doc;
  if (!doc.LoadFile(xmlFilename))
    throw XmlException(&doc);

  // Search the requested widget.
  TiXmlHandle handle(&doc);
  TiXmlElement* xmlElement = handle
    .FirstChild("gui")
    .FirstChildElement().ToElement();

  while (xmlElement) {
    const char* nodename = xmlElement->Attribute("id");

    if (nodename && ustrcmp(nodename, widgetId) == 0) {
      widget = convertXmlElementToWidget(xmlElement, NULL);
      break;
    }

    xmlElement = xmlElement->NextSiblingElement();
  }

  return widget;
}

Widget* WidgetLoader::convertXmlElementToWidget(const TiXmlElement* elem, Widget* root)
{
  const char *elem_name = elem->Value();
  Widget* widget = NULL;
  Widget* child;

  // TODO error handling: add a message if the widget is bad specified

  // Try to use one of the creators.
  TypeCreatorsMap::iterator it = m_typeCreators.find(elem_name);

  if (it != m_typeCreators.end()) {
    widget = it->second->createWidgetFromXml(elem);
  }
  // Boxes
  else if (ustrcmp(elem_name, "box") == 0) {
    bool horizontal  = bool_attr_is_true(elem, "horizontal");
    bool vertical    = bool_attr_is_true(elem, "vertical");
    bool homogeneous = bool_attr_is_true(elem, "homogeneous");

    widget = new Box((horizontal ? JI_HORIZONTAL:
                      vertical ? JI_VERTICAL: 0) |
                     (homogeneous ? JI_HOMOGENEOUS: 0));
  }
  else if (ustrcmp(elem_name, "vbox") == 0) {
    bool homogeneous = bool_attr_is_true(elem, "homogeneous");

    widget = new VBox();
    if (homogeneous)
      widget->setAlign(widget->getAlign() | JI_HOMOGENEOUS);
  }
  else if (ustrcmp(elem_name, "hbox") == 0) {
    bool homogeneous = bool_attr_is_true(elem, "homogeneous");

    widget = new HBox();
    if (homogeneous)
      widget->setAlign(widget->getAlign() | JI_HOMOGENEOUS);
  }
  else if (ustrcmp(elem_name, "boxfiller") == 0) {
    widget = new BoxFiller();
  }
  // Button
  else if (ustrcmp(elem_name, "button") == 0) {
    const char *text = elem->Attribute("text");

    widget = new Button(text ? TRANSLATE_ATTR(text): NULL);
    if (widget) {
      bool left   = bool_attr_is_true(elem, "left");
      bool right  = bool_attr_is_true(elem, "right");
      bool top    = bool_attr_is_true(elem, "top");
      bool bottom = bool_attr_is_true(elem, "bottom");
      bool closewindow = bool_attr_is_true(elem, "closewindow");
      const char *_bevel = elem->Attribute("bevel");

      widget->setAlign((left ? JI_LEFT: (right ? JI_RIGHT: JI_CENTER)) |
                       (top ? JI_TOP: (bottom ? JI_BOTTOM: JI_MIDDLE)));

      if (_bevel != NULL) {
        char* bevel = base_strdup(_bevel);
        int c, b[4];
        char *tok;

        for (c=0; c<4; ++c)
          b[c] = 0;

        for (tok=ustrtok(bevel, " "), c=0;
             tok;
             tok=ustrtok(NULL, " "), ++c) {
          if (c < 4)
            b[c] = ustrtol(tok, NULL, 10);
        }
        base_free(bevel);

        setup_bevels(widget, b[0], b[1], b[2], b[3]);
      }

      if (closewindow) {
        static_cast<Button*>(widget)
          ->Click.connect(Bind<void>(&Widget::closeWindow, widget));
      }
    }
  }
  // Check
  else if (ustrcmp(elem_name, "check") == 0) {
    const char *text = elem->Attribute("text");
    const char *looklike = elem->Attribute("looklike");

    text = (text ? TRANSLATE_ATTR(text): NULL);

    if (looklike != NULL && strcmp(looklike, "button") == 0) {
      widget = new CheckBox(text, JI_BUTTON);
    }
    else {
      widget = new CheckBox(text);
    }

    if (widget) {
      bool center = bool_attr_is_true(elem, "center");
      bool right  = bool_attr_is_true(elem, "right");
      bool top    = bool_attr_is_true(elem, "top");
      bool bottom = bool_attr_is_true(elem, "bottom");

      widget->setAlign((center ? JI_CENTER:
                        (right ? JI_RIGHT: JI_LEFT)) |
                       (top    ? JI_TOP:
                        (bottom ? JI_BOTTOM: JI_MIDDLE)));
    }
  }
  /* combobox */
  else if (ustrcmp(elem_name, "combobox") == 0) {
    widget = new ComboBox();
  }
  /* entry */
  else if (ustrcmp(elem_name, "entry") == 0) {
    const char *maxsize = elem->Attribute("maxsize");
    const char *text = elem->Attribute("text");

    if (maxsize != NULL) {
      bool readonly = bool_attr_is_true(elem, "readonly");

      widget = new Entry(ustrtol(maxsize, NULL, 10),
                         text ? TRANSLATE_ATTR(text): NULL);

      if (readonly)
        ((Entry*)widget)->setReadOnly(true);
    }
  }
  /* grid */
  else if (ustrcmp(elem_name, "grid") == 0) {
    const char *columns = elem->Attribute("columns");
    bool same_width_columns = bool_attr_is_true(elem, "same_width_columns");

    if (columns != NULL) {
      widget = new Grid(ustrtol(columns, NULL, 10),
                        same_width_columns);
    }
  }
  /* label */
  else if (ustrcmp(elem_name, "label") == 0) {
    const char *text = elem->Attribute("text");

    widget = new Label(text ? TRANSLATE_ATTR(text): NULL);
    if (widget) {
      bool center = bool_attr_is_true(elem, "center");
      bool right  = bool_attr_is_true(elem, "right");
      bool top    = bool_attr_is_true(elem, "top");
      bool bottom = bool_attr_is_true(elem, "bottom");

      widget->setAlign((center ? JI_CENTER:
                        (right ? JI_RIGHT: JI_LEFT)) |
                       (top    ? JI_TOP:
                        (bottom ? JI_BOTTOM: JI_MIDDLE)));
    }
  }
  /* listbox */
  else if (ustrcmp(elem_name, "listbox") == 0) {
    widget = new ListBox();
  }
  /* listitem */
  else if (ustrcmp(elem_name, "listitem") == 0) {
    const char *text = elem->Attribute("text");

    widget = new ListBox::Item(text ? TRANSLATE_ATTR(text): NULL);
  }
  /* splitter */
  else if (ustrcmp(elem_name, "splitter") == 0) {
    bool horizontal = bool_attr_is_true(elem, "horizontal");
    bool vertical = bool_attr_is_true(elem, "vertical");
    const char* by = elem->Attribute("by");
    const char* position = elem->Attribute("position");
    Splitter::Type type = (by && strcmp(by, "pixel") == 0 ?
                           Splitter::ByPixel:
                           Splitter::ByPercentage);

    Splitter* splitter = new Splitter(type,
                                      horizontal ? JI_HORIZONTAL:
                                      vertical ? JI_VERTICAL: 0);
    if (position) {
      splitter->setPosition(strtod(position, NULL)
                            * (type == Splitter::ByPixel ? jguiscale(): 1));
    }
    widget = splitter;
  }
  /* radio */
  else if (ustrcmp(elem_name, "radio") == 0) {
    const char* text = elem->Attribute("text");
    const char* group = elem->Attribute("group");
    const char *looklike = elem->Attribute("looklike");

    text = (text ? TRANSLATE_ATTR(text): NULL);
    int radio_group = (group ? ustrtol(group, NULL, 10): 1);

    if (looklike != NULL && strcmp(looklike, "button") == 0) {
      widget = new RadioButton(text, radio_group, JI_BUTTON);
    }
    else {
      widget = new RadioButton(text, radio_group);
    }

    if (widget) {
      bool center = bool_attr_is_true(elem, "center");
      bool right  = bool_attr_is_true(elem, "right");
      bool top    = bool_attr_is_true(elem, "top");
      bool bottom = bool_attr_is_true(elem, "bottom");

      widget->setAlign((center ? JI_CENTER:
                        (right ? JI_RIGHT: JI_LEFT)) |
                       (top    ? JI_TOP:
                        (bottom ? JI_BOTTOM: JI_MIDDLE)));
    }
  }
  /* separator */
  else if (ustrcmp(elem_name, "separator") == 0) {
    const char *text = elem->Attribute("text");
    bool center      = bool_attr_is_true(elem, "center");
    bool right       = bool_attr_is_true(elem, "right");
    bool middle      = bool_attr_is_true(elem, "middle");
    bool bottom      = bool_attr_is_true(elem, "bottom");
    bool horizontal  = bool_attr_is_true(elem, "horizontal");
    bool vertical    = bool_attr_is_true(elem, "vertical");

    widget = new Separator(text ? TRANSLATE_ATTR(text): NULL,
                           (horizontal ? JI_HORIZONTAL: 0) |
                           (vertical ? JI_VERTICAL: 0) |
                           (center ? JI_CENTER:
                            (right ? JI_RIGHT: JI_LEFT)) |
                           (middle ? JI_MIDDLE:
                            (bottom ? JI_BOTTOM: JI_TOP)));
  }
  /* slider */
  else if (ustrcmp(elem_name, "slider") == 0) {
    const char *min = elem->Attribute("min");
    const char *max = elem->Attribute("max");
    int min_value = min != NULL ? ustrtol(min, NULL, 10): 0;
    int max_value = max != NULL ? ustrtol(max, NULL, 10): 0;

    widget = new Slider(min_value, max_value, min_value);
  }
  /* textbox */
  else if (ustrcmp(elem_name, "textbox") == 0) {
    bool wordwrap = bool_attr_is_true(elem, "wordwrap");

    widget = new TextBox(elem->GetText(), wordwrap ? JI_WORDWRAP: 0);
  }
  /* view */
  else if (ustrcmp(elem_name, "view") == 0) {
    widget = new View();
  }
  /* window */
  else if (ustrcmp(elem_name, "window") == 0) {
    const char *text = elem->Attribute("text");

    if (text) {
      bool desktop = bool_attr_is_true(elem, "desktop");

      if (desktop)
        widget = new Window(true, NULL);
      else
        widget = new Window(false, TRANSLATE_ATTR(text));
    }
  }
  /* colorpicker */
  else if (ustrcmp(elem_name, "colorpicker") == 0) {
    widget = new ColorButton(Color::fromMask(), app_get_current_pixel_format());
  }

  // Was the widget created?
  if (widget) {
    const char* id        = elem->Attribute("id");
    const char* tooltip   = elem->Attribute("tooltip");
    bool selected         = bool_attr_is_true(elem, "selected");
    bool disabled         = bool_attr_is_true(elem, "disabled");
    bool expansive        = bool_attr_is_true(elem, "expansive");
    bool magnet           = bool_attr_is_true(elem, "magnet");
    bool noborders        = bool_attr_is_true(elem, "noborders");
    const char* width     = elem->Attribute("width");
    const char* height    = elem->Attribute("height");
    const char* minwidth  = elem->Attribute("minwidth");
    const char* minheight = elem->Attribute("minheight");
    const char* maxwidth  = elem->Attribute("maxwidth");
    const char* maxheight = elem->Attribute("maxheight");
    const char* childspacing = elem->Attribute("childspacing");

    if (id != NULL)
      widget->setId(id);

    if (tooltip != NULL && root != NULL) {
      if (!m_tooltipManager) {
        m_tooltipManager = new ::ui::TooltipManager();
        root->addChild(m_tooltipManager);
      }
      m_tooltipManager->addTooltipFor(widget, tooltip, JI_LEFT);
    }

    if (selected)
      widget->setSelected(selected);

    if (disabled)
      widget->setEnabled(false);

    if (expansive)
      widget->setExpansive(true);

    if (magnet)
      widget->setFocusMagnet(true);

    if (noborders)
      jwidget_noborders(widget);

    if (childspacing)
      widget->child_spacing = ustrtol(childspacing, NULL, 10);

    if (width || minwidth ||
        height || minheight) {
      int w = (width || minwidth) ? ustrtol(width ? width: minwidth, NULL, 10): 0;
      int h = (height || minheight) ? ustrtol(height ? height: minheight, NULL, 10): 0;
      jwidget_set_min_size(widget, w*jguiscale(), h*jguiscale());
    }

    if (width || maxwidth ||
        height || maxheight) {
      int w = (width || maxwidth) ? strtol(width ? width: maxwidth, NULL, 10): INT_MAX;
      int h = (height || maxheight) ? strtol(height ? height: maxheight, NULL, 10): INT_MAX;
      jwidget_set_max_size(widget, w*jguiscale(), h*jguiscale());
    }

    if (!root)
      root = widget;

    // Children
    const TiXmlElement* childElem = elem->FirstChildElement();
    while (childElem) {
      child = convertXmlElementToWidget(childElem, root);
      if (child) {
        // Attach the child in the view
        if (widget->type == JI_VIEW) {
          static_cast<View*>(widget)->attachToView(child);
          break;
        }
        // Add the child in the grid
        else if (widget->type == JI_GRID) {
          const char* cell_hspan = childElem->Attribute("cell_hspan");
          const char* cell_vspan = childElem->Attribute("cell_vspan");
          const char* cell_align = childElem->Attribute("cell_align");
          int hspan = cell_hspan ? ustrtol(cell_hspan, NULL, 10): 1;
          int vspan = cell_vspan ? ustrtol(cell_vspan, NULL, 10): 1;
          int align = cell_align ? convert_align_value_to_flags(cell_align): 0;
          Grid* grid = dynamic_cast<Grid*>(widget);
          ASSERT(grid != NULL);

          grid->addChildInCell(child, hspan, vspan, align);
        }
        // Just add the child in any other kind of widget
        else
          widget->addChild(child);
      }
      childElem = childElem->NextSiblingElement();
    }

    if (widget->type == JI_VIEW) {
      bool maxsize = bool_attr_is_true(elem, "maxsize");
      if (maxsize)
        static_cast<View*>(widget)->makeVisibleAllScrollableArea();
    }
  }

  return widget;
}

} // namespace app

static int convert_align_value_to_flags(const char *value)
{
  char *tok, *ptr = base_strdup(value);
  int flags = 0;

  for (tok=ustrtok(ptr, " ");
       tok != NULL;
       tok=ustrtok(NULL, " ")) {
    if (ustrcmp(tok, "horizontal") == 0) {
      flags |= JI_HORIZONTAL;
    }
    else if (ustrcmp(tok, "vertical") == 0) {
      flags |= JI_VERTICAL;
    }
    else if (ustrcmp(tok, "left") == 0) {
      flags |= JI_LEFT;
    }
    else if (ustrcmp(tok, "center") == 0) {
      flags |= JI_CENTER;
    }
    else if (ustrcmp(tok, "right") == 0) {
      flags |= JI_RIGHT;
    }
    else if (ustrcmp(tok, "top") == 0) {
      flags |= JI_TOP;
    }
    else if (ustrcmp(tok, "middle") == 0) {
      flags |= JI_MIDDLE;
    }
    else if (ustrcmp(tok, "bottom") == 0) {
      flags |= JI_BOTTOM;
    }
    else if (ustrcmp(tok, "homogeneous") == 0) {
      flags |= JI_HOMOGENEOUS;
    }
  }

  base_free(ptr);
  return flags;
}

static bool bool_attr_is_true(const TiXmlElement* elem, const char* attribute_name)
{
  const char* value = elem->Attribute(attribute_name);

  return (value != NULL) && (strcmp(value, "true") == 0);
}
