/***************************************************************************
 *   Copyright (C) 2003 by Sylvain Joyeux                                  *
 *   sylvain.joyeux@m4x.org                                                *
 *   The forms are by Willy De la Court <wdl@linux-lovers.be>              *
 *                                                                         *
 *   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 "apt.h"
#include "regexps.h"

#include "dpkg.h"
#include "rpm.h"

#include "parsers/parsers.h"

#include <qcstring.h>

#include <kapplication.h>
#include <kinstance.h>
#include <kglobal.h>
#include <kconfig.h>
#include <kstandarddirs.h>
#include <klocale.h>
#include <kurl.h>

#include <kdebug.h>

#include <qregexp.h>

#include <stdlib.h>

#include <config.h>

using namespace KIO;

/*************************************************************************
* Common definitions of HTML fragments
*/

static const QString
  html_preamble("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Strict//EN\"\n"
                "\t\"http://www.w3.org/TR/html4/strict.dtd\">\n"
                "<html>\n");
static const QString
  html_redirect(html_preamble +
    QString("<head>\n"
            "\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n"
            "\t<meta http-equiv=\"Refresh\" content=\"0 ; URL=%1\">\n"
            "</head>\n"
            "\n<body></body>\n"
            "</html>"));

static const QString
  html_head(html_preamble +
    QString("<head>\n"
            "\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n"
            "\t<link rel=\"stylesheet\" href=\"file:%1\">\n"
            "\t<title>%2</title>\n"
            "</head>\n\n"
            "<body>\n"));

static QString close_html_head();
static QString open_html_head(const QString& title, bool links, AptProtocol const& config)
{

  static const QString
    html_head_table(
              "<table class=\"header\" style=\"background-image: url(file:%1);\"\n"
                    "\t\tcellspacing=\"0\" cellpadding=\"0\">\n"
              "<tr>\n"
              "\t<td class=\"logo\" %2><img src=\"file:%3\" alt=\"%4\" style=\"border: 0px\" /></td>\n"
              "\t<td class=\"header-title\">%4</td>\n");

  QString rowspan;
  if (links) rowspan = "rowspan=\"2\"";
  QString ret =
   html_head
    .arg(config.stylesheet())
    .arg(title)
   + html_head_table
    .arg(config.header_background())
    .arg(rowspan)
    .arg(config.logo())
    .arg(config.logo_alt())
    .arg(title);

  if (links)
  {
    return ret +
      "</tr>\n"
      "<tr>\n"
      "\t<td class=\"links\">\n"
      "\t<table class=\"links\" cellspacing=\"0\" cellpadding=\"0\">\n"
      "\t<tr>\n";
  }
  else
  {
    return ret + "</tr>\n</table>\n\n";
  }
}
static QString add_html_head_link(const QString& url, const QString& name, const QString& long_desc)
{
  static const QString format("\t\t<td><a href=\"%1\" title=\"%2\">%3</a></td>\n");
  return format.arg(url).arg(long_desc).arg(name);
}
static QString close_html_head()
{
  return "\t</tr>\n"
         "\t</table>\n"
         "\t</td>\n"
         "</tr>"
         "</table>";
}

static const QString
  html_tail("<div class=\"footer\">%1</div>\n"
            "</body>\n"
            "</html>");


QString AptProtocol::make_html_tail(const QString& note, bool with_form)
{
  with_form = m_search && with_form;

  QString ret;
  if (with_form)
    ret = "<hr>\n" + make_html_form();

  if (!note.isEmpty())
    ret += html_tail.arg(note + ". " + i18n("Page generated by kio_apt."));
  else ret += html_tail.arg(i18n("Page generated by kio_apt."));

  return ret;
}

/**********************************************************************************
 * Search form
 */

static const QString
  html_form_begin("\n<form action=\"apt:/\" method=\"GET\">\n"
                  "<table class=\"query\">\n");
static const QString
  html_form_end("<tr>\n"
                "\t<td class=\"button\" colspan=\"2\"><input type=\"submit\" value=\"%1\"></td>\n"
                "</tr>\n"
                "</table>\n"
                "</form>\n");

