#include <nokogiri.h>

VALUE cNokogiriXmlXpathContext;

/*
 * these constants have matching declarations in
 * ext/java/nokogiri/internals/NokogiriNamespaceContext.java
 */
static const xmlChar *NOKOGIRI_BUILTIN_PREFIX = (const xmlChar *)"nokogiri-builtin";
static const xmlChar *NOKOGIRI_BUILTIN_URI = (const xmlChar *)"https://www.nokogiri.org/default_ns/ruby/builtins";

static void
deallocate(xmlXPathContextPtr ctx)
{
  NOKOGIRI_DEBUG_START(ctx);
  xmlXPathFreeContext(ctx);
  NOKOGIRI_DEBUG_END(ctx);
}

/* find a CSS class in an HTML element's `class` attribute */
static const xmlChar *
builtin_css_class(const xmlChar *str, const xmlChar *val)
{
  int val_len;

  if (str == NULL) { return (NULL); }
  if (val == NULL) { return (NULL); }

  val_len = xmlStrlen(val);
  if (val_len == 0) { return (str); }

  while (*str != 0) {
    if ((*str == *val) && !xmlStrncmp(str, val, val_len)) {
      const xmlChar *next_byte = str + val_len;

      /* only match if the next byte is whitespace or end of string */
      if ((*next_byte == 0) || (IS_BLANK_CH(*next_byte))) {
        return ((const xmlChar *)str);
      }
    }

    /* advance str to whitespace */
    while ((*str != 0) && !IS_BLANK_CH(*str)) {
      str++;
    }

    /* advance str to start of next word or end of string */
    while ((*str != 0) && IS_BLANK_CH(*str)) {
      str++;
    }
  }

  return (NULL);
}

/* xmlXPathFunction to wrap builtin_css_class() */
static void
xpath_builtin_css_class(xmlXPathParserContextPtr ctxt, int nargs)
{
  xmlXPathObjectPtr hay, needle;

  CHECK_ARITY(2);

  CAST_TO_STRING;
  needle = valuePop(ctxt);
  if ((needle == NULL) || (needle->type != XPATH_STRING)) {
    xmlXPathFreeObject(needle);
    XP_ERROR(XPATH_INVALID_TYPE);
  }

  CAST_TO_STRING;
  hay = valuePop(ctxt);
  if ((hay == NULL) || (hay->type != XPATH_STRING)) {
    xmlXPathFreeObject(hay);
    xmlXPathFreeObject(needle);
    XP_ERROR(XPATH_INVALID_TYPE);
  }

  if (builtin_css_class(hay->stringval, needle->stringval)) {
    valuePush(ctxt, xmlXPathNewBoolean(1));
  } else {
    valuePush(ctxt, xmlXPathNewBoolean(0));
  }

  xmlXPathFreeObject(hay);
  xmlXPathFreeObject(needle);
}

/*
 * call-seq:
 *  register_ns(prefix, uri)
 *
 * Register the namespace with +prefix+ and +uri+.
 */
static VALUE
register_ns(VALUE self, VALUE prefix, VALUE uri)
{
  xmlXPathContextPtr ctx;
  Data_Get_Struct(self, xmlXPathContext, ctx);

  xmlXPathRegisterNs(ctx,
                     (const xmlChar *)StringValueCStr(prefix),
                     (const xmlChar *)StringValueCStr(uri)
                    );
  return self;
}

/*
 * call-seq:
 *  register_variable(name, value)
 *
 * Register the variable +name+ with +value+.
 */
static VALUE
register_variable(VALUE self, VALUE name, VALUE value)
{
  xmlXPathContextPtr ctx;
  xmlXPathObjectPtr xmlValue;
  Data_Get_Struct(self, xmlXPathContext, ctx);

  xmlValue = xmlXPathNewCString(StringValueCStr(value));

  xmlXPathRegisterVariable(ctx,
                           (const xmlChar *)StringValueCStr(name),
                           xmlValue
                          );

  return self;
}

