
/*

  KLayout Layout Viewer
  Copyright (C) 2006-2017 Matthias Koefferlein

  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; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

*/


#include "layGSIHelpProvider.h"
#include "layHelpSource.h"
#include "tlClassRegistry.h"

#include "gsiDecl.h"
#include "gsiDeclBasic.h"
#include "pya.h"

#include <QUrl> 
#include <QColor>
#include <QPalette>
#include <QApplication>

#include <cstdio>
#include <cctype>
#include <algorithm>

namespace lay
{

// --------------------------------------------------------------------------------------
//  Some utilities

static std::string class_doc_path (const std::string &c)
{
  return "/code/class_" + c + ".xml";
}

static std::string class_doc_path (const std::string &c, const std::string &m)
{
  if (c.empty ()) {
    return "#m_" + m;
  } else {
    return "/code/class_" + c + ".xml#m_" + m;
  }
}

std::string escape_xml_with_formatting (const std::string &s, bool &in_code)
{
  std::string r;
  r.reserve (s.size ());
  for (tl::Extractor sc (s.c_str ()); *sc; ) {
    if (*sc == '<') {
      r += "&lt;";
      ++sc;
    } else if (*sc == '>') {
      r += "&gt;";
      ++sc;
    } else if (*sc == '&') {
      r += "&amp;";
      ++sc;
    } else if (*sc == '@') {
      if (sc.test ("@<")) {

        //  HTML tag
        r += "<";
        while (*sc && *sc != '>') {
          r += *sc;
          ++sc;
        }
        if (*sc == '>') {
          r += ">";
          ++sc;
        }

      } else if (sc.test ("@li")) {
        r += "<li>";
      } else if (sc.test ("@/li")) {
        r += "</li>";
      } else if (sc.test ("@ul")) {
        r += "<ul>";
      } else if (sc.test ("@/ul")) {
        r += "</ul>";
      } else if (sc.test ("@b")) {
        r += "<b>";
      } else if (sc.test ("@/b")) {
        r += "</b>";
      } else if (sc.test ("@u")) {
        r += "<u>";
      } else if (sc.test ("@/u")) {
        r += "</u>";
      } else if (sc.test ("@i")) {
        r += "<i>";
      } else if (sc.test ("@/i")) {
        r += "</i>";
      } else if (sc.test ("@pre") || sc.test ("@code")) {
        in_code = true;
        r += "<pre>";
      } else if (sc.test ("@/pre") || sc.test ("@/code")) {
        in_code = false;
        r += "</pre>";
      } else if (sc.test ("@@")) {
        r += "@";
      } else {
        r += "@";
        ++sc;
      }
    } else {
      r += *sc;
      ++sc;
    }
  }
  return r;
}

static std::string 
full_name (const gsi::MethodBase::MethodSynonym &syn)
{
  if (syn.is_predicate) {
    return syn.name + "?";
  } else if (syn.is_setter) {
    return syn.name + "=";
  } else {
    return syn.name;
  }
}

struct DocumentationParser 
{
  DocumentationParser (const gsi::MethodBase *method)
  {
    std::string doc = method->doc ();
    if (pya::PythonInterpreter::instance ()) {
      std::string pydoc = pya::PythonInterpreter::instance ()->python_doc (method);
      if (! pydoc.empty ()) {
        doc += "\n@<h4>\n";
        doc += tl::to_string (QObject::tr ("Python specific notes:\n"));
        doc += "@</h4>\n\n";
        doc += pydoc;
      }
    }
    parse_doc (doc);
  }

  DocumentationParser (const gsi::ClassBase *cls)
  {
    parse_doc (cls->doc ());
  }

  void parse_doc (const std::string &formatted_doc) 
  {
    hidden = false;
    qt_class = false;

    tl::Extractor ex (formatted_doc.c_str ());
    while (*ex) {

      if (*ex == '@') {

        if (ex.test ("@hide")) {
          hidden = true;
        } else if (ex.test ("@qt")) {
          qt_class = true;
        } else if (ex.test ("@brief")) {

          ex.read (brief_doc, "\n");

        } else if (ex.test ("@alias")) {

          ex.read (alias, "\n");

        } else if (ex.test ("@return") || ex.test ("@returns")) {

          ex.read  (ret_val, "\n");

        } else if (ex.test ("@args")) {

          std::string a;
          ex.try_read (a, "\n");
          a = tl::trim (a);
          if (! a.empty ()) {
            args = tl::split (a, ",");
            for (std::vector<std::string>::iterator i = args.begin (); i != args.end (); ++i) {
              *i = tl::trim (*i);
            }
          }

        } else if (ex.test ("@param")) {

          std::string n;
          ex.try_read (n);
          params.push_back (std::make_pair (n, std::string ()));
          ex.read (params.back ().second, "\n");

        } else {
          doc += "@";
          ++ex;
        }

      } else {
        doc += *ex;
        ++ex;
      }

    }
  }