static const QString
  html_form_line("<tr>\n"
                 "\t<td><label for=\"%1\">%2</label></td>\n"
                 "\t<td><input type=\"text\" name=\"%3\" id=\"%4\"></td>\n"
                 "</tr>\n");

static QString make_html_form_line(const QString& type, const QString& label)
{ return html_form_line.arg(type).arg(label).arg(type).arg(type); }


static QString make_extform_cmd(bool ext_form, const KURL& query)
{
  QString cmd = ext_form ? "0" : "1";
  QString msg = ext_form ? i18n("Hide extended form") : i18n("Show extended form");

  KURL url(query);
  url.addQueryItem("extended_form", cmd);
  url.setRef("extformcmd");

  return 
    "<div class=\"command\" id=\"extformcmd\">\n"
    "\t<a href=\"" + url.htmlURL() + "\">[" + msg + "]</a>\n"
    "</div>\n";
}

/** Prints the HTML code for the query form */
QString AptProtocol::make_html_form() const
{
  bool can_fsearch = false;
  bool ext_form = KGlobal::config() -> readBoolEntry("extended_form", true);
  // Only in config-file. Needed for some dpkg-based distros that are not Debian
  can_fsearch = can_searchfile(true);

  bool online = false;
  bool online_form = KGlobal::config() -> readBoolEntry("online_form", true);
  if (m_pkgmanager)
    online = online_form && (!m_internal) && ext_form && m_pkgmanager -> capabilities(PackageManager::ONLINE);

  QString ret;
  QTextOStream stream(&ret);
  stream << make_extform_cmd(ext_form, m_query);

  if (online)
    stream << "<table class=\"queryform\"><tr><td>\n";

  stream << html_form_begin;
  stream << "<tr><td colspan=\"2\" class=\"title\">" + i18n("Offline search") + "</td></tr>" << endl;
  stream << make_html_form_line("search", i18n("Package search"));
  if (ext_form)
  {
    if (can_fsearch)
      stream << make_html_form_line("fsearch", i18n("File search"));
    stream << make_html_form_line("show", i18n("Package info"));
  }
  stream << html_form_end.arg( i18n("Search") );

  if (online)
  {
    stream << "\n</td><td>\n";
    stream << m_pkgmanager -> getOnlineForm();
    stream << "\n</td></tr>\n</table>";
  }

  return ret;
}

/****************************************************************************************/

AptProtocol::AptProtocol( const QCString &pool_socket, const QCString &app_socket )
    : SlaveBase( "kio_apt", pool_socket, app_socket ),
      m_pkgmanager(0), m_parser(0)

{
  KStandardDirs* dirs = KGlobal::dirs();
  m_stylesheet = dirs->findResource( "data", "kio_apt/kio_apt.css" );

  m_logo = dirs->findResource( "data", "kio_apt/" 
          + KGlobal::config() -> readEntryUntranslated("logo", "kdedeb_logo.png" ) );

  m_header_background = dirs->findResource( "data", "kio_apt/" 
          + KGlobal::config() -> readEntryUntranslated("background", "headerbg.png" ) );

  m_logo_alt = KGlobal::config() -> readEntryUntranslated("alt_tag", i18n("KDE on Debian") );

  connect(&m_process, SIGNAL(token(const QString&, const QString&)),
    this, SLOT(token_dispatch(const QString&, const QString&)));

#ifdef USE_RPM
  m_pkgmanager = new Rpm(this);
#else
  m_pkgmanager = new Dpkg(this);
#endif

  if (m_pkgmanager)
  {
    connect(m_pkgmanager, SIGNAL(token(const QString&, const QString&)),
      this, SLOT(token_dispatch(const QString&, const QString&)));
  }
}

AptProtocol::~AptProtocol() {}

