/*
 * Copyright (C) 2008 Emweb bv, Herent, Belgium.
 *
 * See the LICENSE file for terms of use.
 */

#include "Wt/WAnchor.h"
#include "Wt/WApplication.h"
#include "Wt/WBootstrap5Theme.h"
#include "Wt/WCheckBox.h"
#include "Wt/WContainerWidget.h"
#include "Wt/WEnvironment.h"
#include "Wt/WException.h"
#include "Wt/WLabel.h"
#include "Wt/WMenuItem.h"
#include "Wt/WMenu.h"
#include "Wt/WPopupMenu.h"
#include "Wt/WSignal.h"
#include "Wt/WStackedWidget.h"
#include "Wt/WText.h"
#include "Wt/WTheme.h"

#include "StdWidgetItemImpl.h"

#include <algorithm>
#include <cctype>
#include <memory>

#include <Wt/WLogger.h>

namespace Wt {

LOGGER("WMenuItem");

WMenuItem::WMenuItem(const WString& text, std::unique_ptr<WWidget> contents,
                     ContentLoading policy)
  : separator_(false)
{
  create(std::string(), text, std::move(contents), policy);
}

WMenuItem::WMenuItem(const std::string& iconPath, const WString& text,
                     std::unique_ptr<WWidget> contents, ContentLoading policy)
  : separator_(false)
{
  create(iconPath, text, std::move(contents), policy);
}

WMenuItem::WMenuItem(bool separator, const WString& text)
  : separator_(true)
{
  create(std::string(), WString::Empty, nullptr, ContentLoading::Lazy);

  separator_ = separator;
  selectable_ = false;
  internalPathEnabled_ = false;

  if (!text.empty()) {
    text_ = addWidget(std::make_unique<WLabel>());
    text_->setTextFormat(TextFormat::Plain);
    text_->setText(text);
  }
}

void WMenuItem::create(const std::string& iconPath, const WString& text,
                       std::unique_ptr<WWidget> contents, ContentLoading policy)
{
  customLink_ = false;

  menu_ = nullptr;
  customPathComponent_ = false;
  internalPathEnabled_ = true;
  closeable_ = false;
  selectable_ = true;
  selectConnected_ = false;
  menuItemCheckedConnected_ = false;
  selected_ = false;
  setThemeStyle_ = false;
  textChanged_ = true;

  text_ = nullptr;
  icon_ = nullptr;
  checkBox_ = nullptr;
  subMenu_ = nullptr;
  data_ = nullptr;

  setContents(std::move(contents), policy);

  if (!separator_) {
    addWidget(std::make_unique<WAnchor>());
    updateInternalPath();
  }

  signalsConnected_ = false;

  if (!iconPath.empty())
    setIcon(iconPath);

  if (!separator_)
    setText(text);
}

WMenuItem::~WMenuItem()
{ }

void WMenuItem::setContents(std::unique_ptr<WWidget> contents,
                            ContentLoading policy)
{
  int menuIdx = -1;
  WMenu *menu = menu_;
  std::unique_ptr<WMenuItem> self;
  if (menu) {
    menuIdx = menu->indexOf(this);
    self = menu->removeItem(this);
  }

  uContents_ = std::move(contents);
  oContents_ = uContents_.get();
  loadPolicy_ = policy;

  if (uContents_ && loadPolicy_ == ContentLoading::Lazy) {
     if (!oContentsContainer_) {
       uContentsContainer_.reset(new WContainerWidget());
       oContentsContainer_ = uContentsContainer_.get();
       oContentsContainer_
         ->setJavaScriptMember("wtResize",
                               StdWidgetItemImpl::childrenResizeJS());

       oContentsContainer_->resize(WLength::Auto,
                                  WLength(100, LengthUnit::Percentage));
     }
   }

  if (menu) {
    menu->insertItem(menuIdx, std::move(self));
  }
}

bool WMenuItem::isSectionHeader() const
{
  WAnchor *a = anchor();
  return !separator_ && !a && !subMenu_ && text_;
}

WAnchor *WMenuItem::anchor() const
{
  for (int i = 0; i < count(); ++i) {
    WAnchor *result = dynamic_cast<WAnchor *>(widget(i));
    if (result)
      return result;
  }

  return nullptr;
}

void WMenuItem::setIcon(const std::string& path)
{
  if (!icon_) {
    WAnchor *a = anchor();
    if (!a)
      return;

    icon_ = a->insertWidget(0, std::make_unique<WText>(" "));

    WApplication *app = WApplication::instance();
    app->theme()->apply(this, icon_, MenuItemIcon);
  }

  icon_->decorationStyle().setBackgroundImage(WLink(path));
}

void WMenuItem::setBadge(std::unique_ptr<WBadge> badge)
{
  WAnchor *a = anchor();
  if (!a)
    return;

  if (badge_) {
    a->removeWidget(badge_);
  }
  badge_ = badge.get();
  if (badge) {
    a->addWidget(std::move(badge));
  }
}

std::string WMenuItem::icon() const
{
  if (icon_)
    return icon_->decorationStyle().backgroundImage();
  else
    return std::string();
}

void WMenuItem::setText(const WString& text)
{
  if (!text_) {
    text_ = anchor()->insertWidget(icon_ ? 1 : 0, std::make_unique<WLabel>());
    text_->setTextFormat(TextFormat::Plain);
  }

  text_->setText(text);
  textChanged_ = true;

  if (!customPathComponent_) {
    updateInternalPath();
    if (menu_) {
      menu_->itemPathChanged(this);
    }
  }
}

WString WMenuItem::text() const
{
  if (text_)
    return text_->text();
  else
    return WString::Empty;
}

std::string WMenuItem::pathComponent() const
{
  if (!customPathComponent_ && textChanged_) {
    pathComponent_ = pathComponentFromText();
    textChanged_ = false;
  }
  return pathComponent_;
}

void WMenuItem::setInternalPathEnabled(bool enabled)
{
  internalPathEnabled_ = enabled;
  updateInternalPath();
}

bool WMenuItem::internalPathEnabled() const
{
  return internalPathEnabled_;
}

std::string WMenuItem::pathComponentFromText() const
{
  std::string result;
  WString t = text();

  if (t.literal())
    result = t.narrow();
  else
    result = t.key();

  for (unsigned i = 0; i < result.length(); ++i) {
    if (std::isspace((unsigned char)result[i]))
      result[i] = '-';
    else if (std::isalnum((unsigned char)result[i]))
      result[i] = std::tolower((unsigned char)result[i]);
    else
      result[i] = '_';
  }

  return result;
}

void WMenuItem::setLink(const WLink& link)
{
  WAnchor *a = anchor();
  if (a)
    a->setLink(link);

  customLink_ = true;
}

WLink WMenuItem::link() const
{
  WAnchor *a = anchor();
  if (a)
    return a->link();
  else
    return WLink();
}

void WMenuItem::updateInternalPath()
{
  if (menu_ && menu_->internalPathEnabled() && internalPathEnabled()) {
    std::string internalPath = menu_->internalBasePath() + pathComponent();
    WLink link(LinkType::InternalPath, internalPath);
    WAnchor *a = anchor();
    if (a)
      a->setLink(link);
  } else {
    WAnchor *a = anchor();
    if (a && !customLink_) {
      if (WApplication::instance()->environment().agent() ==
          UserAgent::IE6)
        a->setLink(WLink("#"));
      else
        a->setLink(WLink());
    }
  }
}

void WMenuItem::setPathComponent(const std::string& path)
{
  customPathComponent_ = true;
  pathComponent_ = path;

  updateInternalPath();
  if (menu_)
    menu_->itemPathChanged(this);
}

void WMenuItem::setSelectable(bool selectable)
{
  selectable_ = selectable;
}

void WMenuItem::setCloseable(bool closeable)
{
  if (closeable_ != closeable) {
    closeable_ = closeable;

    if (closeable_) {
      WText *closeIcon = insertWidget(0, std::make_unique<WText>(""));
      WApplication *app = WApplication::instance();
      app->theme()->apply(this, closeIcon, MenuItemClose);

      closeIcon->clicked().connect(this, &WMenuItem::close);
    } else
      removeWidget(widget(0));
  }
}

void WMenuItem::setCheckable(bool checkable)
{
  if (isCheckable() != checkable) {
    if (checkable) {
      checkBox_ = anchor()->insertWidget(0, std::make_unique<WCheckBox>());
      setText(text());

      text_->setBuddy(checkBox_);

      WApplication *app = WApplication::instance();
      app->theme()->apply(this, checkBox_, MenuItemCheckBox);

      signalsConnected_ = false;
      repaint();
    } else {
      anchor()->removeWidget(checkBox_);
      checkBox_ = nullptr;
    }
  }
}

void WMenuItem::setChecked(bool checked)
{
  if (isCheckable()) {
    WCheckBox *cb = dynamic_cast<WCheckBox *>(anchor()->widget(0));
    cb->setChecked(checked);
  }
}

bool WMenuItem::isChecked() const
{
  if (isCheckable()) {
    WCheckBox *cb = dynamic_cast<WCheckBox *>(anchor()->widget(0));
    return cb->isChecked();
  } else
    return false;
}

void WMenuItem::close()
{
  if (menu_)
    menu_->close(this);
}

void WMenuItem::enableAjax()
{
  if (menu_->internalPathEnabled())
    resetLearnedSlots();

  if (uContents_)
    uContents_->enableAjax();

  WContainerWidget::enableAjax();
}

void WMenuItem::setDisabled(bool disabled)
{
  WContainerWidget::setDisabled(disabled);

  if (disabled)
    if (menu_ && !menu_->isDisabled())
      menu_->onItemHidden(menu_->indexOf(this), true);
}

void WMenuItem::setHidden(bool hidden,
                          const WAnimation& animation)
{
  WContainerWidget::setHidden(hidden, animation);

  if (hidden)
    if (menu_ && !menu_->isHidden())
      menu_->onItemHidden(menu_->indexOf(this), true);
}

std::unique_ptr<WWidget> WMenuItem::removeWidget(WWidget* widget)
{
  if (badge_ && widget == badge_) {
    WAnchor *a = anchor();
    if (a) {
      badge_ = nullptr;
      return a->removeWidget(widget);
    }
  }
  return WContainerWidget::removeWidget(widget);
}

void WMenuItem::render(WFlags<RenderFlag> flags)
{
  if (isThemeStyleEnabled()) {
    setThemeStyle_ = true;
    renderSelected(selected_);
  }
  connectSignals();

  WContainerWidget::render(flags);
}

void WMenuItem::renderSelected(bool selected)
{
  selected_ = selected;

  if (setThemeStyle_) {
    WApplication *app = WApplication::instance();

    std::string active = app->theme()->activeClass();

    auto bs5Theme = std::dynamic_pointer_cast<WBootstrap5Theme>(app->theme());

    if (active == "Wt-selected"){ // for CSS theme, our styles are messed up
      removeStyleClass(!selected ? "itemselected" : "item", true);
      addStyleClass(selected ? "itemselected" : "item", true);
    } else {
      if (bs5Theme) {
        auto a = anchor();
        if (a) {
          a->toggleStyleClass(active, selected, true);
        }
      }
      toggleStyleClass(active, selected, true);
    }
  }
}

void WMenuItem::selectNotLoaded()
{
  if (!contentsLoaded())
    select();
}

bool WMenuItem::contentsLoaded() const
{
  return oContents_ && !uContents_;
}

void WMenuItem::loadContents()
{
  if (!uContents_)
    return;
  else if (!contentsLoaded()) {
    oContentsContainer_->addWidget(std::move(uContents_));
    uContents_.reset();
    signalsConnected_ = false;
    connectSignals();
  }
}

void WMenuItem::connectSignals()
{
  if (!signalsConnected_) {
    signalsConnected_ = true;

    if (!oContents_ || contentsLoaded())
      implementStateless(&WMenuItem::selectVisual,
                         &WMenuItem::undoSelectVisual);

    WAnchor *a = anchor();

    if (a) {
      SignalBase *as;
      bool selectFromCheckbox = false;

      if (checkBox_ && !checkBox_->clicked().propagationPrevented()) {
        as = &checkBox_->changed();
        /*
         * Because the checkbox is not a properly exposed form object,
         * we need to relay its value ourselves
         */
        checkBox_->checked().connect(this, &WMenuItem::setCheckBox);
        checkBox_->unChecked().connect(this, &WMenuItem::setUnCheckBox);
        selectFromCheckbox = true;
      } else {
        as = &a->clicked();
      }

      if (checkBox_) {
        a->setLink(WLink());
      }

      if (uContents_) {
        as->connect(this, &WMenuItem::selectNotLoaded);
      } else {
        if (!menuItemCheckedConnected_ && !selectConnected_) {
          as->connect(this, &WMenuItem::selectVisual);
        }
        if (!selectFromCheckbox && !selectConnected_) {
          as->connect(this, &WMenuItem::select);
          selectConnected_ = true;
        } else if (selectFromCheckbox && !menuItemCheckedConnected_) {
          /*  #12367: Fixes a bug where the checkbox of a menu item is not
          *  clicked if the menu item itself is clicked, but the checkbox
          *  or its label were not clicked.
          */
          (&a->clicked())->connect(this, &WMenuItem::menuItemCheckedPropagate);
          menuItemCheckedConnected_ = true;
        }
      }
    }
  }
}

void WMenuItem::setCheckBox()
{
  setChecked(true);
  select();
}

void WMenuItem::setUnCheckBox()
{
  setChecked(false);
  select();
}

void WMenuItem::menuItemCheckedPropagate(){
  if (isCheckable()) {
    if (isChecked()){
      checkBox_->unChecked().emit();
    }
    else{
      checkBox_->checked().emit();
    }
  }
}

void WMenuItem::setParentMenu(WMenu *menu)
{
  menu_ = menu;

  updateInternalPath();
  updateMenuZIndex();
}

WWidget *WMenuItem::contents() const
{
  return oContents_.get();
}

WWidget *WMenuItem::contentsInStack() const
{
  if (oContentsContainer_ && !uContentsContainer_)
    return oContentsContainer_.get();
  else if (oContents_ && !uContents_)
    return oContents_.get();
  else
    return nullptr;
}

std::unique_ptr<WWidget> WMenuItem::removeContents()
{
  auto contents = oContents_.get();
  oContents_.reset();

  WWidget *c = contentsInStack();

  if (c) {
    /* Remove from stack */
    std::unique_ptr<WWidget> w = c->parent()->removeWidget(c);

    if (oContentsContainer_)
      return oContentsContainer_->removeWidget(contents);
    else
      return w;
  } else {
    auto result = std::move(uContents_);
    uContents_.reset();
    return result;
  }
}

std::unique_ptr<WWidget> WMenuItem::takeContentsForStack()
{
  if (!uContents_)
    return nullptr;
  else {
    if (loadPolicy_ == ContentLoading::Lazy) {
      std::unique_ptr<WWidget> result = std::move(uContentsContainer_);
      uContentsContainer_.reset();
      return result;
    } else {
      auto result = std::move(uContents_);
      uContents_.reset();
      return result;
    }
  }
}

void WMenuItem::returnContentsInStack(std::unique_ptr<WWidget> widget)
{
  if (oContentsContainer_) {
    if (!uContents_)
      uContents_ = oContentsContainer_->removeWidget(oContents_.get());

    WWidget* realWidget = widget.release();
    WContainerWidget* uContentsContainer = dynamic_cast<WContainerWidget*>(realWidget);
    if (uContentsContainer) {
      uContentsContainer_ = std::unique_ptr<WContainerWidget>(uContentsContainer);
      oContentsContainer_ = uContentsContainer_.get();
    } else {
      delete realWidget;
      oContentsContainer_ = nullptr;
      LOG_ERROR("returnContentsInStack: widget is not oContentsContainer_. This should not happen.");
    }
  } else
    uContents_ = std::move(widget);
}

void WMenuItem::setFromInternalPath(const std::string& path)
{
  if (internalPathEnabled() &&
      menu_->contentsStack_ &&
      menu_->contentsStack_->currentWidget() != contents())
    menu_->select(menu_->indexOf(this), false);

  if (subMenu_ && subMenu_->internalPathEnabled())
    subMenu_->internalPathChanged(path);
}

void WMenuItem::select()
{
  if (menu_ && selectable_ && !isDisabled())
    menu_->select(this);
}

void WMenuItem::selectVisual()
{
  if (menu_ && selectable_)
    menu_->selectVisual(this);
}

void WMenuItem::undoSelectVisual()
{
  if (menu_ && selectable_)
    menu_->undoSelectVisual();
}

void WMenuItem::setMenu(std::unique_ptr<WMenu> menu)
{
  WApplication* app = WApplication::instance();
  subMenu_ = menu.get();
  subMenu_->parentItem_ = this;

  WPopupMenu *popup = dynamic_cast<WPopupMenu *>(subMenu_);
  if (popup) {
    app->removeGlobalWidget(menu.get());
  }
  addWidget(std::move(menu));

  updateMenuZIndex();

  if (popup) {
    setSelectable(false);
    popup->setButton(anchor());
    updateInternalPath();
    // WPopupMenus are hidden by default, 'show' this WPopupMenu
    // but not really, since the parent is still hidden. This fixes
    // an issue where child widgets would remain unexposed, even
    // though this submenu was open (e.g. in a submenu where items
    // are checkable)
    if (dynamic_cast<WPopupMenu*>(menu_)) {
      if (app->environment().positionAnchorSupported()) {
        popup->anchorAt(anchor(), Orientation::Horizontal);
      }
      popup->show();
    }
  }
}

void WMenuItem::updateMenuZIndex()
{
  if (!subMenu_ || !subMenu_->isPopup() ||
      !menu_ || !menu_->isPopup()) {
    return;
  }

  int currentZIndex = subMenu_->zIndex();
  int newZIndex = std::max(menu_->zIndex() + 1000, currentZIndex);

  if (newZIndex != currentZIndex) {
    subMenu_->webWidget()->setZIndex(newZIndex);

    for (WMenuItem* submenuItem : subMenu_->items()) {
      submenuItem->updateMenuZIndex();
    }
  }
}

void WMenuItem::setItemPadding(bool padding)
{
  if (!checkBox_ && !icon_) {
    WAnchor *a = anchor();
    if (a)
      a->toggleStyleClass("Wt-padded", padding);
  }
}

}
