/***************************************************************************
                          kcategoriesview.cpp  -  description
                             -------------------
    begin                : Sun Jan 20 2002
    copyright            : (C) 2000-2002 by Michael Edwardes <mte@users.sourceforge.net>
                           Javier Campos Morales <javi_c@users.sourceforge.net>
                           Felix Rodriguez <frodriguez@users.sourceforge.net>
                           John C <thetacoturtle@users.sourceforge.net>
                           Thomas Baumgart <ipwizard@users.sourceforge.net>
                           Kevin Tambascio <ktambascio@users.sourceforge.net>
                           (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "kcategoriesview_p.h"

#include <typeinfo>

// ----------------------------------------------------------------------------
// QT Includes

#include <QTimer>
#include <QBitArray>
#include <QMenu>

// ----------------------------------------------------------------------------
// KDE Includes

#include <KMessageBox>

// ----------------------------------------------------------------------------
// Project Includes

#include "mymoneyexception.h"
#include "kmymoneysettings.h"
#include "knewaccountdlg.h"
#include "kcategoryreassigndlg.h"
#include "mymoneyschedule.h"
#include "mymoneybudget.h"
#include "mymoneytransaction.h"
#include "mymoneytransactionfilter.h"
#include "mymoneyenums.h"
#include "storageenums.h"
#include "menuenums.h"

using namespace Icons;

KCategoriesView::KCategoriesView(QWidget *parent) :
    KMyMoneyAccountsViewBase(*new KCategoriesViewPrivate(this), parent)
{
  Q_D(KCategoriesView);
  d->ui->setupUi(this);

  connect(pActions[eMenu::Action::NewCategory],    &QAction::triggered, this, &KCategoriesView::slotNewCategory);
  connect(pActions[eMenu::Action::EditCategory],   &QAction::triggered, this, &KCategoriesView::slotEditCategory);
  connect(pActions[eMenu::Action::DeleteCategory], &QAction::triggered, this, &KCategoriesView::slotDeleteCategory);
}

KCategoriesView::~KCategoriesView()
{
}

void KCategoriesView::executeCustomAction(eView::Action action)
{
  switch(action) {
    case eView::Action::Refresh:
      refresh();
      break;

    case eView::Action::SetDefaultFocus:
      {
        Q_D(KCategoriesView);
        QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus()));
      }
      break;

    default:
      break;
  }
}

void KCategoriesView::refresh()
{
  Q_D(KCategoriesView);
  if (!isVisible()) {
    d->m_needsRefresh = true;
    return;
  }
  d->m_needsRefresh = false;

  d->m_proxyModel->invalidate();
  d->m_proxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts());

  // reinitialize the default state of the hidden categories label
  d->m_haveUnusedCategories = false;
  d->ui->m_hiddenCategories->hide();
  d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory());
}

void KCategoriesView::showEvent(QShowEvent * event)
{
  Q_D(KCategoriesView);
  if (!d->m_proxyModel)
    d->init();

  emit customActionRequested(View::Categories, eView::Action::AboutToShow);

  if (d->m_needsRefresh)
    refresh();

  // don't forget base class implementation
  QWidget::showEvent(event);
}

void KCategoriesView::updateActions(const MyMoneyObject& obj)
{
  Q_D(KCategoriesView);
  if (typeid(obj) != typeid(MyMoneyAccount) &&
      (obj.id().isEmpty() && d->m_currentCategory.id().isEmpty())) // do not disable actions that were already disabled))
    return;

  const auto& acc = static_cast<const MyMoneyAccount&>(obj);

  if (d->m_currentCategory.id().isEmpty() && acc.id().isEmpty())
    return;

  switch (acc.accountType()) {
    case eMyMoney::Account::Type::Income:
    case eMyMoney::Account::Type::Expense:
    {
      const auto file = MyMoneyFile::instance();
      auto b = file->isStandardAccount(acc.id()) ? false : true;
      pActions[eMenu::Action::EditCategory]->setEnabled(b);
      // enable delete action, if category/account itself is not referenced
      // by any object except accounts, because we want to allow
      // deleting of sub-categories. Also, we allow transactions, schedules and budgets
      // to be present because we can re-assign them during the delete process
      QBitArray skip((int)eStorage::Reference::Count);
      skip.fill(false);
      skip.setBit((int)eStorage::Reference::Transaction);
      skip.setBit((int)eStorage::Reference::Account);
      skip.setBit((int)eStorage::Reference::Schedule);
      skip.setBit((int)eStorage::Reference::Budget);

      pActions[eMenu::Action::DeleteCategory]->setEnabled(b && !file->isReferenced(acc, skip));
      d->m_currentCategory = acc;
      break;
    }
    default:
      pActions[eMenu::Action::EditCategory]->setEnabled(false);
      pActions[eMenu::Action::DeleteCategory]->setEnabled(false);
      d->m_currentCategory = MyMoneyAccount();
      break;
  }
}

/**
  * The view is notified that an unused income expense account has been hidden.
  */
