// broken_indicator.cc
//
//   Copyright (C) 2005, 2007 Daniel Burrows
//
//   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; see the file COPYING.  If not, write to
//   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
//   Boston, MA 02111-1307, USA.

#include "broken_indicator.h"

#include <aptitude.h>

#include <generic/apt/apt.h>
#include <generic/apt/aptcache.h>
#include <generic/apt/aptitude_resolver_universe.h>
#include <generic/apt/config_signal.h>
#include <generic/apt/resolver_manager.h>

#include <generic/problemresolver/exceptions.h>
#include <generic/problemresolver/solution.h>

#include <generic/util/util.h>

#include <vscreen/config/colors.h>
#include <vscreen/config/keybindings.h>
#include <vscreen/fragment.h>
#include <vscreen/vs_text_layout.h>
#include <vscreen/vscreen.h>

#include <apt-pkg/pkgsystem.h>

#include <string>
#include <vector>

using namespace std;

typedef generic_solution<aptitude_universe> aptitude_solution;

/** A simple indicator, usually placed at the bottom of the screen,
 *  that describes the current state of the problem resolver.  Hidden
 *  if no problem resolver is active.
 *
 *  \todo write a simple table fragment class and use that to
 *  right-justify the text that obviously should be
 */
class broken_indicator:public vs_text_layout
{
  aptitude_solution last_sol;

  /** Records whether we had generated all solutions at the time of
   *  the last update.
   */
  bool last_complete : 1;

  /** Records whether the background thread was active at the time of
   *  the last update.
   */
  bool last_background_active : 1;

  /** Tracks the phase of the visual "spinner". */
  int spin_count;

  void handle_cache_reload()
  {
    if(resman != NULL)
      resman->state_changed.connect(sigc::mem_fun(*this, &broken_indicator::post_update));

    update();
  }

protected:
  broken_indicator()
    :spin_count(0)
  {
    if(resman != NULL)
      resman->state_changed.connect(sigc::mem_fun(*this, &broken_indicator::post_update));

    cache_closed.connect(sigc::mem_fun(*this, &broken_indicator::update));
    cache_reloaded.connect(sigc::mem_fun(*this, &broken_indicator::handle_cache_reload));

    set_bg_style(get_style("Error"));

    update();

    vscreen_addtimeout(new slot_event(sigc::mem_fun(this, &broken_indicator::tick_timeout)),
		       aptcfg->FindI(PACKAGE "::Spin-Interval", 500));
  }

private:
  static fragment *key_hint_fragment(const resolver_manager::state &state)
  {
    wstring next=global_bindings.readable_keyname("NextSolution");
    wstring prev=global_bindings.readable_keyname("PrevSolution");
    wstring examine=global_bindings.readable_keyname("ExamineSolution");
    wstring apply=global_bindings.readable_keyname("ApplySolution");



    style st_normal;
    style st_disabled;
    st_disabled.attrs_off(A_BOLD);
    st_disabled.attrs_on(A_DIM);
    st_disabled.set_fg(COLOR_BLACK);

    vector<fragment *> key_hints;

    key_hints.push_back(fragf(_("%s: Examine"),
			      examine.c_str()));


    bool can_apply = (state.selected_solution < state.generated_solutions);
    bool can_next = (state.selected_solution < state.generated_solutions &&
		     !(state.selected_solution + 1 == state.generated_solutions && state.solutions_exhausted));

    key_hints.push_back(style_fragment(fragf(_("%s: Apply"),
					     apply.c_str()),
				       can_apply ? st_normal : st_disabled));
    key_hints.push_back(style_fragment(fragf(_("%s: Next"),
					     next.c_str()),
				       can_next ? st_normal : st_disabled));

    bool can_prev = (state.selected_solution > 0);
    key_hints.push_back(style_fragment(fragf(_("%s: Previous"),
					     prev.c_str()),
				       can_prev ? st_normal : st_disabled));

    return join_fragments(key_hints, L"  ");
  }

  void tick_timeout()
  {
    vs_widget_ref tmpref(this);

    if(resman != NULL && resman->background_thread_active())
      {
 	++spin_count;
	update();
	vscreen_update();
      }

    vscreen_addtimeout(new slot_event(sigc::mem_fun(this, &broken_indicator::tick_timeout)),
		       aptcfg->FindI(PACKAGE "::Spin-Interval", 500));
  }

  std::string spin_string(const resolver_manager::state &state) const
  {
    if(!state.background_thread_active)
      return " ";

    switch(spin_count % 4)
      {
      case 0:
	return ".";
      case 1:
	return "o";
      case 2:
	return "O";
      case 3:
	return "o";
      default:
	return "?";
      }
  }

  struct update_event : public vscreen_event
  {
    broken_indicator *b;
  public:
    update_event(broken_indicator *_b)
      : b(_b)
    {
    }

    void dispatch()
    {
      b->update();
    }
  };

  /** Post an update to run in the main thread; needed since the
   *  selected_signal_changed signal might theoretically run from a
   *  background thread.  (at the moment it shouldn't, but this will
   *  help avoid nasty surprises)
   */
  void post_update()
  {
    vscreen_post_event(new update_event(this));
  }
public:
  static ref_ptr<broken_indicator> create()
  {
    ref_ptr<broken_indicator> rval(new broken_indicator);
    rval->decref();
    return rval;
  }