QString AptProtocol::stylesheet() const { return m_stylesheet; }
QString AptProtocol::logo() const { return m_logo; }
QString AptProtocol::logo_alt() const { return m_logo_alt; }
QString AptProtocol::header_background() const { return m_header_background; }

void AptProtocol::token_dispatch(const QString& name, const QString& val)
{
  if (m_parser.get())
    (*m_parser)(this, name, val);
}

void AptProtocol::data(const QCString& string)
{
  using namespace Parsers;
  (*this) << string;
}

void AptProtocol::data(const QString& string)
{
  using namespace Parsers;
  (*this) << string;
}

void AptProtocol::data(const char* string)
{
  using namespace Parsers;
  (*this) << string;
}

void AptProtocol::data(const QByteArray& array)
{ SlaveBase::data(array); }

void AptProtocol::mimetype( const KURL & /*url*/ )
{
  mimeType( "text/html" );
  finished();
}

bool AptProtocol::check_validpackage(const QString& query)
{
  static QRegExp rx_pkgname(rxs_pkgname);
  if (!rx_pkgname.exactMatch(query))
  {
    error( ERR_SLAVE_DEFINED, i18n("\"%1\" is not a valid package name").arg(query) );
    return false;
  }
  return true;
}


/********************************************************************
 * Main entry point
 */

static QString read_option(QMap<QString, QString>& map, const QString& name, const QString& def)
{
  if (!map.contains(name)) return def;
  QString ret = map[name];
  map.remove(name);
  return ret;
}

void AptProtocol::get ( const KURL& url )
{
  /* The queries have two possible formats :

   - clean way to call a command
    apt:/command?query&option=value&option=value&...
   - needed to simplify forms
    apt:/?command=query&command2=&command3=&option=value&option=value&...
   - calls only the query form page
    apt:/
  */

  typedef void (AptProtocol::*Command)(const QString&, const QueryOptions&);
  static const QString commands[] =
  { "search", "show", "policy",
    "fsearch", "list", "online",
    "get", QString::null };
  static const Command methods[] =
  { &AptProtocol::search, &AptProtocol::show, &AptProtocol::policy,
    &AptProtocol::searchfile, &AptProtocol::listfiles, &AptProtocol::online,
    &AptProtocol::pkgmanager };

  QString command, query;
  Command method = 0;
  QueryOptions options = url.queryItems(KURL::CaseInsensitiveKeys);

  // canonize the part before ? : remove the first /
  QString path = url.path();
  if (path [0] == '/')
    path = path.right(path.length() - 1);

  for (int cmd_idx = 0; !commands[cmd_idx].isNull(); ++cmd_idx)
  {
    const QString cmd_it = commands[cmd_idx];

    // Look if the command is in the path part
    if (command.isEmpty() && cmd_it == path)
    {
      command = cmd_it;
      method = methods[cmd_idx];
    }
    if (options.contains(cmd_it))
    {

      if (options[cmd_it].isEmpty() && !options[cmd_it].isNull())
      { // we have a &command=& format, we remove it
        options.remove(cmd_it);
      }
      else if (command.isEmpty() && !options[cmd_it].isEmpty())
      { // the command is set in the options map
        command = cmd_it;
        method = methods[cmd_idx];
        query = options[cmd_it];
        options.remove(cmd_it);
      }
    }
  }

  // Well, we have no query for now, let's find it
  if (query.isEmpty())
  {
    for (QueryOptions::Iterator i = options.begin(); i != options.end(); ++i)
    {
      if ((*i).isNull())
      {
        query = KURL::decode_string(i.key());
        options.remove(i);
        break;
      }
    }
  }

  // Interpret the ioslave config options
  // and remove them from the options map
  QString opt = read_option(options, "extended_form", QString::null);
  if (!opt.isNull())
  {
    bool ext_form = (opt != "0");
    KGlobal::config() -> writeEntry("extended_form", ext_form);
  }

  // Enable install/remove/upgrade/...
  opt = read_option(options, "enable_actions", "1");
  m_act = (opt != "0");

  opt = read_option(options, "enable_search", "1");
  m_search = (opt != "0");

  // Allow links outside of apt:/ hierarchy
  opt = read_option(options, "stay_internal", "0");
  m_internal = (opt == "1");

  // Sync the config (must use kcfg sometime :p)
  KGlobal::config() -> sync();

  if (command.isEmpty() || query.isEmpty())
  {
    // No query or no command, we go in help mode
    m_query = buildURL(KURL("apt:/"));
    help();
  }
  else
  {
    m_query = buildURL(command, query);
    m_query.setHTMLRef(url.htmlRef());
    for (QueryOptions::ConstIterator i = options.begin(); i != options.end(); ++i)
      m_query.addQueryItem(i.key(), i.data());

    kdDebug() << "Old url " << url << ", new url " << m_query << endl;

    if (m_query != url)
    {
      redirection(m_query);
      data(QByteArray());
      finished();
      return;
    }

    (this->*method)(query, options);
  }
}


