/**
 * \file
 *
 * \brief Kate Close Except/Like plugin implementation
 *
 * Copyright (C) 2012 Alex Turbov <i.zaufi@gmail.com>
 *
 * \date Thu Mar  8 08:13:43 MSK 2012 -- Initial design
 */
/*
 * KateCloseExceptPlugin 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 3 of the License, or
 * (at your option) any later version.
 *
 * KateCloseExceptPlugin 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, see <http://www.gnu.org/licenses/>.
 */

// Project specific includes
#include "config.h"
#include "close_except_plugin.h"
#include "close_confirm_dialog.h"

// Standard includes
#include <kate/application.h>
#include <kate/documentmanager.h>
#include <kate/mainwindow.h>
#include <KAboutData>
#include <KActionCollection>
#include <KDebug>
#include <KPassivePopup>
#include <KPluginFactory>
#include <KPluginLoader>
#include <KTextEditor/Editor>
#include <QtCore/QFileInfo>

K_PLUGIN_FACTORY(CloseExceptPluginFactory, registerPlugin<kate::CloseExceptPlugin>();)
K_EXPORT_PLUGIN(
    CloseExceptPluginFactory(
        KAboutData(
            "katecloseexceptplugin"
          , "katecloseexceptplugin"
          , ki18n("Close Except/Like Plugin")
          , PLUGIN_VERSION
          , ki18n("Close all documents started from specified path")
          , KAboutData::License_LGPL_V3
          )
      )
  )

