/***************************************************************************
	pseudoDtd.cpp
	copyright			: (C) 2001-2002 by Daniel Naber
	email				: daniel.naber@t-online.de
 ***************************************************************************/

/***************************************************************************
 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ***************************************************************************/

#include "pseudo_dtd.h"

#include <assert.h>

#include <qdom.h>
#include <qregexp.h>

#include <klocale.h>
#include <kmessagebox.h>

PseudoDTD::PseudoDTD()
{
  // "SGML support" only means case-insensivity, because HTML is case-insensitive up to version 4:
  m_sgmlSupport = true;	  // TODO: make this an run-time option ( maybe automatically set )
}

PseudoDTD::~PseudoDTD()
{
}

void PseudoDTD::analyzeDTD( QString &metaDtdUrl, QString &metaDtd )
{
  QDomDocument doc( "dtdIn_xml" );
  if ( ! doc.setContent( metaDtd) )
  {
    KMessageBox::error(0, i18n("The file '%1' could not be parsed. "
        "Please check that the file is well-formed XML.", metaDtdUrl ),
        i18n( "XML Plugin Error") );
    return;
  }

  if ( doc.doctype().name() != "dtd" )
  {
    KMessageBox::error(0, i18n("The file '%1' is not in the expected format. "
        "Please check that the file is of this type:\n"
            "-//Norman Walsh//DTD DTDParse V2.0//EN\n"
            "You can produce such files with dtdparse. "
            "See the Kate Plugin documentation for more information.", metaDtdUrl ),
        i18n("XML Plugin Error") );
    return;
  }

  uint listLength = 0;
  listLength += doc.elementsByTagName( "entity" ).count();
  listLength += doc.elementsByTagName( "element" ).count();
	// count this twice, as it will be iterated twice ( TODO: optimize that? ):
  listLength += doc.elementsByTagName( "attlist" ).count() * 2;

  QProgressDialog progress( i18n("Analyzing meta DTD..."), i18n("Cancel"), 0, listLength );
  progress.setMinimumDuration( 400 );
  progress.setValue(0);

  // Get information from meta DTD and put it in Qt data structures for fast access:
  if( ! parseEntities( &doc, &progress ) )
    return;

  if( ! parseElements( &doc, &progress ) )
    return;

  if( ! parseAttributes( &doc, &progress ) )
    return;

  if( ! parseAttributeValues( &doc, &progress ) )
    return;

  progress.setValue( listLength );	// just to make sure the dialog disappears

}

// ========================================================================
// DOM stuff:

/**
 * Iterate through the XML to get a mapping which sub-elements are allowed for
 * all elements.
 */
bool PseudoDTD::parseElements( QDomDocument *doc, QProgressDialog *progress )
{

  m_elementsList.clear();
	// We only display a list, i.e. we pretend that the content model is just
	// a set, so we use a map. This is necessay e.g. for xhtml 1.0's head element,
	// which would otherwise display some elements twice.
  QMap<QString,bool> subelementList;	// the bool is not used

  QDomNodeList list = doc->elementsByTagName( "element" );
  uint listLength = list.count();      // speedup (really! )

  for( uint i = 0; i < listLength; i++ )
  {
    if( progress->wasCanceled() )
      return false;

      progress->setValue( progress->value()+1 );
    // FIXME!:
    //qApp->processEvents();

    subelementList.clear();
    QDomNode node = list.item( i );
    QDomElement elem = node.toElement();

    if( !elem.isNull() )
    {
      // Enter the expanded content model, which may also include stuff not allowed.
      // We do not care if it's a <sequence-group> or whatever.
      QDomNodeList contentModelList = elem.elementsByTagName( "content-model-expanded" );
      QDomNode contentModelNode = contentModelList.item(0);
      QDomElement contentModelElem = contentModelNode.toElement();
      if( ! contentModelElem.isNull() )
      {
        // check for <pcdata/>:
        QDomNodeList pcdataList = contentModelElem.elementsByTagName( "pcdata" );

        // check for other sub elements:
        QDomNodeList subList = contentModelElem.elementsByTagName( "element-name" );
        uint subListLength = subList.count();
        for( uint l = 0; l < subListLength; l++ )
        {
          QDomNode subNode = subList.item(l);
          QDomElement subElem = subNode.toElement();
          if( !subElem.isNull() )
            subelementList[subElem.attribute( "name" )] = true;
        }

        // anders: check if this is an EMPTY element, and put "__EMPTY" in the
        // sub list, so that we can insert tags in empty form if required.
        QDomNodeList emptyList = elem.elementsByTagName( "empty" );
        if ( emptyList.count() )
          subelementList["__EMPTY"] = true;
      }

      // Now remove the elements not allowed (e.g. <a> is explicitly not allowed in <a>
      // in the HTML 4.01 Strict DTD):
      QDomNodeList exclusionsList = elem.elementsByTagName( "exclusions" );
      if( exclusionsList.length() > 0 )
      {	// sometimes there are no exclusions ( e.g. in XML DTDs there are never exclusions )
        QDomNode exclusionsNode = exclusionsList.item(0);
        QDomElement exclusionsElem = exclusionsNode.toElement();
        if( ! exclusionsElem.isNull() )
        {
          QDomNodeList subList = exclusionsElem.elementsByTagName( "element-name" );
          uint subListLength = subList.count();
          for( uint l = 0; l < subListLength; l++ )
          {
            QDomNode subNode = subList.item(l);
            QDomElement subElem = subNode.toElement();
            if( !subElem.isNull() )
            {
              QMap<QString,bool>::Iterator it = subelementList.find( subElem.attribute( "name" ) );
              if( it != subelementList.end() )
                subelementList.erase(it);
            }
          }
        }
      }

      // turn the map into a list:
      QStringList subelementListTmp;
      QMap<QString,bool>::Iterator it;
      for( it = subelementList.begin(); it != subelementList.end(); ++it )
        subelementListTmp.append( it.key() );

        m_elementsList.insert( elem.attribute( "name" ), subelementListTmp );

    }

  } // end iteration over all <element> nodes
  return true;
}