/***********************************************************************************
*
*  form
*
*/

void AptProtocol::help()
{
  mimeType("text/html");

  QString buffer;
  QTextOStream stream(&buffer);
  stream
        << open_html_head(i18n("Search Form"), false, *this)
        << make_html_form()
        << make_html_tail(QString::null, false);
  data(buffer);
  data(QByteArray());
  finished();
}







/***********************************************************************************
 *   apt-cache search
 */

void AptProtocol::search( const QString& query, const QueryOptions& /*options*/ )
{
  mimeType("text/html");

  data(open_html_head(i18n("Package search result for \"%1\"").arg(query), false, *this));

  m_parser.reset(new Parsers::Search);
  (*m_parser)(this, "begin", query);
  if (!m_process.search( query ))
  {
    error(ERR_SLAVE_DEFINED, i18n("Error launching the search").arg(query));
    return;
  }
  (*m_parser)(this, "end", QString::null);

  data(make_html_tail( i18n("%1 results").arg(m_parser -> result_count())) );
  data(QByteArray());
  finished();
}






/***********************************************************************************
 *   apt-cache show
 */

static QString filelist_cmd(bool show_filelist, const KURL& query)
{
  QString value = show_filelist ? "0" : "1";
  QString msg = show_filelist ? i18n("Hide file list") : i18n("Show file list");

  KURL url(query);
  url.addQueryItem("show_filelist", value);
  url.setRef("filelistcmd");

  return
    "<div class=\"command\" id=\"filelistcmd\">\n"
    "\t<a href=\"" + url.htmlURL() + "\">"
    "[" + msg + "]"
    "</a>\n"
    "</div>";
}

