/***************************************************************************
                          xsltransformation.cpp -  description
                             -------------------
    begin                : Sat Oct 8 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : vdboor --at-- codingdomain.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 "xsltransformation.h"

#include "../kmessdebug.h"

// stdlib.h is required to build on Solaris
#include <stdlib.h>
#include <string.h>

#include <libxml/globals.h>
#include <libxml/parser.h>
#include <libxslt/xsltconfig.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>  // After xsltconfig and xsltInternals or it breaks compiling on some libxslt versions.

#include <QFile>
#include <QRegExp>

#include <KLocale>
#include <KUrl>


/*
 * The kopetexsl.cpp file from Kopete is used as working
 * demo/example to create this code, since it's more
 * to-the-point than the online libxml documentation.
 */


// The constructor
XslTransformation::XslTransformation()
  : styleSheet_(0)
  , xslDoc_(0)
  , xslParams_(0)
  , xslParamCount_(0)
{
  // Init libxml
  xmlLoadExtDtdDefaultValue = 0;
  xmlSubstituteEntitiesDefault( 1 );
}



// The destructor
XslTransformation::~XslTransformation()
{
  if(styleSheet_ != 0)
  {
    xsltFreeStylesheet(styleSheet_);
    // also frees xslDoc_
  }

  // Remove xsl params
  for( int i = 0; i < xslParamCount_; i++ )
  {
    free( xslParams_[i] );
  }
  delete[] xslParams_;

  xsltCleanupGlobals();
  xmlCleanupParser();
}



// Convert an XML string
QString XslTransformation::convertXmlString( const QString &xml ) const
{
  // Asserts
  if( KMESS_NULL(styleSheet_) ) return QString::null;

  // Intermediate variables
  QString  result;
  xmlDoc  *resultXmlDoc = 0;
  xmlDoc  *sourceXmlDoc = 0;
  xmlChar *xmlData      = 0;
  int      xmlDataSize  = 0;

  // Convert data to C-string
  // It needs to be stored in a local variable
  // before it can be passed to a function.
  QByteArray xmlCString = xml.toUtf8();

  // Create XmlDoc
  sourceXmlDoc = xmlParseMemory( xmlCString.data(), xmlCString.length() );
  if( sourceXmlDoc == 0 )
  {
    kmWarning() << "could not parse the XML data.";
    return QString::null;
  }

  // Apply XSL
  resultXmlDoc = xsltApplyStylesheet( styleSheet_, sourceXmlDoc, (const char**)xslParams_ );
  if( resultXmlDoc == 0 )
  {
    kmWarning() << "could not convert the XML data.";
    goto cleanSource;
  }

  // Save
  // Fills xmlData with the results.
  xmlDocDumpMemory( resultXmlDoc, &xmlData, &xmlDataSize );

  // Load result in a QString, strip xml header and last \n from result string
  result = QString::fromUtf8( reinterpret_cast<char*>( xmlData ), xmlDataSize );
  result = result.replace( QRegExp("^<\\?xml [^?]+\\?>\n?"), QString::null )
                 .replace( QRegExp("\r?\n?$"),               QString::null );

  // Reverse calls to free intermediate results too.
  // You gotta love C-API's.. ;-)

//cleanAll:
  xmlFree( xmlData );
  xmlFreeDoc( resultXmlDoc );

cleanSource:
  xmlFreeDoc( sourceXmlDoc );

  // Return result
  // xsltApplyStylesheet returns an empty result
  // when the parsing of user parameters failed.
  if( result.isEmpty() )
  {
    return QString::null;
  }
  else
  {
    return result;
  }
}



// Check whether a stylesheet is loaded successfully
bool XslTransformation::hasStylesheet() const
{
  return (styleSheet_ != 0);
}



// Set an XSL parameter
void XslTransformation::setParameter( const QString &key, const QVariant &value )
{
  parametersMap_[ key ] = value.toString();
  updateParametersList();
}



