/*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.4
   *
   * "(STR) Returns the lexical form of a literal; returns the codepoint representation of an IRI."
   *
   * @see http://www.w3.org/TR/rdf-sparql-query
   */
  private void convertStr(E_Str expr) {
    logger.debug("convertStr " + expr.toString());

    expr.getArg().visit(this);

    Expression arg = expression.pop();

    if (arg instanceof AttributeExprEx) {
      // make a new AttributeExprEx with changed NodeMaker, which returns plain literal
      // TODO this seems to work, but needs more testing.
      AttributeExprEx attribute = (AttributeExprEx) arg;
      TypedNodeMaker nodeMaker = (TypedNodeMaker) attribute.getNodeMaker();
      TypedNodeMaker newNodeMaker =
          new TypedNodeMaker(
              TypedNodeMaker.PLAIN_LITERAL, nodeMaker.valueMaker(), nodeMaker.isUnique());
      logger.debug("changing nodemaker " + nodeMaker + " to " + newNodeMaker);
      expression.push(
          new AttributeExprEx((Attribute) attribute.attributes().iterator().next(), newNodeMaker));
    } else if (arg instanceof ConstantEx) {
      ConstantEx constant = (ConstantEx) arg;
      Node node = constant.getNode();
      String lexicalForm = node.getLiteral().getLexicalForm();
      node = Node.createLiteral(lexicalForm);
      ConstantEx constantEx = new ConstantEx(NodeValue.makeNode(node).asString(), node);
      logger.debug("pushing " + constantEx);
      expression.push(constantEx);
    } else {
      conversionFailed(expr);
    }
  }
  /*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.7
   *
   * "(DATATYPE) Returns the datatype IRI of typedLit; returns xsd:string if the parameter is a simple literal."
   *
   * @see http://www.w3.org/TR/rdf-sparql-query
   */
  private void convertDataType(E_Datatype expr) {
    logger.debug("convertDataType " + expr.toString());

    expr.getArg().visit(this);

    Expression arg = expression.pop();

    if (arg instanceof AttributeExprEx) {
      AttributeExprEx variable = (AttributeExprEx) arg;
      NodeMaker nm = variable.getNodeMaker();
      DetermineNodeType filter = new DetermineNodeType();
      nm.describeSelf(filter);
      if (!filter.isLimittedToLiterals()) {
        // type error, return false?
        logger.warn("type error: " + variable + " is not a literal, returning FALSE");
        expression.push(Expression.FALSE);
        return;
      }
      RDFDatatype datatype = filter.getDatatype();
      logger.debug("datatype " + datatype);

      Node node =
          Node.createURI((datatype != null) ? datatype.getURI() : XSDDatatype.XSDstring.getURI());

      ConstantEx constantEx = new ConstantEx(NodeValue.makeNode(node).asString(), node);
      logger.debug("pushing " + constantEx);
      expression.push(constantEx);
    } else if (arg instanceof ConstantEx) {
      ConstantEx constant = (ConstantEx) arg;
      Node node = constant.getNode();
      if (!node.isLiteral()) {
        // type error, return false?
        logger.warn("type error: " + node + " is not a literal, returning FALSE");
        expression.push(Expression.FALSE);
        return;
      }
      RDFDatatype datatype = node.getLiteralDatatype();
      logger.debug("datatype " + datatype);
      node =
          Node.createURI((datatype != null) ? datatype.getURI() : XSDDatatype.XSDstring.getURI());
      ConstantEx constantEx = new ConstantEx(NodeValue.makeNode(node).asString(), node);
      logger.debug("pushing " + constantEx);
      expression.push(constantEx);
    } else {
      conversionFailed(expr);
    }
  }
  /*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.6
   *
   * "(LANG) Returns the language tag of ltrl, if it has one. It returns "" if ltrl has no language tag."
   * @see http://www.w3.org/TR/rdf-sparql-query
   */
  private void convertLang(E_Lang expr) {
    logger.debug("convertLang " + expr.toString());

    expr.getArg().visit(this);

    Expression arg = expression.pop();

    if (arg instanceof AttributeExprEx) {
      AttributeExprEx variable = (AttributeExprEx) arg;
      NodeMaker nm = variable.getNodeMaker();
      DetermineNodeType filter = new DetermineNodeType();
      nm.describeSelf(filter);
      String lang = filter.getLanguage();
      logger.debug("lang " + lang);
      if (lang == null) lang = "";

      // NodeValue.makeString(lang); TODO better?
      Node node = Node.createLiteral(lang);

      ConstantEx constantEx = new ConstantEx(NodeValue.makeNode(node).asString(), node);
      logger.debug("pushing " + constantEx);
      expression.push(constantEx);
    } else if (arg instanceof ConstantEx) {
      ConstantEx constant = (ConstantEx) arg;
      Node node = constant.getNode();
      if (!node.isLiteral()) {
        // type error, return false?
        logger.warn("type error: " + node + " is not a literal, returning FALSE");
        expression.push(Expression.FALSE);
        return;
      }
      String lang = node.getLiteralLanguage();
      logger.debug("lang " + lang);
      if (lang == null) lang = "";

      node = Node.createLiteral(lang);

      ConstantEx constantEx = new ConstantEx(NodeValue.makeNode(node).asString(), node);
      logger.debug("pushing " + constantEx);
      expression.push(constantEx);
    } else {
      conversionFailed(expr);
    }
  }
  /*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.12
   *
   * "(LANGMATCHES) returns true if language-tag (first argument) matches language-range (second argument)
   * per the basic filtering scheme defined in [RFC4647] section 3.3.1. language-range is a basic language
   * range per Matching of Language Tags [RFC4647] section 2.1. A language-range of "*" matches any non-empty
   * language-tag string."
   *
   * @see http://www.w3.org/TR/rdf-sparql-query
   */
  private void convertLangMatches(E_LangMatches expr) {
    logger.debug("convertLangMatches " + expr.toString());

    expr.getArg1().visit(this);
    expr.getArg2().visit(this);
    Expression e2 = expression.pop();
    Expression e1 = expression.pop();

    if (e1 instanceof ConstantEx && e2 instanceof ConstantEx) {
      ConstantEx lang1 = (ConstantEx) e1;
      ConstantEx lang2 = (ConstantEx) e2;
      NodeValue nv1 = NodeValue.makeString(lang1.getNode().getLiteral().getLexicalForm());
      NodeValue nv2 = NodeValue.makeString(lang2.getNode().getLiteral().getLexicalForm());
      NodeValue match = NodeFunctions.langMatches(nv1, nv2);
      expression.push(match.equals(NodeValue.TRUE) ? Expression.TRUE : Expression.FALSE);
    } else {
      expression.push(Expression.FALSE);
    }
  }
  /*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.3
   *
   * "(ISBLANK) Returns true if term is a blank node. Returns false otherwise."
   *
   * @see http://www.w3.org/TR/rdf-sparql-query
   */
  private void convertIsBlank(E_IsBlank expr) {
    logger.debug("convertIsBlank " + expr.toString());

    expr.getArg().visit(this);

    Expression arg = expression.pop();

    if (arg instanceof AttributeExprEx) {
      AttributeExprEx variable = (AttributeExprEx) arg;
      NodeMaker nm = variable.getNodeMaker();
      DetermineNodeType filter = new DetermineNodeType();
      nm.describeSelf(filter);
      expression.push(filter.isLimittedToBlankNodes() ? Expression.TRUE : Expression.FALSE);
    } else if (arg instanceof ConstantEx) {
      ConstantEx constant = (ConstantEx) arg;
      Node node = constant.getNode();
      expression.push(node.isBlank() ? Expression.TRUE : Expression.FALSE);
    } else {
      conversionFailed(expr);
    }
  }
  /*
   * See http://www.w3.org/TR/rdf-sparql-query paragraph 11.4.11
   *
   * "(SAMETERM) Returns TRUE if term1 and term2 are the same RDF term as defined
   * in Resource Description Framework (RDF): Concepts and Abstract Syntax [CONCEPTS]; returns FALSE otherwise."
   *
   * @see http://www.w3.org/TR/rdf-sparql-query
   * @see http://www.w3.org/TR/rdf-concepts/
   */
  private void convertSameTerm(E_SameTerm expr) {
    logger.debug("convertSameTerm " + expr.toString());

    expr.getArg1().visit(this);
    expr.getArg2().visit(this);
    Expression e2 = expression.pop();
    Expression e1 = expression.pop();

    // TODO Expression.FALSE and Expression.TRUE are not constants
    if (e1.equals(Expression.FALSE)) e1 = CONSTANT_FALSE;
    else if (e1.equals(Expression.TRUE)) e1 = CONSTANT_TRUE;
    if (e2.equals(Expression.FALSE)) e2 = CONSTANT_FALSE;
    else if (e2.equals(Expression.TRUE)) e2 = CONSTANT_TRUE;

    if (e1 instanceof AttributeExprEx && e2 instanceof Constant
        || e2 instanceof AttributeExprEx && e1 instanceof Constant) {

      AttributeExprEx variable;
      ConstantEx constant;

      if (e1 instanceof AttributeExprEx) {
        variable = (AttributeExprEx) e1;
        constant = (ConstantEx) e2;
      } else {
        variable = (AttributeExprEx) e2;
        constant = (ConstantEx) e1;
      }

      logger.debug("isEqual(" + variable + ", " + constant + ")");

      NodeMaker nm = variable.getNodeMaker();

      if (nm instanceof TypedNodeMaker) {
        ValueMaker vm = ((TypedNodeMaker) nm).valueMaker();
        Node node = constant.getNode();
        logger.debug("checking " + node + " with " + nm);

        boolean empty = nm.selectNode(node, RelationalOperators.DUMMY).equals(NodeMaker.EMPTY);
        logger.debug("result " + new Boolean(empty));
        if (!empty) {
          if (node.isURI()) expression.push(vm.valueExpression(node.getURI()));
          else if (node.isLiteral()) expression.push(vm.valueExpression(constant.value()));
          else conversionFailed(expr); // TODO blank nodes?
          return;
        } else {
          expression.push(Expression.FALSE);
          return;
        }
      } else {
        logger.warn("nm is not a TypedNodemaker");
      }
    } else if (e1 instanceof ConstantEx && e2 instanceof ConstantEx) {
      logger.debug("isEqual(" + e1 + ", " + e2 + ")");
      ConstantEx constant1 = (ConstantEx) e1;
      ConstantEx constant2 = (ConstantEx) e2;
      boolean equals = NodeFunctions.sameTerm(constant1.getNode(), constant2.getNode());
      logger.debug("constants same? " + new Boolean(equals));
      expression.push(equals ? Expression.TRUE : Expression.FALSE);
      return;
    } else if (e1 instanceof AttributeExprEx && e2 instanceof AttributeExprEx) {
      logger.debug("isEqual(" + e1 + ", " + e2 + ")");
      AttributeExprEx variable1 = (AttributeExprEx) e1;
      AttributeExprEx variable2 = (AttributeExprEx) e2;

      NodeMaker nm1 = variable1.getNodeMaker();
      NodeMaker nm2 = variable2.getNodeMaker();

      NodeSetConstraintBuilder nodeSet = new NodeSetConstraintBuilder();
      nm1.describeSelf(nodeSet);
      nm2.describeSelf(nodeSet);

      if (nodeSet.isEmpty()) {
        logger.debug("nodes " + nm1 + " " + nm2 + " incompatible");
        expression.push(Expression.FALSE);
        return;
      }
    }

    expression.push(Equality.create(e1, e2));
  }
  private void convertNotEquals(E_NotEquals expr) {
    logger.debug("convertNotEquals " + expr.toString());

    expr.getArg1().visit(this);
    expr.getArg2().visit(this);
    Expression e2 = expression.pop();
    Expression e1 = expression.pop();

    // TODO Expression.FALSE and Expression.TRUE are not constants
    if (e1.equals(Expression.FALSE)) e1 = CONSTANT_FALSE;
    else if (e1.equals(Expression.TRUE)) e1 = CONSTANT_TRUE;
    if (e2.equals(Expression.FALSE)) e2 = CONSTANT_FALSE;
    else if (e2.equals(Expression.TRUE)) e2 = CONSTANT_TRUE;

    if (e1 instanceof AttributeExprEx && e2 instanceof Constant
        || e2 instanceof AttributeExprEx && e1 instanceof Constant) {
      AttributeExprEx variable;
      ConstantEx constant;

      if (e1 instanceof AttributeExprEx) {
        variable = (AttributeExprEx) e1;
        constant = (ConstantEx) e2;
      } else {
        variable = (AttributeExprEx) e2;
        constant = (ConstantEx) e1;
      }

      logger.debug("isNotEqual(" + variable + ", " + constant + ")");

      NodeMaker nm = variable.getNodeMaker();

      if (nm instanceof TypedNodeMaker) {
        ValueMaker vm = ((TypedNodeMaker) nm).valueMaker();
        Node node = constant.getNode();
        logger.debug("checking " + node + " with " + nm);
        if (XSD.isNumeric(node)) {
          DetermineNodeType filter = new DetermineNodeType();
          nm.describeSelf(filter);
          RDFDatatype datatype = filter.getDatatype();
          if (datatype != null && XSD.isNumeric(datatype)) {
            RDFDatatype numericType = XSD.getNumericType(datatype, node.getLiteralDatatype());
            nm = cast(nm, numericType);
            node = XSD.cast(node, numericType);
          }
        }
        boolean empty = nm.selectNode(node, RelationalOperators.DUMMY).equals(NodeMaker.EMPTY);
        logger.debug("result " + new Boolean(empty));
        if (!empty) {
          if (node.isURI()) expression.push(new Negation(vm.valueExpression(node.getURI())));
          else if (node.isLiteral()) {
            if (XSD.isSupported(node.getLiteralDatatype()))
              expression.push(new Negation(vm.valueExpression(constant.value())));
            else // type = boolean or an unknown type
            conversionFailed("cannot compare values of type " + node.getLiteralDatatypeURI(), expr);
          } else conversionFailed(expr); // TODO blank nodes?
          return;
        } else {
          expression.push(Expression.TRUE);
          return;
        }
      }
    } else if (e1 instanceof ConstantEx && e2 instanceof ConstantEx) {
      logger.debug("isNotEqual(" + e1 + ", " + e2 + ")");
      Node c1 = ((ConstantEx) e1).getNode();
      Node c2 = ((ConstantEx) e2).getNode();
      boolean equals;
      if (XSD.isNumeric(c1) && XSD.isNumeric(c2)) {
        RDFDatatype datatype = XSD.getNumericType(c1.getLiteralDatatype(), c2.getLiteralDatatype());
        equals = XSD.cast(c1, datatype).equals(XSD.cast(c2, datatype));
      } else if (isSimpleLiteral(c1) && isSimpleLiteral(c2)) {
        equals = c1.getLiteralValue().equals(c2.getLiteralValue());
      } else if (XSD.isString(c1) && XSD.isString(c2)) {
        equals = c1.getLiteralValue().equals(c2.getLiteralValue());
      } else {
        try {
          equals = NodeFunctions.rdfTermEquals(c1, c2);
        } catch (ExprEvalException e) {
          equals = false;
        }
      }
      logger.debug("constants equal? " + new Boolean(equals));
      expression.push(equals ? Expression.FALSE : Expression.TRUE);
      return;
    } else if (e1 instanceof AttributeExprEx && e2 instanceof AttributeExprEx) {
      logger.debug("isNotEqual(" + e1 + ", " + e2 + ")");
      AttributeExprEx variable1 = (AttributeExprEx) e1;
      AttributeExprEx variable2 = (AttributeExprEx) e2;

      NodeMaker nm1 = variable1.getNodeMaker();
      NodeMaker nm2 = variable2.getNodeMaker();

      DetermineNodeType filter1 = new DetermineNodeType();
      nm1.describeSelf(filter1);
      RDFDatatype datatype1 = filter1.getDatatype();

      DetermineNodeType filter2 = new DetermineNodeType();
      nm2.describeSelf(filter2);
      RDFDatatype datatype2 = filter2.getDatatype();

      if (datatype1 != null
          && XSD.isNumeric(datatype1)
          && datatype2 != null
          && XSD.isNumeric(datatype2)) {
        RDFDatatype numericType = XSD.getNumericType(filter1.getDatatype(), filter2.getDatatype());
        nm1 = cast(nm1, numericType);
        nm2 = cast(nm2, numericType);
      }

      NodeSetConstraintBuilder nodeSet = new NodeSetConstraintBuilder();
      nm1.describeSelf(nodeSet);
      nm2.describeSelf(nodeSet);

      if (nodeSet.isEmpty()) {
        logger.debug("nodes " + nm1 + " " + nm2 + " incompatible");
        expression.push(Expression.TRUE);
        return;
      }
    }

    expression.push(new Negation(Equality.create(e1, e2)));
  }