void
Nokogiri_marshal_xpath_funcall_and_return_values(xmlXPathParserContextPtr ctx, int nargs, VALUE handler,
    const char *function_name)
{
  int i;
  VALUE result, doc;
  VALUE *argv;
  VALUE node_set = Qnil;
  xmlNodeSetPtr xml_node_set = NULL;
  xmlXPathObjectPtr obj;

  assert(ctx->context->doc);
  assert(DOC_RUBY_OBJECT_TEST(ctx->context->doc));

  argv = (VALUE *)calloc((size_t)nargs, sizeof(VALUE));
  for (i = 0 ; i < nargs ; ++i) {
    rb_gc_register_address(&argv[i]);
  }

  doc = DOC_RUBY_OBJECT(ctx->context->doc);

  if (nargs > 0) {
    i = nargs - 1;
    do {
      obj = valuePop(ctx);
      switch (obj->type) {
      case XPATH_STRING:
        argv[i] = NOKOGIRI_STR_NEW2(obj->stringval);
        break;
      case XPATH_BOOLEAN:
        argv[i] = obj->boolval == 1 ? Qtrue : Qfalse;
        break;
      case XPATH_NUMBER:
        argv[i] = rb_float_new(obj->floatval);
        break;
      case XPATH_NODESET:
        argv[i] = noko_xml_node_set_wrap(obj->nodesetval, doc);
        break;
      default:
        argv[i] = NOKOGIRI_STR_NEW2(xmlXPathCastToString(obj));
      }
      xmlXPathFreeNodeSetList(obj);
    } while (i-- > 0);
  }

  result = rb_funcall2(handler, rb_intern((const char *)function_name), nargs, argv);

  for (i = 0 ; i < nargs ; ++i) {
    rb_gc_unregister_address(&argv[i]);
  }
  free(argv);

  switch (TYPE(result)) {
  case T_FLOAT:
  case T_BIGNUM:
  case T_FIXNUM:
    xmlXPathReturnNumber(ctx, NUM2DBL(result));
    break;
  case T_STRING:
    xmlXPathReturnString(
      ctx,
      xmlCharStrdup(StringValueCStr(result))
    );
    break;
  case T_TRUE:
    xmlXPathReturnTrue(ctx);
    break;
  case T_FALSE:
    xmlXPathReturnFalse(ctx);
    break;
  case T_NIL:
    break;
  case T_ARRAY: {
    VALUE args[2];
    args[0] = doc;
    args[1] = result;
    node_set = rb_class_new_instance(2, args, cNokogiriXmlNodeSet);
    Data_Get_Struct(node_set, xmlNodeSet, xml_node_set);
    xmlXPathReturnNodeSet(ctx, xmlXPathNodeSetMerge(NULL, xml_node_set));
  }
  break;
  case T_DATA:
    if (rb_obj_is_kind_of(result, cNokogiriXmlNodeSet)) {
      Data_Get_Struct(result, xmlNodeSet, xml_node_set);
      /* Copy the node set, otherwise it will get GC'd. */
      xmlXPathReturnNodeSet(ctx, xmlXPathNodeSetMerge(NULL, xml_node_set));
      break;
    }
  default:
    rb_raise(rb_eRuntimeError, "Invalid return type");
  }
}

static void
ruby_funcall(xmlXPathParserContextPtr ctx, int nargs)
{
  VALUE handler = Qnil;
  const char *function = NULL ;

  assert(ctx);
  assert(ctx->context);
  assert(ctx->context->userData);
  assert(ctx->context->function);

  handler = (VALUE)(ctx->context->userData);
  function = (const char *)(ctx->context->function);

  Nokogiri_marshal_xpath_funcall_and_return_values(ctx, nargs, handler, function);
}

static xmlXPathFunction
lookup(void *ctx,
       const xmlChar *name,
       const xmlChar *ns_uri)
{
  VALUE xpath_handler = (VALUE)ctx;
  if (rb_respond_to(xpath_handler, rb_intern((const char *)name))) {
    return ruby_funcall;
  }

  return NULL;
}

NORETURN(static void xpath_generic_exception_handler(void *ctx, const char *msg, ...));
static void
xpath_generic_exception_handler(void *ctx, const char *msg, ...)
{
  char *message;

  va_list args;
  va_start(args, msg);
  vasprintf(&message, msg, args);
  va_end(args);

  rb_raise(rb_eRuntimeError, "%s", message);
}