/**
 * Check which elements are allowed inside a parent element. This returns
 * a list of allowed elements, but it doesn't care about order or if only a certain
 * number of occurrences is allowed.
 */
QStringList PseudoDTD::allowedElements( QString parentElement )
{
  if( m_sgmlSupport )
  {
    // find the matching element, ignoring case:
    QMap<QString,QStringList>::Iterator it;
    for( it = m_elementsList.begin(); it != m_elementsList.end(); ++it )
    {
      if( it.key().compare( parentElement, Qt::CaseInsensitive ) == 0 )
        return it.value();
    }
  }
  else if( m_elementsList.contains(parentElement) )
    return m_elementsList[parentElement];

  return QStringList();
}

/**
 * Iterate through the XML to get a mapping which attributes are allowed inside
 * all elements.
 */
bool PseudoDTD::parseAttributes( QDomDocument *doc, QProgressDialog *progress )
{
  m_attributesList.clear();
//   QStringList allowedAttributes;
  QDomNodeList list = doc->elementsByTagName( "attlist" );
  uint listLength = list.count();

  for( uint i = 0; i < listLength; i++ )
  {
    if( progress->wasCanceled() )
      return false;

    progress->setValue( progress->value()+1 );
    // FIXME!!
    //qApp->processEvents();

    ElementAttributes attrs;
    QDomNode node = list.item(i);
    QDomElement elem = node.toElement();
    if( !elem.isNull() )
    {
      QDomNodeList attributeList = elem.elementsByTagName( "attribute" );
      uint attributeListLength = attributeList.count();
      for( uint l = 0; l < attributeListLength; l++ )
      {
        QDomNode attributeNode = attributeList.item(l);
        QDomElement attributeElem = attributeNode.toElement();

        if( ! attributeElem.isNull() )
        {
          if ( attributeElem.attribute("type") == "#REQUIRED" )
            attrs.requiredAttributes.append( attributeElem.attribute("name") );
          else
            attrs.optionalAttributes.append( attributeElem.attribute("name") );
        }
      }
      m_attributesList.insert( elem.attribute("name"), attrs );
    }
  }

  return true;
}

/** Check which attributes are allowed for an element.
 */
QStringList PseudoDTD::allowedAttributes( QString element )
{
  if( m_sgmlSupport )
  {
    // find the matching element, ignoring case:
    QMap<QString,ElementAttributes>::Iterator it;
    for( it = m_attributesList.begin(); it != m_attributesList.end(); ++it ) {
      if( it.key().compare( element, Qt::CaseInsensitive ) == 0 )
      {
        return it.value().optionalAttributes + it.value().requiredAttributes;
      }
    }
  }
  else if( m_attributesList.contains(element) )
    return m_attributesList[element].optionalAttributes + m_attributesList[element].requiredAttributes;

  return QStringList();
}

QStringList PseudoDTD::requiredAttributes( const QString &element ) const
{
  if ( m_sgmlSupport )
  {
    QMap<QString,ElementAttributes>::ConstIterator it;
    for( it = m_attributesList.begin(); it != m_attributesList.end(); ++it )
    {
      if( it.key().compare( element, Qt::CaseInsensitive ) == 0 )
        return it.value().requiredAttributes;
    }
  }
  else if( m_attributesList.contains(element) )
    return m_attributesList[element].requiredAttributes;

  return QStringList();
}

/**
 * Iterate through the XML to get a mapping which attribute values are allowed
 * for all attributes inside all elements.
 */