  // TODO: split this monster up.
  void update()
  {
    vs_widget_ref tmpref(this);

    if(resman == NULL || !resman->resolver_exists())
      {
	set_fragment(fragf(""));
	last_sol.nullify();
	hide();
	return;
      }

    // Take a snapshot of the state.
    resolver_manager::state state = resman->state_snapshot();

    if(state.solutions_exhausted && state.generated_solutions == 0)
      {
	set_fragment(fragf(_("Unable to resolve dependencies.")));
	last_sol.nullify();
	show();
	return;
      }

    // Handle the case where the resolver is churning away.
    if(state.selected_solution >= state.generated_solutions)
      {
	if(state.background_thread_aborted)
	  {
	    set_fragment(fragf(_("Fatal error in resolver")));
	    last_sol.nullify();
	    show();
	    return;
	  }

	// TODO: add a column-generating fragment that can
	//       left/right justify stuff.

	vector<fragment_column_entry> columns;

	columns.push_back(fragment_column_entry(true, 1, fragment_column_entry::top, flowbox(text_fragment(ssprintf(_("[%d(%d)/...] Resolving dependencies"),
														    state.selected_solution + 1,
														    state.generated_solutions)))));

	columns.push_back(fragment_column_entry(false, 1, fragment_column_entry::top, NULL));

	columns.push_back(fragment_column_entry(false, 1, fragment_column_entry::top, text_fragment(spin_string(state))));

	set_fragment(sequence_fragment(fragment_columns(columns),
				       key_hint_fragment(state),
				       NULL));
	last_sol.nullify();
	show();
	return;
      }

    aptitude_solution sol = resman->get_solution(state.selected_solution, 0);

    // This test always fails the first time update() is called, since
    // sol is never NULL and last_sol is initialized to NULL.
    if(sol == last_sol && state.solutions_exhausted == last_complete &&
       // If there's an active thread we need to redraw the widget to
       // include the spinner.
       !last_background_active &&
       state.background_thread_active == last_background_active)
      return;

    last_sol = sol;
    last_complete = state.solutions_exhausted;
    last_background_active = state.background_thread_active;

    if(sol.get_actions().empty())
      {
	set_fragment(fragf("%s", _("Internal error: unexpected null solution.")));
	show();
	return;
      }

    int install_count=0, remove_count=0, keep_count=0, upgrade_count=0, downgrade_count=0;

    for(imm::map<aptitude_universe::package, aptitude_solution::action>::const_iterator
	  i = sol.get_actions().begin();
	i != sol.get_actions().end(); ++i)
      {
	pkgCache::PkgIterator pkg=i->first.get_pkg();
	pkgCache::VerIterator curver=pkg.CurrentVer();
	pkgCache::VerIterator instver=(*apt_cache_file)[pkg].InstVerIter(*apt_cache_file);
	pkgCache::VerIterator newver=i->second.ver.get_ver();

	// If not, we have a problem.
	eassert(instver!=newver);

	if(newver == curver)
	  ++keep_count;
	else if(curver.end())
	  ++install_count;
	else if(newver.end())
	  ++remove_count;
	else
	  {
	    int cmp=_system->VS->CmpVersion(curver.VerStr(),
					    newver.VerStr());

	    // The versions shouldn't be equal -- otherwise
	    // something is majorly wrong.
	    // eassert(cmp!=0);
	    //
	    // The above is not true: consider, eg, the case of a
	    // locally compiled package and a standard package.

	    /** \todo indicate "sidegrades" separately? */
	    if(cmp<=0)
	      ++upgrade_count;
	    else if(cmp>0)
	      ++downgrade_count;
	  }
      }

    vector<fragment *> fragments;

    string countstr
      = ssprintf(state.solutions_exhausted?"[%d/%d]":"[%d(%d)/...]",
		 state.selected_solution + 1,
		 state.generated_solutions);
    fragments.push_back(fragf("%s ", countstr.c_str()));


    vector<fragment *> suggestions;

    if(install_count>0)
      suggestions.push_back(text_fragment(ssprintf(ngettext("%d install",
							    "%d installs",
							    install_count),
						   install_count)));

    if(remove_count>0)
      suggestions.push_back(text_fragment(ssprintf(ngettext("%d removal",
							    "%d removals",
							    remove_count),
						   remove_count)));

    if(keep_count>0)
      suggestions.push_back(text_fragment(ssprintf(ngettext("%d keep",
							    "%d keeps",
							    keep_count),
						   keep_count)));

    if(upgrade_count>0)
      suggestions.push_back(text_fragment(ssprintf(ngettext("%d upgrade",
							    "%d upgrades",
							    upgrade_count),
						   upgrade_count)));

    if(downgrade_count>0)
      suggestions.push_back(text_fragment(ssprintf(ngettext("%d downgrade",
							    "%d downgrades",
							    downgrade_count),
						   downgrade_count)));

    /* ForTranslators: %F is replaced with a comma separated list such as
       "n1 installs, n2 removals", ...
     */
    fragments.push_back(fragf(_("Suggest %F"), join_fragments(suggestions, L", ")));

    if(state.background_thread_active)
      {
	vector<fragment_column_entry> columns;
	columns.push_back(fragment_column_entry(true, 1, fragment_column_entry::top, hardwrapbox(sequence_fragment(fragments))));

	columns.push_back(fragment_column_entry(false, 1, fragment_column_entry::top, NULL));

	columns.push_back(fragment_column_entry(false, 1, fragment_column_entry::top, text_fragment(spin_string(state))));


	fragments.clear();
	fragments.push_back(fragment_columns(columns));
      }
    else
      fragments.push_back(newline_fragment());
    fragments.push_back(hardwrapbox(key_hint_fragment(state)));

    fragment *f=sequence_fragment(fragments);
    set_fragment(f);

    show();
  }
};

ref_ptr<vscreen_widget> make_broken_indicator()
{
  return broken_indicator::create();
}