// Set the XSL parameters
void XslTransformation::setParameters( const QMap<QString,QString> xslParameters )
{
  parametersMap_ = xslParameters;
  updateParametersList();
}



// Set the XSL stylesheet.
void XslTransformation::setStylesheet(const QString &xslFileName)
{
  // No expensive operations when file is identical.
  if(xslFileName == xslFileName_) return;
  xslFileName_ = xslFileName;

#ifdef KMESSDEBUG_XSLTRANSFORMATION
  kmDebug() << "loading new style sheet '" << xslFileName << "'";
#endif

  // Clean up previous stylesheet
  if(styleSheet_ != 0)
  {
    xsltFreeStylesheet(styleSheet_);
    // also frees xslDoc_
    styleSheet_ = 0;
    xslDoc_     = 0;
  }

  // Get file path
  KUrl fileUrl(xslFileName);
//   QByteArray basePath = fileUrl.directory( KUrl::AppendTrailingSlash ).toUtf8();

  // Open XSL file
  QFile file(xslFileName);
  if( ! file.open(QIODevice::ReadOnly) )
  {
    kmWarning() << "could not load '" << xslFileName << "'!";
    return;
  }

  // Read all data
  QByteArray fileData = file.readAll();
  file.close();

  // Parse file data as XML
  xslDoc_ = xmlParseMemory( fileData.data(), fileData.size() );
  if(xslDoc_ == 0)
  {
    kmWarning() << "could not parse XML from '" << xslFileName << "'!";
    return;
  }

  // Documentation on the libxml data types (all have public fields):
  // http://xmlsoft.org/html/libxml-tree.html#xmlDoc
  // http://xmlsoft.org/html/libxml-tree.html#xmlNode
  // http://www.xmlsoft.org/examples/index.html

  // Useful commands:
  // xmlDocDump( stdout, xslDoc_ );
  //

  xmlNode *xslRoot = xmlDocGetRootElement( xslDoc_ );

  // Set base path (e.g. xml:base attribute), required for xsl imports/includes
//  xmlNodeSetBase( xslRoot, reinterpret_cast<const xmlChar*>( basePath.data() ) );


  // Process structure, replace translated strings.
/*
  // Find the prefix the XML document uses for the "XSL namespace"
  xmlNs *xslNs = xmlSearchNsByHref( xslDoc_, xslRoot, (xmlChar*) "http://www.w3.org/1999/XSL/Transform" );
  if( xslNs == 0 )
  {
    kmWarning() << "Could not find XSL namespace in chat style.";
  }
*/


  // Translate the <xsl:text> nodes in the XSL document.
  xmlNs *xslNs   = xmlSearchNsByHref( xslDoc_, xslRoot, (xmlChar*) "http://www.w3.org/1999/XSL/Transform" );
  xmlNs *kmessNs = xmlSearchNsByHref( xslDoc_, xslRoot, (xmlChar*) "http://www.kmess.org/xmlns/ChatStyles/v1/" );
  if( xslNs == 0 || kmessNs == 0 )
  {
    kmWarning() << "'xsl' and 'kmess' namespaces not found in '"
                << xslFileName << "' chat style, "
                << "chat style can't be translated." << endl;
  }
  else
  {
    updateXslTranslations( xslRoot, xslNs, kmessNs );
  }


  // Parse XML data as XSL
  styleSheet_ = xsltParseStylesheetDoc( xslDoc_ );
  if( styleSheet_ == 0 || styleSheet_->errors != 0 )
  {
    // Also happens when an <xsl:import> fails
    kmWarning() << "could not parse XSL from '" << xslFileName << "'!";
    xmlFreeDoc( xslDoc_ );
    xslDoc_ = 0;
  }
#ifdef KMESSDEBUG_XSLTRANSFORMATION
  else
  {
    kmDebug() << "style sheet loaded.";
  }
#endif

}