void KCategoriesView::slotUnusedIncomeExpenseAccountHidden()
{
  Q_D(KCategoriesView);
  d->m_haveUnusedCategories = true;
  d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories);
}

void KCategoriesView::slotProfitChanged(const MyMoneyMoney &profit)
{
  Q_D(KCategoriesView);
  d->netBalProChanged(profit, d->ui->m_totalProfitsLabel, View::Categories);
}

void KCategoriesView::slotShowCategoriesMenu(const MyMoneyAccount& acc)
{
  Q_UNUSED(acc);
  pMenus[eMenu::Menu::Category]->exec(QCursor::pos());
}

void KCategoriesView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent)
{
  switch(intent) {
    case eView::Intent::UpdateActions:
      updateActions(obj);
      break;

    case eView::Intent::OpenContextMenu:
      slotShowCategoriesMenu(static_cast<const MyMoneyAccount&>(obj));
      break;

    default:
      break;
  }
}

void KCategoriesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent)
{
  switch (intent) {
    case eView::Intent::UpdateProfit:
      if (variant.count() == 1)
        slotProfitChanged(variant.first().value<MyMoneyMoney>());
      break;
    default:
      break;
  }
}

void KCategoriesView::slotNewCategory()
{
  Q_D(KCategoriesView);
  MyMoneyAccount parent;
  MyMoneyAccount account;

  // Preselect the parent account by looking at the current selected account/category
  if (!d->m_currentCategory.id().isEmpty() &&
      d->m_currentCategory.isIncomeExpense()) {
    try {
      parent = MyMoneyFile::instance()->account(d->m_currentCategory.id());
    } catch (const MyMoneyException &) {
    }
  }

  KNewAccountDlg::createCategory(account, parent);
}

void KCategoriesView::slotEditCategory()
{
  Q_D(KCategoriesView);
  if (d->m_currentCategory.id().isEmpty())
    return;

  const auto file = MyMoneyFile::instance();
  if (file->isStandardAccount(d->m_currentCategory.id()))
    return;

  switch (d->m_currentCategory.accountType()) {
    case eMyMoney::Account::Type::Income:
    case eMyMoney::Account::Type::Expense:
      break;
    default:
      return;
  }

  // set a status message so that the application can't be closed until the editing is done
  //        slotStatusMsg(caption);

  QPointer<KNewAccountDlg> dlg =
      new KNewAccountDlg(d->m_currentCategory, true, true, 0, i18n("Edit category '%1'", d->m_currentCategory.name()));

  dlg->setOpeningBalanceShown(false);
  dlg->setOpeningDateShown(false);

  if (dlg && dlg->exec() == QDialog::Accepted) {
    try {
      MyMoneyFileTransaction ft;

      auto account = dlg->account();
      auto parent = dlg->parentAccount();

      // we need to modify first, as reparent would override all other changes
      file->modifyAccount(account);
      if (account.parentAccountId() != parent.id())
        file->reparentAccount(account, parent);

      ft.commit();

      // reload the account object as it might have changed in the meantime
      emit selectByObject(account, eView::Intent::None);

    } catch (const MyMoneyException &e) {
      KMessageBox::error(this, i18n("Unable to modify category '%1'. Cause: %2", d->m_currentCategory.name(), QString::fromLatin1(e.what())));
    }
  }

  delete dlg;
  //        ready();
}