void AptProtocol::show(const QString& package, const QueryOptions& options)
{
  if (!check_validpackage(package)) return;

  if (options.contains("show_filelist"))
  {
    KGlobal::config() -> writeEntry("show_filelist", options["show_filelist"] != "0");
    KGlobal::config() -> sync();
  }

  mimeType("text/html");

  QString installed_version;

  /** First, we parse policy
   * We use here the fact that HTML is generated
   * during the call of (*policy)(...,"end",...),
   * since the header changes when the package
   * is installed or not */
  Parsers::Policy* policy = new Parsers::Policy(package, m_act);
  m_parser.reset(policy);
  (*m_parser)(this, "begin", QString::null);
  {
    if (!m_process.policy( package ))
    {
      error(ERR_SLAVE_DEFINED, i18n("Can't launch \"apt-cache policy %1\"").arg(package));
      return;
    }

    installed_version = policy->getInstalled();
    bool can_list = can_listfiles(!installed_version.isEmpty());
    QString buffer;
    QTextOStream s(&buffer);
    if (can_list)
    {
      KURL url = buildURL("list", package);
      s << open_html_head(i18n("Package description for \"%1\"").arg(package), true, *this)
        <<   add_html_head_link(url.htmlURL(), i18n("List package files"), "")
        << close_html_head();
    }
    else
    {
      s << open_html_head(i18n("Package description for \"%1\"").arg(package), false, *this);
    }
    data(buffer);
  }
  (*m_parser)(this, "end", QString::null);


  /** Add package description section */
  m_parser.reset(new Parsers::Show(package, installed_version, m_act));
  (*m_parser)(this, "begin", QString::null);
  {
    if (!m_process.show(package))
    {
      error(ERR_SLAVE_DEFINED, i18n("Can't launch \"apt-cache show %1\"").arg(package));
      return;
    }
    if (!m_parser -> result_count())
    {
      data("<div class=\"error\">" + i18n("No package found named \"%1\"").arg(package) + "</div>\n");
      data(make_html_tail());
      data(QByteArray());
      finished();
      return;
    }
  }
  (*m_parser)(this, "end", QString::null);



  /** Add file list (if enabled) */
  bool show_filelist = KGlobal::config() -> readBoolEntry("show_filelist", false);
  if ( show_filelist )
  {
    if (can_listfiles(!installed_version.isEmpty()))
    {
      data(
        "<hr>\n"
        + filelist_cmd(show_filelist, m_query)
        + "<div class=\"filelist\">\n");

      m_parser.reset(new Parsers::List(!m_internal));
      (*m_parser)(this, "begin", QString::null);
      if (!m_pkgmanager -> list(package))
      {
        error(ERR_SLAVE_DEFINED, i18n("Error listing files of %1").arg(package));
        return;
      }
      (*m_parser)(this, "end", QString::null);

      data("\n</div>\n");
    }
    else // cannot list files
    {
      data(
        "<hr>\n"
        + filelist_cmd(show_filelist, m_query)
        + "<div class=\"error\">" + i18n("Cannot list files for non-installed packages") + "</div>\n");
    }
  }
  else
  {
    data("<hr>\n" + filelist_cmd(show_filelist, m_query));
  }


  data(make_html_tail());
  data(QByteArray());
  finished();
}




/***********************************************************************************
 *   apt-cache policy
 */

void AptProtocol::policy( const QString& query, const QueryOptions& /*options*/ )
{
  if (!check_validpackage(query)) return;

  mimeType("text/html");

  data( open_html_head(i18n("Apt policy for \"%1\"").arg(query), false, *this) );

  m_parser.reset(new Parsers::Policy(query, m_act));
  (*m_parser)(this, "begin", QString::null);
  if (!m_process.policy( query ))
  {
    error(ERR_SLAVE_DEFINED, i18n("Can't launch the policy for %1").arg(query));
    return;
  }
  (*m_parser)(this, "end", QString::null);

  data(make_html_tail());
  data(QByteArray());
  finished();
}



/***********************************************************************************
*   Search the package which contains a specific file
*/

static const QString
  html_dpkgs_begin("\n\n<table>\n"),
  html_dpkgs_end("\n\n</table>\n");


bool AptProtocol::can_searchfile(bool is_installed) const
{
  if (!m_pkgmanager) return false;
  int caps = m_pkgmanager -> capabilities(PackageManager::SEARCH_FILE | PackageManager::OFFLINE);
  if (!caps) return false;
  return is_installed || !(caps & PackageManager::INSTALLED_ONLY);
}
void AptProtocol::searchfile(const QString& query, const QueryOptions& /*options*/)
{
  if (!can_searchfile(true)) return;

  mimeType("text/html");
  data( open_html_head(i18n("File search for \"%1\"").arg(query), false, *this) + html_dpkgs_begin );

  m_parser.reset(new Parsers::FileSearch);
  (*m_parser)(this, "begin", QString::null);
  if (!m_pkgmanager -> search( query ))
  {
    error(ERR_SLAVE_DEFINED, i18n("Can't launch the package manager").arg(query));
    return;
  }
  (*m_parser)(this, "end", QString::null);

  data( html_dpkgs_end + make_html_tail(i18n("%1 files found").arg(m_parser -> result_count())) );
  data(QByteArray());
  finished();
}




/***********************************************************************************
*   List the files of a package
*/