// Refresh the actual XSL parameters list
void XslTransformation::updateParametersList()
{
  // Delete the previous parameter list
  for( int i = 0; i < xslParamCount_; i++ )
  {
    delete xslParams_[i];
  }
  delete[] xslParams_;

  // Is the list empty? No parameters will be passed to the parser
  if( parametersMap_.isEmpty() )
  {
    xslParams_ = 0;
    return;
  }

  // Update the XSL parameters list with our list
  int argCount = parametersMap_.count() * 2 + 1;
  xslParams_   = new char*[ argCount ];

  int i = 0;
  QMap<QString, QString>::const_iterator it;
  for( it = parametersMap_.begin(); it != parametersMap_.end(); ++it )
  {
    // Duplicate the strings as they will be deleted outside this scope.
    QByteArray key    ( it.key().toUtf8() );
    QByteArray value  ( QString( "'" + it.value() + "'" ).toUtf8() );
    xslParams_[ i++ ] = strdup( key  .data() );
    xslParams_[ i++ ] = strdup( value.data() );
  }

  xslParamCount_    = i;
  xslParams_[ i++ ] = 0;

#ifdef KMESSTEST
  KMESS_ASSERT( i == argCount );
#endif
}



// Update the translation strings in the XSL document.
void XslTransformation::updateXslTranslations( xmlNode *node, const xmlNs *xslNs, const xmlNs *kmessNs )
{
  // This method updates the string of all nodes which match:
  //  <xsl:text kmess:translate="true">fdsfsd</xsl:text>
  // the contents is trimmed to avoid translations are broken too easily.

  QRegExp trimmer("^([ \t\r\n\f\v]+)(.+)([ \t\r\n\f\v]+)$");

  // Browse all child nodes
  for( xmlNode *curNode = node; curNode != 0; curNode = curNode->next )
  {
    // Ignore leaf elements, even <xsl:text> elements have a text() childnode,
    if( curNode->children == 0 )
    {
      continue;
    }

    // Check node properties.
    if( curNode->type == XML_ELEMENT_NODE
    &&  curNode->ns   == xslNs
    &&  xmlStrEqual( curNode->name, (const xmlChar *) "text" ) )
    {
      // Found <xsl:text> node
      // Check if node has kmess:translate="true" property.
      xmlChar *translate  = xmlGetNsProp( curNode, (const xmlChar *) "translate", kmessNs->href );
      bool kmessTranslate = ( translate != 0 && xmlStrEqual( translate,  (const xmlChar *) "true" ) );
      if( translate != 0 )
      {
        xmlFree( translate );
      }

      // Ignore if node is not marked for translation.
      if( ! kmessTranslate )
      {
        continue;
      }

      // Get the content and convert to QString.
      xmlChar *rawContent = xmlNodeGetContent( curNode );
      QString content( QString::fromUtf8( (const char *) rawContent, xmlStrlen( rawContent ) ) );
      xmlFree( rawContent );

#ifdef KMESSDEBUG_XSLTRANSFORMATION
      QString oldContent( content );
#endif

      // Trim and simplyfy spaces to avoid breaking translations too easily.
      // Ignore <xsl:text> </xsl:text> nodes.
      bool timmed = trimmer.indexIn( content ) != -1;
      content = content.simplified();
      if( content.isEmpty() )
      {
        continue;
      }

      // Translate the string.
      // Add the trimmed spaces back.
      content = i18nc( "chat-style-text", content.toLatin1() );
      if( timmed )
      {
        content = trimmer.cap(1) + content + trimmer.cap(3);
      }

#ifdef KMESSDEBUG_XSLTRANSFORMATION
      kmDebug() << "translated <xsl:text> node \"" << oldContent << "\""
                << " to \"" << content << "\" using i18n(\"" << oldContent.simplified() << "\")." << endl;
#endif

      // Write the translated string back.
      QByteArray utf8Content = content.toUtf8();  // store to avoid cleanup before end of if-block.
      const xmlChar *newRawContent = (const xmlChar*) utf8Content.data();
      xmlNodeSetContent( curNode, newRawContent );
      continue;
    }

    // Recursion for sub elements.
    updateXslTranslations( curNode->children, xslNs, kmessNs );
  }
}

