/*
 * This file is part of the Poliqarp suite.
 * 
 * Copyright (C) 2009 by Instytut Podstaw Informatyki Polskiej
 * Akademii Nauk (IPI PAN; Institute of Computer Science, Polish
 * Academy of Sciences; cf. www.ipipan.waw.pl).  All rights reserved.
 * 
 * This file may be distributed and/or modified under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation and appearing in the file gpl.txt included in the packaging
 * of this file.  (See http://www.gnu.org/licenses/translations.html for
 * unofficial translations.)
 * 
 * A commercial license is available from IPI PAN (contact
 * Michal.Ciesiolka@ipipan.waw.pl or ipi@ipipan.waw.pl for more
 * information).  Licensees holding a valid commercial license from IPI
 * PAN may use this file in accordance with that license.
 * 
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE.
 */

#include <libxml/parserInternals.h>
#include <libxml/uri.h>
#include <libxml/xpathInternals.h>

#include <bp/ng/list.h>
#include <bp/ng/xpath.h>

typedef xmlXPathParserContext xpath_parser_context;

const char *xml_ns_poliqarp = "http://poliqarp.sourceforge.net/ns/2009";

struct xpath_user_data
{
   const char *id;
};

xpath_context *xpath_context_new(xml_document *document, const char *id)
{
   assert(document != NULL);
   assert(id != NULL);
   xpath_context *context = xmlXPathNewContext(document);
   if (context == NULL)
      bp_fatal("unable to create an XPath context");
   struct xpath_user_data *user_data = bp_malloc(sizeof (struct xpath_user_data));
   user_data->id = id;
   context->userData = user_data;
   return context;
}

void xpath_context_free(xpath_context *context)
{
   if (context == NULL)
      return;
   free(context->userData);
   xmlXPathFreeContext(context);
}

void xpath_register_ns(xpath_context *context, const char *prefix,
   const char *uri)
{
   int rc;
   rc = xmlXPathRegisterNs(context, (const xmlChar*) prefix,
      (const xmlChar*) uri);
   if (rc != 0)
      bp_fatal("unable to register XPath namespace: %s=%s", prefix, uri);
}

xpath_object *xpath_new(xpath_context *context, const char *path)
{
   xpath_object *object;
   object = xmlXPathEvalExpression((const xmlChar*) path, context);
   if (object == NULL) 
      bp_fatal("unable to evaluate XPath expression: %s", path);
   return object;
}

void xpath_free(xpath_object *object)
{
   xmlXPathFreeObject(object);
}

static void fn_document_id(xpath_parser_context *ctxt, int nargs)
{
   CHECK_ARITY(0);
   assert(ctxt->context->userData != NULL);
   const char * id = ((struct xpath_user_data*)ctxt->context->userData)->id;
   assert(id != NULL);
   xpath_object *object = xmlXPathNewCString(id);
   if (object == NULL)
      goto memory_error;
   if (valuePush(ctxt, object) == 0)
      goto memory_error;
   return;
memory_error:
   bp_system_error("memory allocation error");
}

static xml_document *cached_document = NULL;
#pragma omp threadprivate(cached_document)