/*
 * call-seq:
 *  evaluate(search_path, handler = nil)
 *
 * Evaluate the +search_path+ returning an XML::XPath object.
 */
static VALUE
evaluate(int argc, VALUE *argv, VALUE self)
{
  VALUE search_path, xpath_handler;
  VALUE thing = Qnil;
  xmlXPathContextPtr ctx;
  xmlXPathObjectPtr xpath;
  xmlChar *query;

  Data_Get_Struct(self, xmlXPathContext, ctx);

  if (rb_scan_args(argc, argv, "11", &search_path, &xpath_handler) == 1) {
    xpath_handler = Qnil;
  }

  query = (xmlChar *)StringValueCStr(search_path);

  if (Qnil != xpath_handler) {
    /* FIXME: not sure if this is the correct place to shove private data. */
    ctx->userData = (void *)xpath_handler;
    xmlXPathRegisterFuncLookup(ctx, lookup, (void *)xpath_handler);
  }

  xmlResetLastError();
  xmlSetStructuredErrorFunc(NULL, Nokogiri_error_raise);

  /* For some reason, xmlXPathEvalExpression will blow up with a generic error */
  /* when there is a non existent function. */
  xmlSetGenericErrorFunc(NULL, xpath_generic_exception_handler);

  xpath = xmlXPathEvalExpression(query, ctx);
  xmlSetStructuredErrorFunc(NULL, NULL);
  xmlSetGenericErrorFunc(NULL, NULL);

  if (xpath == NULL) {
    xmlErrorPtr error = xmlGetLastError();
    rb_exc_raise(Nokogiri_wrap_xml_syntax_error(error));
  }

  assert(ctx->doc);
  assert(DOC_RUBY_OBJECT_TEST(ctx->doc));

  switch (xpath->type) {
  case XPATH_STRING:
    thing = NOKOGIRI_STR_NEW2(xpath->stringval);
    xmlFree(xpath->stringval);
    break;
  case XPATH_NODESET:
    thing = noko_xml_node_set_wrap(xpath->nodesetval,
                                   DOC_RUBY_OBJECT(ctx->doc));
    break;
  case XPATH_NUMBER:
    thing = rb_float_new(xpath->floatval);
    break;
  case XPATH_BOOLEAN:
    thing = xpath->boolval == 1 ? Qtrue : Qfalse;
    break;
  default:
    thing = noko_xml_node_set_wrap(NULL, DOC_RUBY_OBJECT(ctx->doc));
  }

  xmlXPathFreeNodeSetList(xpath);

  return thing;
}

/*
 * call-seq:
 *  new(node)
 *
 * Create a new XPathContext with +node+ as the reference point.
 */
static VALUE
new (VALUE klass, VALUE nodeobj)
{
  xmlNodePtr node;
  xmlXPathContextPtr ctx;
  VALUE self;

  Data_Get_Struct(nodeobj, xmlNode, node);

  xmlXPathInit();

  ctx = xmlXPathNewContext(node->doc);
  ctx->node = node;

  xmlXPathRegisterNs(ctx, NOKOGIRI_BUILTIN_PREFIX, NOKOGIRI_BUILTIN_URI);
  xmlXPathRegisterFuncNS(ctx, (const xmlChar *)"css-class", NOKOGIRI_BUILTIN_URI,
                         xpath_builtin_css_class);

  self = Data_Wrap_Struct(klass, 0, deallocate, ctx);
  return self;
}

void
noko_init_xml_xpath_context(void)
{
  /*
   * XPathContext is the entry point for searching a Document by using XPath.
   */
  cNokogiriXmlXpathContext = rb_define_class_under(mNokogiriXml, "XPathContext", rb_cObject);

  rb_define_singleton_method(cNokogiriXmlXpathContext, "new", new, 1);

  rb_define_method(cNokogiriXmlXpathContext, "evaluate", evaluate, -1);
  rb_define_method(cNokogiriXmlXpathContext, "register_variable", register_variable, 2);
  rb_define_method(cNokogiriXmlXpathContext, "register_ns", register_ns, 2);
}