void KCategoriesView::slotDeleteCategory()
{
    Q_D(KCategoriesView);
    if (d->m_currentCategory.id().isEmpty())
      return;  // need an account ID

    const auto file = MyMoneyFile::instance();
    // can't delete standard accounts or account which still have transactions assigned
    if (file->isStandardAccount(d->m_currentCategory.id()))
      return;

    // check if the account is referenced by a transaction or schedule
    QBitArray skip((int)eStorage::Reference::Count);
    skip.fill(false);
    skip.setBit((int)eStorage::Reference::Account);
    skip.setBit((int)eStorage::Reference::Institution);
    skip.setBit((int)eStorage::Reference::Payee);
    skip.setBit((int)eStorage::Reference::Tag);
    skip.setBit((int)eStorage::Reference::Security);
    skip.setBit((int)eStorage::Reference::Currency);
    skip.setBit((int)eStorage::Reference::Price);
    const auto hasReference = file->isReferenced(d->m_currentCategory, skip);

    // if we get here and still have transactions referencing the account, we
    // need to check with the user to possibly re-assign them to a different account
    auto needAskUser = true;

    MyMoneyFileTransaction ft;

    if (hasReference) {
      // show transaction reassignment dialog

      needAskUser = false;
      auto dlg = new KCategoryReassignDlg(this);
      auto categoryId = dlg->show(d->m_currentCategory);
      delete dlg; // and kill the dialog
      if (categoryId.isEmpty())
        return; // the user aborted the dialog, so let's abort as well

      auto newCategory = file->account(categoryId);
      try {
        {
          //        KMSTATUS(i18n("Adjusting transactions..."));
          /*
            d->m_currentCategory.id() is the old id, categoryId the new one
            Now search all transactions and schedules that reference d->m_currentCategory.id()
            and replace that with categoryId.
          */
          // get the list of all transactions that reference the old account
          MyMoneyTransactionFilter filter(d->m_currentCategory.id());
          filter.setReportAllSplits(false);
          QList<MyMoneyTransaction> tlist;
          QList<MyMoneyTransaction>::iterator it_t;
          file->transactionList(tlist, filter);

          //        slotStatusProgressBar(0, tlist.count());
//          int cnt = 0;
          for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) {
            //          slotStatusProgressBar(++cnt, 0);
            MyMoneyTransaction t = (*it_t);
            if (t.replaceId(categoryId, d->m_currentCategory.id()))
              file->modifyTransaction(t);
          }
          //        slotStatusProgressBar(tlist.count(), 0);
        }
        // now fix all schedules
        {
          //        KMSTATUS(i18n("Adjusting scheduled transactions..."));
          QList<MyMoneySchedule> slist = file->scheduleList(d->m_currentCategory.id());
          QList<MyMoneySchedule>::iterator it_s;

//          int cnt = 0;
          //        slotStatusProgressBar(0, slist.count());
          for (it_s = slist.begin(); it_s != slist.end(); ++it_s) {
            //          slotStatusProgressBar(++cnt, 0);
            MyMoneySchedule sch = (*it_s);
            if (sch.replaceId(categoryId, d->m_currentCategory.id())) {
              file->modifySchedule(sch);
            }
          }
          //        slotStatusProgressBar(slist.count(), 0);
        }
        // now fix all budgets
        {
          //        KMSTATUS(i18n("Adjusting budgets..."));
          QList<MyMoneyBudget> blist = file->budgetList();
          QList<MyMoneyBudget>::const_iterator it_b;
          for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) {
            if ((*it_b).hasReferenceTo(d->m_currentCategory.id())) {
              MyMoneyBudget b = (*it_b);
              MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_currentCategory.id());
              MyMoneyBudget::AccountGroup toBudget = b.account(categoryId);
              toBudget += fromBudget;
              b.setAccount(toBudget, categoryId);
              b.removeReference(d->m_currentCategory.id());
              file->modifyBudget(b);

            }
          }
          //        slotStatusProgressBar(blist.count(), 0);
        }
      } catch (MyMoneyException &e) {
        KMessageBox::error(this, i18n("Unable to exchange category <b>%1</b> with category <b>%2</b>. Reason: %3", d->m_currentCategory.name(), newCategory.name(), QString::fromLatin1(e.what())));
        //    slotStatusProgressBar(-1, -1);
        return;
      }
      //    slotStatusProgressBar(-1, -1);
    }

    // retain the account name for a possible later usage in the error message box
    // since the account removal notifies the views the selected account can be changed
    // so we make sure by doing this that we display the correct name in the error message
    auto selectedAccountName = d->m_currentCategory.name();

    // at this point, we must not have a reference to the account
    // to be deleted anymore
    // special handling for categories to allow deleting of empty subcategories
    {
      // open a compound statement here to be able to declare variables
      // which would otherwise not work within a case label.

      // case A - only a single, unused category without subcats selected
      if (d->m_currentCategory.accountList().isEmpty()) {
        if (!needAskUser || (KMessageBox::questionYesNo(this, i18n("<qt>Do you really want to delete category <b>%1</b>?</qt>", selectedAccountName)) == KMessageBox::Yes)) {
          try {
            file->removeAccount(d->m_currentCategory);
            d->m_currentCategory.clearId();
            emit selectByObject(d->m_currentCategory, eView::Intent::None);
            ft.commit();
          } catch (const MyMoneyException &e) {
            KMessageBox::error(this, i18n("<qt>Unable to delete category <b>%1</b>. Cause: %2</qt>", selectedAccountName, QString::fromLatin1(e.what())));
          }
        }
        return;
      }
      // case B - we have some subcategories, maybe the user does not want to
      //          delete them all, but just the category itself?
      auto parentAccount = file->account(d->m_currentCategory.parentAccountId());

      QStringList accountsToReparent;
      int result = KMessageBox::questionYesNoCancel(this,
                                                    i18n("<qt>Do you want to delete category <b>%1</b> with all its sub-categories or only "
                                                         "the category itself? If you only delete the category itself, all its sub-categories "
                                                         "will be made sub-categories of <b>%2</b>.</qt>", selectedAccountName, parentAccount.name()),
                                                    QString(),
                                                    KGuiItem(i18n("Delete all")),
                                                    KGuiItem(i18n("Just the category")));
      if (result == KMessageBox::Cancel)
        return; // cancel pressed? ok, no delete then...
      // "No" means "Just the category" and that means we need to reparent all subaccounts
      bool need_confirmation = false;
      // case C - User only wants to delete the category itself
      if (result == KMessageBox::No)
        accountsToReparent = d->m_currentCategory.accountList();
      else {
        // case D - User wants to delete all subcategories, now check all subcats of
        //          d->m_currentCategory and remember all that cannot be deleted and
        //          must be "reparented"
        foreach (const auto accountID, d->m_currentCategory.accountList()) {
          // reparent account if a transaction is assigned
          if (file->transactionCount(accountID) != 0)
            accountsToReparent.push_back(accountID);
          else if (!file->account(accountID).accountList().isEmpty()) {
            // or if we have at least one sub-account that is used for transactions
            if (!file->hasOnlyUnusedAccounts(file->account(accountID).accountList())) {
              accountsToReparent.push_back(accountID);
              //qDebug() << "subaccount not empty";
            }
          }
        }
        if (!accountsToReparent.isEmpty())
          need_confirmation = true;
      }
      if (!accountsToReparent.isEmpty() && need_confirmation) {
        if (KMessageBox::questionYesNo(this, i18n("<p>Some sub-categories of category <b>%1</b> cannot "
                                                  "be deleted, because they are still used. They will be made sub-categories of <b>%2</b>. Proceed?</p>", selectedAccountName, parentAccount.name())) != KMessageBox::Yes) {
          return; // user gets wet feet...
        }
      }
      // all good, now first reparent selected sub-categories
      try {
        auto parent = file->account(d->m_currentCategory.parentAccountId());
        for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) {
          auto child = file->account(*it);
          file->reparentAccount(child, parent);
        }
        // reload the account because the sub-account list might have changed
        d->m_currentCategory = file->account(d->m_currentCategory.id());
        // now recursively delete remaining sub-categories
        file->removeAccountList(d->m_currentCategory.accountList());
        // don't forget to update d->m_currentCategory, because we still have a copy of
        // the old account list, which is no longer valid
        d->m_currentCategory = file->account(d->m_currentCategory.id());
      } catch (const MyMoneyException &e) {
        KMessageBox::error(this, i18n("<qt>Unable to delete a sub-category of category <b>%1</b>. Reason: %2</qt>", selectedAccountName, QString::fromLatin1(e.what())));
        return;
      }
    }
    // the category/account is deleted after the switch


    try {
      file->removeAccount(d->m_currentCategory);
      d->m_currentCategory.clearId();
      emit selectByObject(MyMoneyAccount(), eView::Intent::None);
      ft.commit();
    } catch (const MyMoneyException &e) {
      KMessageBox::error(this, i18n("Unable to delete category '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what())));
    }
}