  std::string doc_html ()
  {
    std::string r;
    r = "<p>";

    size_t p = 0;
    size_t pe = std::string::npos;
    bool in_code = false;
    while ((pe = doc.find ("\n\n", p)) != std::string::npos) {
      r += escape_xml_with_formatting (std::string (doc, p, pe - p), in_code);
      p = pe;
      if (in_code) {
        while (p < doc.size () && doc [p] == '\n') {
          r += doc [p];
          ++p;
        }
      } else {
        while (p < doc.size () && doc [p] == '\n') {
          ++p;
        }
        if (p < doc.size ()) {
          r += "</p><p>";
        }
      }
    }

    if (p < doc.size ()) {
      r += escape_xml_with_formatting (std::string (doc, p), in_code);
    }

    r += "</p>";
    return r;
  }

  bool hidden, qt_class;
  std::string doc;
  std::string brief_doc;
  std::string alias;
  std::vector<std::string> args;
  std::string ret_val;
  std::vector<std::pair <std::string, std::string> > params;
};

//  A cache for the parsed class documentation

static std::map <const gsi::ClassBase *, DocumentationParser> s_cls_doc;

static DocumentationParser &cls_documentation (const gsi::ClassBase *cls)
{
  if (cls->doc ().empty () && cls->declaration ()) {
    cls = cls->declaration ();
  }

  std::map <const gsi::ClassBase *, DocumentationParser>::iterator cd = s_cls_doc.find (cls);
  if (cd != s_cls_doc.end ()) {
    return cd->second;
  } else {
    return s_cls_doc.insert (std::make_pair (cls, DocumentationParser (cls))).first->second;
  }
}

static const std::string &aliased_name (const gsi::ClassBase *cls)
{
  const std::string &alias = cls_documentation (cls).alias;
  if (alias.empty ()) {
    return cls->name ();
  } else {
    return alias;
  }
}

static std::string make_qualified_name (const gsi::ClassBase *cls)
{
  std::string qname;

  const gsi::ClassBase *p = cls;
  while (p) {

    DocumentationParser &doc = cls_documentation (p);

    std::string n = p->name ();
    if (p->declaration () == p && ! doc.alias.empty ()) {
      n = doc.alias;
    }

    if (qname.empty ()) {
      qname = n;
    } else {
      qname = n + "::" + qname;
    }

    p = p->parent ();

  }

  return qname;
}

static const gsi::ClassBase *
real_class (const gsi::ClassBase *cls)
{
  return cls->declaration () ? cls->declaration () : cls;
}

static std::string
replace_references (const std::string &t, const gsi::ClassBase *cls_base)
{
  cls_base = real_class (cls_base);
  tl_assert (cls_base);

  //  build a name map for simple name replacement
  std::set<std::string> name_map;
  for (gsi::ClassBase::method_iterator m = cls_base->begin_methods (); m != cls_base->end_methods (); ++m) {
    for (gsi::MethodBase::synonym_iterator syn = (*m)->begin_synonyms (); syn != (*m)->end_synonyms (); ++syn) {
      name_map.insert (full_name (*syn));
    }
  }

  std::string r;

  size_t q = 0;
  size_t p;
  while ((p = t.find ("\\", q)) != std::string::npos) {

    r += std::string (t, q, p - q);

    size_t pp = ++p;
    while (p < t.size () && (t[p] == '_' || isalnum (t [p]))) {
      ++p;
    }
    if (p < t.size () && (t[p] == '?' || t [p] == '=')) {
      ++p;
    }

    std::string id (t, pp, p - pp);

    std::string mid;

    if (p < t.size () && t[p] == '#') {
      pp = ++p;
      while (p < t.size () && (t[p] == '_' || isalnum (t [p]))) {
        ++p;
      }
      if (p < t.size () && (t[p] == '?' || t [p] == '=')) {
        ++p;
      }
      mid = std::string (t, pp, p - pp);
    }

    bool found = false;
    if (name_map.find (id) != name_map.end ()) {
      r += "<a href=\"" + escape_xml (class_doc_path (std::string (), id)) + "\">" + escape_xml (id) + "</a>";
      found = true;
    }

    for (gsi::ClassBase::class_iterator c = gsi::ClassBase::begin_classes (); c != gsi::ClassBase::end_classes () && !found; ++c) {
      if (c->name () == id) {
        r += "<a href=\"";
        if (mid.empty ()) {
          r += escape_xml (class_doc_path (id));
        } else {
          r += escape_xml (class_doc_path (id, mid));
        }
        r += "\">";
        if (mid.empty ()) {
          r += escape_xml (id);
        } else if (id.empty ()) {
          r += escape_xml (mid);
        } else {
          r += escape_xml (id) + "#" + escape_xml (mid);
        }
        r += "</a>";
        found = true;
      }
    }

    if (! found) {
      r += escape_xml (id);
      if (! mid.empty ()) {
        r += "#";
        r += mid;
      }
    }

    q = p;

  }

  r += std::string (t, q);

  return r;
}

// --------------------------------------------------------------------------------------
//  Implementation

std::string 
GSIHelpProvider::folder () const
{
  return "code";
}

std::string 
GSIHelpProvider::title () const
{
  return tl::to_string (QObject::tr ("API Reference"));
}

static 
void produce_toc (const gsi::ClassBase *cls, std::vector <std::string> &toc)
{
  DocumentationParser &doc = cls_documentation (cls);
  if (! doc.hidden) {
    toc.push_back (class_doc_path (make_qualified_name (cls)));
  }
  for (tl::weak_collection<gsi::ClassBase>::const_iterator cc = cls->begin_child_classes (); cc != cls->end_child_classes (); ++cc) {
    produce_toc (cc.operator-> (), toc);
  }
}

void 
GSIHelpProvider::toc (std::vector<std::string> &t)
{
  for (gsi::ClassBase::class_iterator c = gsi::ClassBase::begin_classes (); c != gsi::ClassBase::end_classes (); ++c) {
    produce_toc (c.operator-> (), t);
  }
}

QDomDocument
GSIHelpProvider::get (const std::string &path) const
{
  QUrl url = QUrl::fromEncoded (path.c_str ());
  QString url_path = url.path ();
  QRegExp class_doc_url (QString::fromUtf8 ("^/code/class_(.*)\\.xml$"));

  std::string text;
  if (url_path == QString::fromUtf8 ("/code/index.xml")) {
    text = produce_class_index ();
  } else if (class_doc_url.indexIn (url_path) == 0) {
    text = produce_class_doc (tl::to_string (class_doc_url.cap (1)));
  } else {
    throw tl::Exception (tl::to_string (QObject::tr ("Page not found: ")) + path);
  }

  QDomDocument doc;
  QString errorMsg;
  int errorLine = 0;

  if (! doc.setContent (QByteArray (text.c_str (), text.size ()), true, &errorMsg, &errorLine)) {

    //  fallback: provide the original text plus the error message
    std::string fallback_text = std::string ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") +
                                "<!DOCTYPE language SYSTEM \"klayout_doc.dtd\">\n" +
                                "<doc><p>\n" + 
                                "<b>XML Parser Error: </b>" + escape_xml (tl::to_string (errorMsg)) + ", in line " + tl::to_string (errorLine) + " of " + path + "\n" +
                                "</p><pre>\n" + 
                                escape_xml (text) + "\n" +
                                "</pre></doc>";

    doc.setContent (QByteArray (fallback_text.c_str (), fallback_text.size ()), true, &errorMsg, &errorLine);

  }
  
  return doc;
}

static 
void produce_doc_index (const gsi::ClassBase *cls, std::ostringstream &os, std::vector <std::pair <std::string, std::string> > &class_names, std::vector <std::pair <std::string, std::string> > &qt_class_names)
{
  DocumentationParser &doc = cls_documentation (cls);
  std::string qname = make_qualified_name (cls);

  //  Only list the name if the class is not hidden, it's a top-level class or the path is an expanded one
  //  (the last criterion avoids generating classes such as A::B_C)
  if (! doc.hidden) {
    if (! doc.qt_class) {
      class_names.push_back (std::make_pair (qname, doc.brief_doc));
    } else {
      qt_class_names.push_back (std::make_pair (qname, doc.brief_doc));
    }
    os << "<topic-ref href=\"" << escape_xml (class_doc_path (qname)) << "\"/>" << std::endl;
  }

  for (tl::weak_collection<gsi::ClassBase>::const_iterator cc = cls->begin_child_classes (); cc != cls->end_child_classes (); ++cc) {
    produce_doc_index (cc.operator-> (), os, class_names, qt_class_names);
  }
}

std::string  
GSIHelpProvider::produce_class_index () const
{
  std::ostringstream os;

  os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl
     << "<!DOCTYPE language SYSTEM \"klayout_doc.dtd\">" << std::endl
     << std::endl;

  os << "<doc><title>" << tl::to_string (QObject::tr ("Class Index")) << "</title>" << std::endl;

  std::vector <std::pair <std::string, std::string> > class_names;
  std::vector <std::pair <std::string, std::string> > qt_class_names;
  for (gsi::ClassBase::class_iterator c = gsi::ClassBase::begin_classes (); c != gsi::ClassBase::end_classes (); ++c) {
    produce_doc_index (&*c, os, class_names, qt_class_names);
  }

  if (! qt_class_names.empty ()) {
    os << tl::to_string (QObject::tr ("Find Qt class documentation")) << " <a href=\"#qtclasses\">" << tl::to_string (QObject::tr ("here")) << "</a>" << std::endl;
  }

  if (! class_names.empty ()) {
    
    std::sort (class_names.begin (), class_names.end ());

    os << "<h2>KLayout classes</h2>" << std::endl;
    os << "<table>" << std::endl;
    int n = 0;
    for (std::vector <std::pair <std::string, std::string> >::const_iterator cc = class_names.begin (); cc != class_names.end (); ++cc, ++n) {
      os << "<tr class=\"row" << (n % 2)  << "\">" << std::endl;
      os << "<td><a href=\"" << escape_xml (class_doc_path (cc->first)) << "\">" << escape_xml (cc->first) << "</a></td><td>&nbsp;&nbsp;</td><td><nobr>" << escape_xml (cc->second) << "</nobr></td></tr>" << std::endl;
    }
    os << "</table>" << std::endl;

  }

  if (! qt_class_names.empty ()) {
    
    std::sort (qt_class_names.begin (), qt_class_names.end ());

    os << "<a name=\"qtclasses\"/><h2>Qt classes</h2>" << std::endl;
    os << "<table>" << std::endl;
    int n = 0;
    for (std::vector <std::pair <std::string, std::string> >::const_iterator cc = qt_class_names.begin (); cc != qt_class_names.end (); ++cc, ++n) {
      os << "<tr class=\"row" << (n % 2)  << "\">" << std::endl;
      os << "<td><a href=\"" << escape_xml (class_doc_path (cc->first)) << "\">" << escape_xml (cc->first) << "</a></td><td>&nbsp;&nbsp;</td><td><nobr>" << escape_xml (cc->second) << "</nobr></td></tr>" << std::endl;
    }
    os << "</table>" << std::endl;

  }

  os << "</doc>" << std::endl;

  return os.str ();

}

static std::string
type_to_s (const gsi::ArgType &a, bool linked, bool for_return)
{
  std::string s;
  switch (a.type ()) {
  case gsi::T_void_ptr:
    s += "void *"; break;
  case gsi::T_void:
    s += "void"; break;
  case gsi::T_bool:
    s += "bool"; break;
  case gsi::T_char:
    s += "char"; break;
  case gsi::T_schar:
    s += "signed char"; break;
  case gsi::T_uchar:
    s += "unsigned char"; break;
  case gsi::T_short:
    s += "short"; break;
  case gsi::T_ushort:
    s += "unsigned short"; break;
  case gsi::T_int:
    s += "int"; break;
  case gsi::T_uint:
    s += "unsigned int"; break;
  case gsi::T_long:
    s += "long"; break;
  case gsi::T_ulong:
    s += "unsigned long"; break;
  case gsi::T_longlong:
    s += "long long"; break;
  case gsi::T_ulonglong:
    s += "unsigned long long"; break;
  case gsi::T_double:
    s += "double"; break;
  case gsi::T_float:
    s += "float"; break;
  case gsi::T_string:
    s += "string"; break;
  case gsi::T_var:
    s += "variant"; break;
  case gsi::T_object:
    if (a.is_cptr () || (! for_return && a.is_cref ())) {
      s = "const ";
    }
    if (a.pass_obj ()) {
      s += "new ";
    }
    if (linked) {
      s += "<a href=\"" + escape_xml (class_doc_path (aliased_name (a.cls ()))) + "\">" + escape_xml (aliased_name (a.cls ())) + "</a>";
    } else {
      s += aliased_name (a.cls ());
    }
    break;
  case gsi::T_vector:
    if (a.inner ()) {
      s += type_to_s (*a.inner (), linked, false);
    } 
    s += "[]";
    break;
  case gsi::T_map:
    s += "map&lt;";
    if (a.inner_k ()) {
      s += type_to_s (*a.inner_k (), linked, false);
    } 
    s += ",";
    if (a.inner ()) {
      s += type_to_s (*a.inner (), linked, false);
    } 
    s += "&gt;";
    break;
  }
  if (a.is_cptr () || a.is_ptr ()) {
    s += " ptr";
  }
  return s;
}

static std::string 
method_attributes (const gsi::MethodBase *method, DocumentationParser & /*doc*/, bool without_static = false, bool without_prot = true)
{
  std::string r;
  if (method->is_signal ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "signal";
  }
  if (method->is_callback ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "virtual";
  }
  if (! without_static && method->is_static ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "static";
  }
  if (method->is_const ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "const";
  }
  if (method->ret_type ().is_iter ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "iter";
  }
  if (! without_prot && method->is_protected ()) {
    if (! r.empty ()) {
      r += ",";
    }
    r += "protected";
  }
  return r;
}

static std::string 
method_return (const gsi::MethodBase *method, DocumentationParser & /*doc*/, bool linked = false)
{
  return type_to_s (method->ret_type (), linked, true);
}

static std::string
method_arguments (const gsi::MethodBase *method, const gsi::ClassBase *cls_obj, DocumentationParser &doc, bool linked = false, const char *sep = "<br/>")
{
  std::string r;
  if (method->begin_arguments () == method->end_arguments ()) {

    if (! doc.args.empty ()) {

      r += "(";

      //  special case of external method - just dump the argument names
      for (size_t i = 0; i < doc.args.size (); ++i) {
        if (i > 0) {
          r += ",";
          r += sep;
          r += "&nbsp;";
        }
        r += escape_xml (doc.args [i]);
      }

      r += ")";

    }

    return r;

  } else {

    r += "(";

    int n = 0;
    for (gsi::MethodBase::argument_iterator a = method->begin_arguments (); a != method->end_arguments (); ++a, ++n) {
      if (n > 0) {
        r += ",";
        r += sep;
        r += "&nbsp;";
      }
      r += type_to_s (*a, linked, false);
      r += " ";
      if (a->spec () && !a->spec ()->name ().empty()) {
        r += escape_xml (a->spec ()->name ());
        if (a->spec ()->has_default ()) {
          r += " = ";
          if (! a->spec ()->init_doc ().empty ()) {
            r += replace_references (a->spec ()->init_doc (), cls_obj);
          } else {
            r += escape_xml (a->spec ()->default_value ().to_string ());
          }
        }
      } else if (n < int (doc.args.size ())) {
        r += escape_xml (doc.args [n]);
      } else {
        r += "arg" + tl::to_string (n + 1);
      }
    }

    r += ")";

  }

  return r;
}

const gsi::ClassBase *
find_child_with_declaration (const gsi::ClassBase *pc, const gsi::ClassBase *decl)
{
  while (pc) {

    for (tl::weak_collection<gsi::ClassBase>::const_iterator sc = pc->begin_child_classes (); sc != pc->end_child_classes (); ++sc) {

      if (sc->declaration () == decl) {
        return sc.operator-> ();
      }

      const gsi::ClassBase *cc = find_child_with_declaration (sc.operator-> (), decl);
      if (cc) {
        return cc;
      }

    }

    //  Try the base classes - due to skipping of the hidden classes we might pick a parent 
    //  class initially.
    pc = pc->base ();

  }

  return 0;
}

std::string
GSIHelpProvider::produce_class_doc (const std::string &cls) const
{
  std::ostringstream os;

  const gsi::ClassBase *cls_obj = 0;

  std::vector<std::string> comp = tl::split (cls, "::");
  if (comp.empty ()) {
    return "Invalid class name: " + cls;
  }

  std::vector<std::string> cp = comp;

  for (gsi::ClassBase::class_iterator c = gsi::ClassBase::begin_classes (); c != gsi::ClassBase::end_classes (); ++c) {
    if (c->name () == cp.front ()) {
      cls_obj = &*c;
      cp.erase (cp.begin ());
      break;
    }
  }

  if (! cls_obj) {
    return "Unknown class: " + cls;
  }

  while (! cp.empty ()) {

    const gsi::ClassBase *pc = cls_obj;

    cls_obj = 0;
    while (pc && !cls_obj) {

      for (tl::weak_collection<gsi::ClassBase>::const_iterator sc = pc->begin_child_classes (); sc != pc->end_child_classes (); ++sc) {
        if (sc->name () == cp.front ()) {
          cls_obj = sc.operator-> ();
          cp.erase (cp.begin ());
          break;
        }
      }

      //  Try the base classes too - since we might have skipped some of the classes in the 
      //  inheritance hierarchy, the child may be in a base class.
      pc = pc->base ();

    }

    if (! cls_obj) {
      return "Unknown class: " + cls;
    }

  }

  const gsi::ClassBase *tl_alias = 0;

  if (cls_obj != cls_obj->declaration ()) {

    //  check if there is an alias for this class (the declaration)
    tl_alias = cls_obj->declaration ();

  } else {

    //  check if there is a top-level alias class for this one the other way around (A::B from A_B)
    for (gsi::ClassBase::class_iterator c = gsi::ClassBase::begin_classes (); c != gsi::ClassBase::end_classes () && !tl_alias; ++c) {
      tl_alias = find_child_with_declaration (&*c, cls_obj);
    }

  }

  DocumentationParser &class_doc = cls_documentation (cls_obj);

  os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl
     << "<!DOCTYPE language SYSTEM \"klayout_doc.dtd\">" << std::endl
     << std::endl;

  os << "<doc><title>" << tl::to_string (QObject::tr ("API reference - Class")) << " " << escape_xml (cls) << "</title>" << std::endl;

  os << "<keyword name=\"" << escape_xml (cls) << "\"/>" << std::endl;

  os << "<p>"
     << "<link href=\"/about/rba_notation.xml\"/>"
     << "</p>" << std::endl;

  os << "<p><b>" << tl::to_string (QObject::tr ("Description")) << "</b>: " << escape_xml (class_doc.brief_doc) << "</p>" << std::endl;

  std::vector<const gsi::ClassBase *> classes;
  classes.push_back (real_class (cls_obj));

  const gsi::ClassBase *base = real_class (cls_obj)->base ();
  if (base) {

    const gsi::ClassBase *last_cls = real_class (cls_obj);
    bool all_collected = false;

    os << "<p><b>" << tl::to_string (QObject::tr ("Class hierarchy")) << "</b>: " << make_qualified_name (cls_obj);
    while (base) {

      DocumentationParser &bdoc = cls_documentation (base);
      if (! bdoc.alias.empty ()) {
        //  suppress direct base class alias to our class (x_Native for x)
        if (bdoc.alias != last_cls->name ()) {
          os << " &#187; <a href=\"" << escape_xml (class_doc_path (bdoc.alias)) << "\">" << escape_xml (bdoc.alias) << "</a>";
          all_collected = true;
        } else if (! all_collected) {
          classes.push_back (base);
        }
      } else if (! bdoc.hidden) {
        os << " &#187; <a href=\"" << escape_xml (class_doc_path (base->name ())) << "\">" << escape_xml (base->name ()) << "</a>";
        all_collected = true;
      } else if (! all_collected) {
        //  class needs to be mixed into the parent
        classes.push_back (base);
      }

      last_cls = base;
      base = base->base ();

    }

    os << "</p>" << std::endl;

  }

  if (tl_alias) {
    os << "<p>" << tl::to_string (QObject::tr ("This class is equivalent to the class "));
    std::string n = make_qualified_name (tl_alias);
    os << "<a href=\"" << escape_xml (class_doc_path (n)) << "\">" << escape_xml (n) << "</a>";
    os << "</p>" << std::endl;
  }

  bool any = false;

  for (std::vector<const gsi::ClassBase *>::const_iterator c = classes.begin (); c != classes.end (); ++c) {
    for (tl::weak_collection<gsi::ClassBase>::const_iterator cc = (*c)->begin_child_classes (); cc != (*c)->end_child_classes (); ++cc) {

      DocumentationParser &cdoc = cls_documentation (cc.operator-> ());
      if (! cdoc.hidden || ! cdoc.alias.empty ()) {

        if (any) {
          os << ", ";
        } else {
          os << "<p><b>" << tl::to_string (QObject::tr ("Sub-classes")) << "</b>: ";
          any = true;
        }

        os << "<a href=\"" << escape_xml (class_doc_path (make_qualified_name (cc.operator-> ()))) << "\">" << escape_xml (cc->name ()) << "</a>";

      }

    }
  }

  if (any) {
    os << "</p>" << std::endl;
  }

  os << "<h2-index/>" << std::endl;

  os << replace_references (class_doc.doc_html (), cls_obj) << std::endl;

  //  collect the methods of the class and their hidden base classes
  //  (in the reverse order so that derived classes override their super classes methods)

  std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> > mm;

  for (size_t n = classes.size (); n > 0; ) {

    --n;

    //  remove the base classes' definitions if the name matches
    for (gsi::ClassBase::method_iterator m = classes [n]->begin_methods (); m != classes [n]->end_methods (); ++m) {
      for (gsi::MethodBase::synonym_iterator syn = (*m)->begin_synonyms (); syn != (*m)->end_synonyms (); ++syn) {
        std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::iterator im;
        while ((im = mm.find (full_name (*syn))) != mm.end ()) {
          mm.erase (im);
        }
      }
    }

    //  add the current classes' methods
    for (gsi::ClassBase::method_iterator m = classes [n]->begin_methods (); m != classes [n]->end_methods (); ++m) {
      for (gsi::MethodBase::synonym_iterator syn = (*m)->begin_synonyms (); syn != (*m)->end_synonyms (); ++syn) {
        DocumentationParser mdoc (*m);
        if (! mdoc.hidden) {
          mm.insert (std::make_pair (full_name (*syn), std::make_pair (*m, syn - (*m)->begin_synonyms ())));
        }
      }
    }

  }

  if (mm.empty ()) {
    os << "</doc>" << std::endl;
    return os.str ();
  }
  
  int n = 0;
  int row = 0;

  any = false;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    if (i->second.first->is_static () && syn.name == "new" && ! syn.deprecated && ! i->second.first->is_protected ()) {

      if (! any) {
        os << "<h2>" << tl::to_string (QObject::tr ("Public constructors")) << "</h2>" << std::endl;
        os << "<table>" << std::endl;
        any = true;
      }

      DocumentationParser method_doc (i->second.first);
      os << "<tr class=\"row" << (row % 2)  << "\">" << std::endl;
      ++row;
      os << "<td>" << method_return (i->second.first, method_doc) << "</td>";
      os << "<td><b><a href=\"#method" << n << "\">" << escape_xml (i->first) << "</a></b></td>";
      os << "<td>" << method_arguments (i->second.first, cls_obj, method_doc) << "</td>";
      os << "<td>" << replace_references (escape_xml (method_doc.brief_doc), cls_obj) << "</td>";
      os << "</tr>" << std::endl;

    }

  }