namespace kate {
//BEGIN CloseExceptPlugin
CloseExceptPlugin::CloseExceptPlugin(
    QObject* application
  , const QList<QVariant>&
  )
  : Kate::Plugin(static_cast<Kate::Application*>(application), "katecloseexceptplugin")
{
}

Kate::PluginView* CloseExceptPlugin::createView(Kate::MainWindow* parent)
{
    return new CloseExceptPluginView(parent, CloseExceptPluginFactory::componentData(), this);
}

void CloseExceptPlugin::readSessionConfig(KConfigBase* config, const QString& groupPrefix)
{
    KConfigGroup scg(config, groupPrefix + "menu");
    m_show_confirmation_needed = scg.readEntry("ShowConfirmation", true);
}

void CloseExceptPlugin::writeSessionConfig(KConfigBase* config, const QString& groupPrefix)
{
    KConfigGroup scg(config, groupPrefix + "menu");
    scg.writeEntry("ShowConfirmation", m_show_confirmation_needed);
    scg.sync();
}
//END CloseExceptPlugin

//BEGIN CloseExceptPluginView
CloseExceptPluginView::CloseExceptPluginView(
    Kate::MainWindow* mw
  , const KComponentData& data
  , CloseExceptPlugin* plugin
  )
  : Kate::PluginView(mw)
  , Kate::XMLGUIClient(data)
  , m_plugin(plugin)
  , m_show_confirmation_action(new KToggleAction(i18nc("@action:inmenu", "Show Confirmation"), this))
  , m_except_menu(new KActionMenu(
        i18nc("@action:inmenu close docs except the following...", "Close Except")
      , this
      ))
  , m_like_menu(new KActionMenu(
        i18nc("@action:inmenu close docs like the following...", "Close Like")
      , this
      ))
{
    actionCollection()->addAction("file_close_except", m_except_menu);
    actionCollection()->addAction("file_close_like", m_like_menu);

    // Subscribe self to document creation
    connect(
        m_plugin->application()->editor()
      , SIGNAL(documentCreated(KTextEditor::Editor*, KTextEditor::Document*))
      , this
      , SLOT(documentCreated(KTextEditor::Editor*, KTextEditor::Document*))
      );
    // Configure toggle action and connect it to update state
    m_show_confirmation_action->setChecked(m_plugin->showConfirmationNeeded());
    connect(
        m_show_confirmation_action
      , SIGNAL(toggled(bool))
      , m_plugin
      , SLOT(toggleShowConfirmation(bool))
      );
    //
    connect(
        mainWindow()
      , SIGNAL(viewCreated(KTextEditor::View*))
      , this
      , SLOT(viewCreated(KTextEditor::View*))
      );
    // Fill menu w/ currently opened document masks/groups
    updateMenu();

    mainWindow()->guiFactory()->addClient(this);
}

CloseExceptPluginView::~CloseExceptPluginView()
{
    mainWindow()->guiFactory()->removeClient(this);
}

void CloseExceptPluginView::viewCreated(KTextEditor::View* view)
{
    connectToDocument(view->document());
    updateMenu();
}

void CloseExceptPluginView::documentCreated(KTextEditor::Editor*, KTextEditor::Document* document)
{
    connectToDocument(document);
    updateMenu();
}

void CloseExceptPluginView::connectToDocument(KTextEditor::Document* document)
{
    // Subscribe self to document close and name changes
    connect(
        document
      , SIGNAL(aboutToClose(KTextEditor::Document*))
      , this
      , SLOT(updateMenuSlotStub(KTextEditor::Document*))
      );
    connect(
        document
      , SIGNAL(documentNameChanged(KTextEditor::Document*))
      , this
      , SLOT(updateMenuSlotStub(KTextEditor::Document*))
      );
    connect(
        document
      , SIGNAL(documentUrlChanged(KTextEditor::Document*))
      , this
      , SLOT(updateMenuSlotStub(KTextEditor::Document*))
      );
}

void CloseExceptPluginView::updateMenuSlotStub(KTextEditor::Document*)
{
    updateMenu();
}

void CloseExceptPluginView::appendActionsFrom(
    const std::set<QString>& paths
  , actions_map_type& actions
  , KActionMenu* menu
  , QSignalMapper* mapper
  )
{
    Q_FOREACH(const QString& path, paths)
    {
        QString action = path.startsWith('*') ? path : path + '*';
        actions[action] = QPointer<KAction>(new KAction(action, menu));
        menu->addAction(actions[action]);
        connect(actions[action], SIGNAL(triggered()), mapper, SLOT(map()));
        mapper->setMapping(actions[action], action);
    }
}

QPointer<QSignalMapper> CloseExceptPluginView::updateMenu(
    const std::set<QString>& paths
  , const std::set<QString>& masks
  , actions_map_type& actions
  , KActionMenu* menu
  )
{
    // turn menu ON or OFF depending on collected results
    menu->setEnabled(!paths.empty());

    // Clear previous menus
    for (actions_map_type::iterator it = actions.begin(), last = actions.end(); it !=last;)
    {
        menu->removeAction(*it);
        actions.erase(it++);
    }
    // Form a new one
    QPointer<QSignalMapper> mapper = QPointer<QSignalMapper>(new QSignalMapper(this));
    appendActionsFrom(paths, actions, menu, mapper);
    if (!masks.empty())
    {
        if (!paths.empty())
            menu->addSeparator();                           // Add separator between paths and file's ext filters
        appendActionsFrom(masks, actions, menu, mapper);
    }
    // Append 'Show Confirmation' toggle menu item
    menu->addSeparator();                                   // Add separator between paths and show confirmation
    menu->addAction(m_show_confirmation_action);
    return mapper;
}

void CloseExceptPluginView::updateMenu()
{
    const QList<KTextEditor::Document*>& docs = m_plugin->application()->documentManager()->documents();
    if (docs.size() < 2)
    {
        kDebug() << "No docs r (or the only) opened right now --> disable menu";
        m_except_menu->setEnabled(false);
        m_except_menu->addSeparator();
        m_like_menu->setEnabled(false);
        m_like_menu->addSeparator();
        /// \note It seems there is always a document present... it named \em 'Untitled'
    }
    else
    {
        // Iterate over documents and form a set of candidates
        typedef std::set<QString> paths_set_type;
        paths_set_type doc_paths;
        paths_set_type masks;
        Q_FOREACH(KTextEditor::Document* document, docs)
        {
            const QString& ext = QFileInfo(document->url().path()).completeSuffix();
            if (!ext.isEmpty())
                masks.insert("*." + ext);
            doc_paths.insert(document->url().upUrl().path());
        }
        paths_set_type paths = doc_paths;
        kDebug() << "stage #1: Collected" << paths.size() << "paths and" << masks.size() << "masks";
        // Add common paths to the collection
        for (paths_set_type::iterator it = doc_paths.begin(), last = doc_paths.end(); it != last; ++it)
        {
            for (
                KUrl url = *it
              ; url.hasPath() && url.path() != "/"
              ; url = url.upUrl()
              )
            {
                paths_set_type::iterator not_it = it;
                for (++not_it; not_it != last; ++not_it)
                    if (!not_it->startsWith(url.path()))
                        break;
                if (not_it == last)
                {
                    paths.insert(url.path());
                    break;
                }
            }
        }
        kDebug() << "stage #2: Collected" << paths.size() << "paths and" << masks.size() << "masks";
        //
        m_except_mapper = updateMenu(paths, masks, m_except_actions, m_except_menu);
        m_like_mapper = updateMenu(paths, masks, m_like_actions, m_like_menu);
        connect(m_except_mapper, SIGNAL(mapped(const QString&)), this, SLOT(closeExcept(const QString&)));
        connect(m_like_mapper, SIGNAL(mapped(const QString&)), this, SLOT(closeLike(const QString&)));
    }
}

void CloseExceptPluginView::close(const QString& item, const bool close_if_match)
{
    assert(
        "Parameter seems invalid! Is smth has changed in the code?"
      && !item.isEmpty() && (item[0] == '*' || item[item.size() - 1] == '*')
      );

    const bool is_path = item[0] != '*';
    const QString mask = is_path ? item.left(item.size() - 1) : item;
    kDebug() << "Going to close items [" << close_if_match << "/" << is_path << "]: " << mask;

    QList<KTextEditor::Document*> docs2close;
    const QList<KTextEditor::Document*>& docs = m_plugin->application()->documentManager()->documents();
    Q_FOREACH(KTextEditor::Document* document, docs)
    {
        const QString& path = document->url().upUrl().path();
        /// \note Take a dot in account, so \c *.c would not match for \c blah.kcfgc
        const QString& ext = '.' + QFileInfo(document->url().fileName()).completeSuffix();
        const bool match = (!is_path && mask.endsWith(ext))
          || (is_path && path.startsWith(mask))
          ;
        if (match == close_if_match)
        {
            kDebug() << "*** Will close: " << document->url();
            docs2close.push_back(document);
        }
    }
    if (docs2close.isEmpty())
    {
        KPassivePopup::message(
            i18nc("@title:window", "Error")
          , i18nc("@info:tooltip", "No files to close ...")
          , qobject_cast<QWidget*>(this)
          );
        return;
    }
    // Show confirmation dialog if needed
    const bool removeNeeded = !m_plugin->showConfirmationNeeded()
      || CloseConfirmDialog(docs2close, m_show_confirmation_action, qobject_cast<QWidget*>(this)).exec();
    if (removeNeeded)
    {
        if (docs2close.isEmpty())
        {
            KPassivePopup::message(
                i18nc("@title:window", "Error")
              , i18nc("@info:tooltip", "No files to close ...")
              , qobject_cast<QWidget*>(this)
              );
        }
        else
        {
            // Close 'em all!
            m_plugin->application()->documentManager()->closeDocumentList(docs2close);
            updateMenu();
            KPassivePopup::message(
                i18nc("@title:window", "Done")
              , i18np("%1 file closed", "%1 files closed", docs2close.size())
              , qobject_cast<QWidget*>(this)
              );
        }
    }
}
//END CloseExceptPluginView
}                                                           // namespace kate