bool PseudoDTD::parseAttributeValues( QDomDocument *doc, QProgressDialog *progress )
{
  m_attributevaluesList.clear();						// 1 element : n possible attributes
  QMap<QString,QStringList> attributevaluesTmp;		// 1 attribute : n possible values
  QDomNodeList list = doc->elementsByTagName( "attlist" );
  uint listLength = list.count();

  for( uint i = 0; i < listLength; i++ )
  {
    if( progress->wasCanceled() )
      return false;

    progress->setValue( progress->value()+1 );
    // FIXME!
    //qApp->processEvents();

    attributevaluesTmp.clear();
    QDomNode node = list.item(i);
    QDomElement elem = node.toElement();
    if( !elem.isNull() )
    {
      // Enter the list of <attribute>:
      QDomNodeList attributeList = elem.elementsByTagName( "attribute" );
      uint attributeListLength = attributeList.count();
      for( uint l = 0; l < attributeListLength; l++ )
      {
        QDomNode attributeNode = attributeList.item(l);
        QDomElement attributeElem = attributeNode.toElement();
        if( ! attributeElem.isNull() )
        {
          QString value = attributeElem.attribute( "value" );
          attributevaluesTmp.insert( attributeElem.attribute("name"), value.split( QChar(' ') ) );
        }
      }
      m_attributevaluesList.insert( elem.attribute("name"), attributevaluesTmp );
    }
  }
  return true;
}

/**
 * Check which attributes values are allowed for an attribute in an element
 * (the element is necessary because e.g. "href" inside <a> could be different
 * to an "href" inside <link>):
 */
QStringList PseudoDTD::attributeValues( QString element, QString attribute )
{
  // Direct access would be faster than iteration of course but not always correct,
  // because we need to be case-insensitive.
  if( m_sgmlSupport ) {
    // first find the matching element, ignoring case:
    QMap< QString,QMap<QString,QStringList> >::Iterator it;
    for( it = m_attributevaluesList.begin(); it != m_attributevaluesList.end(); ++it )
    {
      if( it.key().compare( element, Qt::CaseInsensitive ) == 0 )
      {
        QMap<QString,QStringList> attrVals = it.value();
        QMap<QString,QStringList>::Iterator itV;
        // then find the matching attribute for that element, ignoring case:
        for( itV = attrVals.begin(); itV != attrVals.end(); ++itV )
        {
          if( itV.key().compare( attribute, Qt::CaseInsensitive ) == 0 )
            return( itV.value() );
        }
      }
    }
  }
  else if( m_attributevaluesList.contains(element) )
  {
    QMap<QString,QStringList> attrVals = m_attributevaluesList[element];
    if( attrVals.contains(attribute) )
      return attrVals[attribute];
  }

  // no predefined values available:
  return QStringList();
}

/**
 * Iterate through the XML to get a mapping of all entity names and their expanded
 * version, e.g. nbsp => &#160;. Parameter entities are ignored.
 */
bool PseudoDTD::parseEntities( QDomDocument *doc, QProgressDialog *progress )
{
  m_entityList.clear();
  QDomNodeList list = doc->elementsByTagName( "entity" );
  uint listLength = list.count();

  for( uint i = 0; i < listLength; i++ )
  {
    if( progress->wasCanceled() )
      return false;

    progress->setValue( progress->value()+1 );
    //FIXME!!
    //qApp->processEvents();
    QDomNode node = list.item(i);
    QDomElement elem = node.toElement();
    if( !elem.isNull()
         && elem.attribute( "type" ) != "param" )
    { // TODO: what's cdata <-> gen ?
      QDomNodeList expandedList = elem.elementsByTagName( "text-expanded" );
      QDomNode expandedNode = expandedList.item(0);
      QDomElement expandedElem = expandedNode.toElement();
      if( ! expandedElem.isNull() )
      {
        QString exp = expandedElem.text();
        // TODO: support more than one &#...; in the expanded text
        /* TODO include do this when the unicode font problem is solved:
        if( exp.contains(QRegExp("^&#x[a-zA-Z0-9]+;$")) ) {
        // hexadecimal numbers, e.g. "&#x236;"
        uint end = exp.find( ";" );
        exp = exp.mid( 3, end-3 );
        exp = QChar();
      } else if( exp.contains(QRegExp("^&#[0-9]+;$")) ) {
        // decimal numbers, e.g. "&#236;"
        uint end = exp.find( ";" );
        exp = exp.mid( 2, end-2 );
        exp = QChar( exp.toInt() );
      }
    */
        m_entityList.insert( elem.attribute("name"), exp );
      }
      else
      {
        m_entityList.insert( elem.attribute("name"), QString() );
      }
    }
  }
  return true;
}

/**
 * Get a list of all ( non-parameter ) entities that start with a certain string.
 */
QStringList PseudoDTD::entities( QString start )
{
  QStringList entities;
  QMap<QString,QString>::Iterator it;
  for( it = m_entityList.begin(); it != m_entityList.end(); ++it ) {
    if( (*it).startsWith(start) )
    {
      QString str = it.key();
      /* TODO: show entities as unicode character
      if( !it.data().isEmpty() ) {
      //str += " -- " + it.data();
      QRegExp re( "&#(\\d+);" );
      if( re.search(it.data()) != -1 ) {
      uint ch = re.cap( 1).toUInt();
      str += " -- " + QChar( ch).decomposition();
    }
    //kDebug() << "#" << it.data();
    }
   */
      entities.append( str );
    // TODO: later use a table view
    }
  }
  return entities;
}

// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