  if (any) {
    os << "</table>" << std::endl;
  }

  n = 0;
  row = 0;
  any = false;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    if (! i->second.first->is_static () && ! syn.deprecated && ! i->second.first->is_protected ()) {

      if (! any) {
        os << "<h2>" << tl::to_string (QObject::tr ("Public methods")) << "</h2>" << std::endl;
        os << "<table>" << std::endl;
        any = true;
      }

      DocumentationParser method_doc (i->second.first);
      os << "<tr class=\"row" << (row % 2)  << "\">" << std::endl;
      ++row;
      std::string attr = method_attributes (i->second.first, method_doc, true /*without static*/);
      if (! attr.empty ()) {
        os << "<td><i>[" << escape_xml (method_attributes (i->second.first, method_doc)) << "]</i></td>";
      } else {
        os << "<td></td>";
      }
      os << "<td>" << method_return (i->second.first, method_doc) << "</td>";
      os << "<td><b><a href=\"#method" << n << "\">" << escape_xml (i->first) << "</a></b></td>";
      os << "<td>" << method_arguments (i->second.first, cls_obj, method_doc) << "</td>";
      os << "<td>" << replace_references (escape_xml (method_doc.brief_doc), cls_obj) << "</td>";
      os << "</tr>" << std::endl;

    }

  }

  if (any) {
    os << "</table>" << std::endl;
  }

  any = false;

  n = 0;
  row = 0;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    if (i->second.first->is_static () && syn.name != "new" && ! syn.deprecated && ! i->second.first->is_protected ()) {

      if (! any) {
        any = true;
        os << "<h2>" << tl::to_string (QObject::tr ("Public static methods and constants")) << "</h2>" << std::endl;
        os << "<table>" << std::endl;
      }

      DocumentationParser method_doc (i->second.first);
      os << "<tr class=\"row" << (row % 2)  << "\">" << std::endl;
      ++row;
      std::string attr = method_attributes (i->second.first, method_doc, true /*without static*/);
      if (! attr.empty ()) {
        os << "<td><i>[" << escape_xml (method_attributes (i->second.first, method_doc)) << "]</i></td>";
      } else {
        os << "<td></td>";
      }
      os << "<td>" << method_return (i->second.first, method_doc) << "</td>";
      os << "<td><b><a href=\"#method" << n << "\">" << escape_xml (i->first) << "</a></b></td>";
      os << "<td>" << method_arguments (i->second.first, cls_obj, method_doc) << "</td>";
      os << "<td>" << replace_references (escape_xml (method_doc.brief_doc), cls_obj) << "</td>";
      os << "</tr>" << std::endl;

    }

  }

  if (any) {
    os << "</table>" << std::endl;
  }

  any = false;

  n = 0;
  row = 0;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    if (! syn.deprecated && i->second.first->is_protected ()) {

      if (! any) {
        any = true;
        os << "<h2>" << tl::to_string (QObject::tr ("Protected methods (static, non-static and constructors)")) << "</h2>" << std::endl;
        os << "<table>" << std::endl;
      }

      DocumentationParser method_doc (i->second.first);
      os << "<tr class=\"row" << (row % 2)  << "\">" << std::endl;
      ++row;
      std::string attr = method_attributes (i->second.first, method_doc);
      if (! attr.empty ()) {
        os << "<td><i>[" << escape_xml (method_attributes (i->second.first, method_doc)) << "]</i></td>";
      } else {
        os << "<td></td>";
      }
      os << "<td>" << method_return (i->second.first, method_doc) << "</td>";
      os << "<td><b><a href=\"#method" << n << "\">" << escape_xml (i->first) << "</a></b></td>";
      os << "<td>" << method_arguments (i->second.first, cls_obj, method_doc) << "</td>";
      os << "<td>";
      os << "<td>" << replace_references (escape_xml (method_doc.brief_doc), cls_obj) << "</td>";
      os << "</td>";
      os << "</tr>" << std::endl;

    }

  }

  if (any) {
    os << "</table>" << std::endl;
  }

  any = false;

  n = 0;
  row = 0;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    if (syn.deprecated) {

      gsi::MethodBase::synonym_iterator nd_syn = i->second.first->begin_synonyms ();
      while (nd_syn != i->second.first->end_synonyms () && nd_syn->deprecated) {
        ++nd_syn;
      }

      if (! any) {
        any = true;
        os << "<h2>" << tl::to_string (QObject::tr ("Deprecated methods (protected, public, static, non-static and constructors)")) << "</h2>" << std::endl;
        os << "<table>" << std::endl;
      }

      DocumentationParser method_doc (i->second.first);
      os << "<tr class=\"row" << (row % 2)  << "\">" << std::endl;
      ++row;
      std::string attr = method_attributes (i->second.first, method_doc, false, false);
      if (! attr.empty ()) {
        os << "<td><i>[" << escape_xml (method_attributes (i->second.first, method_doc)) << "]</i></td>";
      } else {
        os << "<td></td>";
      }
      os << "<td>" << method_return (i->second.first, method_doc) << "</td>";
      os << "<td><b><a href=\"#method" << n << "\">" << escape_xml (i->first) << "</a></b></td>";
      os << "<td>" << method_arguments (i->second.first, cls_obj, method_doc) << "</td>";
      os << "<td>";
      if (nd_syn != i->second.first->end_synonyms ()) {
        os << tl::to_string (QObject::tr ("Use of this method is deprecated. Use")) << " " << full_name (*nd_syn) << " " << tl::to_string (QObject::tr ("instead"));
      } else {
        os << tl::to_string (QObject::tr ("Use of this method is deprecated"));
      }
      os << "</td>";
      os << "</tr>" << std::endl;

    }

  }

  if (any) {
    os << "</table>" << std::endl;
  }

  n = 0;

  os << "<a name=\"detailed\"/><h2>" << tl::to_string (QObject::tr ("Detailed description")) << "</h2>" << std::endl;

  for (std::multimap <std::string, std::pair<const gsi::MethodBase *, size_t> >::const_iterator i = mm.begin (); i != mm.end (); ++i, ++n) {

    const gsi::MethodBase::MethodSynonym &syn = i->second.first->begin_synonyms () [i->second.second];

    DocumentationParser method_doc (i->second.first);

    os << "<a name=\"method" << n << "\"/>"
       << "<a name=\"m_" << escape_xml (i->first) << "\"/>"
       << "<keyword title=\"" << tl::to_string (QObject::tr ("API reference - Class")) << " " << escape_xml (cls) << ", " << tl::to_string (QObject::tr ("Method")) << " " << escape_xml (i->first) <<  "\" name=\"" << escape_xml (cls) << "#" << escape_xml (i->first) << "\"/>" << std::endl;
    
    os << "<h3>";
    std::string attr = method_attributes (i->second.first, method_doc);
    if (! attr.empty ()) {
      os << "<i>[" << escape_xml (attr) << "] </i>";
    }
    os << method_return (i->second.first, method_doc, true) << " " << escape_xml (i->first) << method_arguments (i->second.first, cls_obj, method_doc, true, "");
    os << "</h3>" << std::endl;

    os << "<p><b>" << tl::to_string (QObject::tr ("Description")) << "</b>: " << replace_references (escape_xml (method_doc.brief_doc), cls_obj) << "</p>" << std::endl;

    if (! method_doc.params.empty () || ! method_doc.ret_val.empty ()) {
      os << "<table>" << std::endl;
      for (std::vector<std::pair <std::string, std::string> >::const_iterator p = method_doc.params.begin (); p != method_doc.params.end (); ++p) {
        os << "<tr><td><b>" << escape_xml (p->first) << "</b>:</td><td>" << replace_references (escape_xml (p->second), cls_obj) << "</td></tr>" << std::endl;
      }
      if (! method_doc.ret_val.empty ()) {
        os << "<tr><td><b>" << tl::to_string (QObject::tr ("Returns")) << "</b>:</td><td>" << replace_references (escape_xml (method_doc.ret_val), cls_obj) << "</td></tr>" << std::endl;
      }
      os << "</table>" << std::endl;
    }

    if (syn.deprecated) {

      gsi::MethodBase::synonym_iterator nd_syn = i->second.first->begin_synonyms ();
      while (nd_syn != i->second.first->end_synonyms () && nd_syn->deprecated) {
        ++nd_syn;
      }

      if (nd_syn != i->second.first->end_synonyms ()) {
        os << "<p>" << tl::to_string (QObject::tr ("Use of this method is deprecated. Use")) << " " << full_name (*nd_syn) << " " << tl::to_string (QObject::tr ("instead")) << "</p>" << std::endl;
      } else {
        os << "<p>" << tl::to_string (QObject::tr ("Use of this method is deprecated")) << "</p>" << std::endl;
      }

    } else {
      std::string dh = method_doc.doc;
      if (! tl::Extractor (dh.c_str ()).at_end ()) {
        os << "<p>" << replace_references (method_doc.doc_html (), cls_obj) << "</p>" << std::endl;
      }
    }

  }

  os << "</doc>" << std::endl;
  return os.str ();

}

static tl::RegisteredClass<lay::HelpProvider> gsi_help_provider (new GSIHelpProvider (), 1000);

}