static void fn_follow(xpath_parser_context *ctxt, int nargs)
{
   char *ids;
   CHECK_ARITY(2);
   {
      xpath_object *object;
      CAST_TO_STRING;
      CHECK_TYPE(XPATH_STRING);
      object = valuePop(ctxt);
      ids = bp_strdup((const char*)object->stringval);
      xpath_free(object);
   }
   {
      xpath_object *object;
      xmlChar *uri, *href, *base;
      CAST_TO_STRING;
      CHECK_TYPE(XPATH_STRING);
      object = valuePop(ctxt);
      href = object->stringval;
      base = xmlNodeGetBase(ctxt->context->doc, ctxt->context->node);
      if (base == NULL)
         uri = xmlBuildURI(href, ctxt->context->doc->URL);
      else
         uri = xmlBuildURI(href, base);
      if (uri == NULL) {
         xmlChar *esc_base = xmlURIEscape(base);
         xmlChar *esc_href = xmlURIEscape(href);
         uri = xmlBuildURI(esc_href, esc_base);
         free(esc_base);
         free(esc_href);
      }
      free(base);
      xpath_free(object);
      if (uri == NULL)
         goto invalid_operand;
      if (cached_document == NULL || strcmp((const char*) cached_document->URL, (const char*) uri) != 0) {
         if (cached_document != NULL)
            xml_document_free(cached_document);
         cached_document = xml_document_new((const char*) uri);
      }
      free(uri);
   }
   if (cached_document == NULL)
      goto invalid_operand;
   xmlNodeSetPtr result = xmlXPathNodeSetCreate(NULL);
   if (result == NULL)
      goto done;

   char *right, *left = ids;
   bool eot = false;
   while (!eot)
   {
      xmlAttrPtr attr;
      xmlNodePtr node;
      while (IS_BLANK_CH(*left))
         left++;
      if (*left != '#') {
         bp_warning("%s cannot be followed", cached_document->URL, left);
         while (!IS_BLANK_CH(*left) && (*left != '\0'))
            left++;
         if (*left == '\0')
            break;
      }
      right = left = left + 1;
      while (!IS_BLANK_CH(*right) && (*right != '\0'))
         right++;
      if (*right == '\0')
         eot = true;
      else
         *right = '\0';
      attr = xmlGetID(cached_document, (xmlChar*) left);
      if (attr == NULL)
         bp_warning("%s#%s not found", cached_document->URL, left);
      left = right + 1;
      if (attr == NULL)
         continue;
      if (attr->type == XML_ATTRIBUTE_NODE)
         node = attr->parent;
      else if (attr->type == XML_ELEMENT_NODE)
         node = (xmlNodePtr) attr;
      else
         node = NULL;
      if (node == NULL)
         continue;
      xmlXPathNodeSetAdd(result, node);
   }
done:
   free(ids);
   xmlXPathReturnNodeSet(ctxt, result);
   return;
invalid_operand:
   free(ids);
   XP_ERROR(XPATH_INVALID_OPERAND);
}

static void fn_strip_prefix(xpath_parser_context *ctxt, int nargs)
{
   size_t prefix_len;
   xpath_object *object, *prefix;
   CHECK_ARITY(2);
   object = valuePop(ctxt);
   CAST_TO_STRING;
   CHECK_TYPE(XPATH_STRING);
   prefix = valuePop(ctxt);
   prefix_len = xmlStrlen(prefix->stringval);
   if (object == NULL) {
      xpath_free(prefix);
      XP_ERROR(XPATH_INVALID_OPERAND);
   }
   if (object->type == XPATH_NODESET) {
      xmlNodeSetPtr nodeset = xmlXPathNodeSetCreate(NULL);
      if (nodeset != NULL && object->nodesetval != NULL) {
         int i;
         for (i = 0; i < object->nodesetval->nodeNr; i++) {
            xmlChar *strobject;
            strobject = xmlXPathCastNodeToString(object->nodesetval->nodeTab[i]);
            if (xmlStrncmp(prefix->stringval, strobject, prefix_len) == 0) {
               xml_node *node = xmlNewDocRawNode(ctxt->context->doc,
                  NULL,
                  (const xmlChar*) "poliqarp-temporary-node",
                  strobject + prefix_len
               );
               xmlAddChild((xmlNodePtr) ctxt->context->doc, node);
               xmlXPathNodeSetAdd(nodeset, node);
            }
            free(strobject);
         }
      }
      xmlXPathReturnNodeSet(ctxt, nodeset);
   } else {
      valuePush(ctxt, object);
      object = NULL;
      CAST_TO_STRING;
      CHECK_TYPE(XPATH_STRING);
      if (xmlStrncmp(prefix->stringval, ctxt->value->stringval, prefix_len) == 0) {
         xpath_object *result = xmlXPathNewString(valuePop(ctxt)->stringval + prefix_len);
         valuePush(ctxt, result);
      }
   }
   xpath_free(prefix);
   xpath_free(object);
}

void xpath_register_functions(xpath_context *context)
{
   xmlXPathRegisterFuncNS(context, (const xmlChar*) "follow",
      (const xmlChar*) xml_ns_poliqarp, fn_follow);
   xmlXPathRegisterFuncNS(context, (const xmlChar*) "strip-prefix",
      (const xmlChar*) xml_ns_poliqarp, fn_strip_prefix);
   xmlXPathRegisterFuncNS(context, (const xmlChar*) "document-id",
      (const xmlChar*) xml_ns_poliqarp, fn_document_id);
}

char *xpath_object_as_string(xpath_object *object)
{
   char *result = (char*) xmlXPathCastToString(object);
   if (result == NULL)
      bp_system_error("memory allocation error");
   return result;
}

void xpath_cleanup()
{
   #pragma omp parallel
   {
      if (cached_document != NULL)
         xml_document_free(cached_document);
   }
}