bool AptProtocol::can_listfiles(bool is_installed) const
{
  if (!m_pkgmanager) return false;
  int caps = m_pkgmanager -> capabilities(PackageManager::LIST_FILES | PackageManager::OFFLINE);
  if (!caps) return false;
  return is_installed || !(caps & PackageManager::INSTALLED_ONLY);
}

void AptProtocol::listfiles(const QString& query, const QueryOptions& /*options*/)
{
  if (!can_listfiles(true)) return;
  if (!check_validpackage(query)) return;

  mimeType("text/html");

  KURL ret_url = buildURL("show", query);

  QString buffer;
  QTextOStream stream(&buffer);
  stream
        << open_html_head(i18n("Files in \"%1\"").arg(query), true, *this)
        <<   add_html_head_link(ret_url.htmlURL(), i18n("Show package info"), "")
        << close_html_head()
        << endl;
  data(buffer);

  m_parser.reset(new Parsers::List(!m_internal));
  (*m_parser)(this, "begin", QString::null);
  if (!m_pkgmanager -> list( query ))
  {
    error(ERR_SLAVE_DEFINED, i18n("Can't launch the package manager").arg(query));
    return;
  }
  (*m_parser)(this, "end", QString::null);

  data(make_html_tail());
  data(QByteArray());
  finished();
}




/***********************************************************************************
 * Go use online search services (like packages.debian.org for instance)
 */

//bool AptProtocol::can_online(int mode) const
//{
////  if (!m_pkgmanager) return false;
////  return m_pkgmanager -> capabilities(PackageManager::ONLINE | mode);
//  return false;
//}

void AptProtocol::online(const QString& query, const QueryOptions& options)
{
  QString url = m_pkgmanager -> getOnlineURL(query, options);
  redirection(url);
  finished();
  return;
}

/***********************************************************************************
 * Send commands for kpkgmanager
 */
void AptProtocol::pkgmanager(const QString& query, const QueryOptions& options)
{
  QString command;
  if (query == "install") {
    command = "+";
  } else if (query == "remove") {
    command = "-";
  }

  if (command.isEmpty())
  {
    error(ERR_SLAVE_DEFINED, i18n("No package manager command specified"));
    return;
  }

  if (!options.contains("package"))
  {
    error(ERR_SLAVE_DEFINED, i18n("No package specified"));
    return;
  }
  command += options["package"];

  QString version = options["version"];
  QString dist = options["dist"];
  if (! (version.isEmpty() || dist.isEmpty()))
  {
    error(ERR_SLAVE_DEFINED, i18n("Both package version and distribution are specified"));
    return;
  }

  if (!version.isEmpty())
    command += "=" + version;
  else if (!dist.isEmpty())
    command += "/" + dist;

  kdDebug(DEBUG_ZONE) << command << endl;

  mimeType("application/x-pkgmanager");
  data(command + "\n");
  data(QByteArray());
  finished();
}

KURL AptProtocol::buildURL( const QString & command, const QString & query ) const
{
  KURL url;
  url.setProtocol("apt");
  if (!command.startsWith("/"))
    url.setPath("/" + command);
  else
    url.setPath(command);
  url.setQuery(query);
  return buildURL(url);
}

KURL AptProtocol::buildURL( const KURL& query ) const
{
  KURL url(query);

  if (!m_act)
    url.addQueryItem("enable_actions", "0");
  if (!m_search)
    url.addQueryItem("enable_search", "0");
  if (m_internal)
    url.addQueryItem("stay_internal", "1");

  return url;
}

/***********************************************************************************
*
*   kdemain
*
*/

extern "C" {
  int kdemain( int argc, char **argv ) {
    KInstance instance( "kio_apt" );

    if ( argc != 4 ) {
      kdDebug( DEBUG_ZONE ) << "Usage: kio_apt  protocol domain-socket1 domain-socket2" << endl;
      exit ( -1 );
    }

    AptProtocol slave( argv[ 2 ], argv[ 3 ] );
    slave.dispatchLoop();

   return 0;
  }
}

#include "apt.moc"
