package edu.stanford.nlp.sempre.roboy;

import java.util.*;
import com.google.common.collect.Lists;
import edu.stanford.nlp.sempre.roboy.config.ConfigManager;
import fig.basic.*;
import edu.stanford.nlp.sempre.*;

/**
 * Utilities for constructing SPARQL expressions.
 * Note that we represent a subset of the SPARQL.
 * @author Percy Liang
 */
public interface SparqlExpr {
}

// Example: { <expr> ... <expr> }
class SparqlBlock implements SparqlExpr {
  public final List<SparqlExpr> children = Lists.newArrayList();

  public SparqlBlock add(SparqlExpr expr) {
    if (expr instanceof SparqlBlock)
      this.children.addAll(((SparqlBlock) expr).children);
    else
      this.children.add(expr);
    return this;
  }

  private String getId(PrimitiveFormula formula) {
    if (!(formula instanceof ValueFormula)) return null;
    Value value = ((ValueFormula) formula).value;
    if (!(value instanceof NameValue)) return null;
    return ((NameValue) value).id;
  }

  private boolean isPrimitiveType(String id) {
    for (String key : CanonicalNames.PRIMITIVES) {
      if (key.equals(id)) {
        return true;
      }
    }
    return false;
  }

  void addStatement(PrimitiveFormula arg1, String property, PrimitiveFormula arg2, boolean optional) {
    // Check multiple prefixes
    boolean correct_prefix = false;
    Map<String, String> glossary = new HashMap();
    try {
        glossary = ConfigManager.DB_GLOSSARY;
    }
    catch (Exception e) {
        throw new RuntimeException(e);
    }
    try{
      for (String key : glossary.keySet()) {
        if (property.contains(key)) {//if (property.startsWith(URLEncoder.encode(key, "UTF-8"))) {
          correct_prefix = true;
        }
      }
    } catch(Exception e) {
      LogInfo.errors("Exception: %s", e);
    }

    if (!correct_prefix &&!SparqlStatement.isOperator(property) && !SparqlStatement.isSpecialFunction(property) && !SparqlStatement.isIndependent(property))
      throw new RuntimeException("Invalid SPARQL property: " + property);
    // Ignore statements like:
    //   ?x fb:type.object.type fb:type.datetime
    // because we should have already captured the semantics using other formulas that involve ?x.
    if (property.equals(DatabaseInfo.getType(property,"TYPE"))) {
      String id = getId(arg2);
      if (isPrimitiveType(id) || DatabaseInfo.getType(property,"ANY").equals(id)) return;
    }
    if (SparqlStatement.isIndependent(property)) return;  // Nothing connecting arg1 and arg2
    add(new SparqlStatement(arg1, property, arg2, optional));
  }

  @Override public String toString() {
    List<String> strings = Lists.newArrayList();
    for (SparqlExpr expr : children) {
      if (expr instanceof SparqlSelect)
        strings.add("{ " + expr + " }");
      else
        strings.add(expr.toString());
    }
    return "{ " + StrUtils.join(strings, " . ") + " }";
  }
}

// Example: SELECT ?x ?y WHERE { ... } ORDER BY ?x ?y LIMIT 10 OFFSET 3
class SparqlSelect implements SparqlExpr {
  static class Var {
    public final VariableFormula var;
    public final String asValue;  // for COUNT(?x3) as ?x2
    public final String unit;  // Specifies the types of the variable (used to parse back the results)
    public final boolean isAuxiliary;  // Whether this is supporting information (e.g., names)
    public final String description;  // Human-friendly for display

    public Var(VariableFormula var, String asValue, String unit, boolean isAuxiliary, String description) {
      this.var = var;
      this.asValue = asValue;
      this.unit = unit;
      this.isAuxiliary = isAuxiliary;
      this.description = description;
    }

    @Override public String toString() {
      if (asValue == null) return var.name;
      return "(" + asValue + " AS " + var.name + ")";
    }
  }

  public final List<Var> selectVars = Lists.newArrayList();

  public SparqlBlock where;
  public final List<VariableFormula> sortVars = Lists.newArrayList();
  public int offset = 0;  // Start at this point when returning results
  public int limit = -1;  // Number of results to return

  @Override public String toString() {
    StringBuilder out = new StringBuilder();
    out.append("SELECT DISTINCT");
    for (Var var : selectVars)
      out.append(" " + var.toString());
    out.append(" WHERE " + where);
    if (sortVars.size() > 0) {
      out.append(" ORDER BY");
      for (PrimitiveFormula sortVar : sortVars)
        out.append(" " + SparqlUtils.plainStr(sortVar));
    }
    if (limit != -1) out.append(" LIMIT " + limit);
    if (offset != 0) out.append(" OFFSET " + offset);
    return out.toString();
  }
}

// Example: { ... } UNION { ... } UNION { ... }
class SparqlUnion implements SparqlExpr {
  public final List<SparqlBlock> children = Lists.newArrayList();
  public SparqlUnion add(SparqlBlock block) { this.children.add(block); return this; }
  @Override public String toString() {
    return "{ " + StrUtils.join(children, " UNION ") + " }";
  }
}

// Example: FILTER NOT EXISTS { ... }
class SparqlNot implements SparqlExpr {
  public final SparqlBlock block;
  public SparqlNot(SparqlBlock block) { this.block = block; }
  @Override public String toString() {
    return "FILTER NOT EXISTS " + block;
  }
}

class SparqlStatement implements SparqlExpr {
  public final PrimitiveFormula arg1;
  public final String relation;
  public final PrimitiveFormula arg2;
  public boolean optional;
  public String options;

  public SparqlStatement(PrimitiveFormula arg1, String relation, PrimitiveFormula arg2, boolean optional) {
    this.arg1 = arg1;
    this.relation = relation;
    this.arg2 = arg2;
    this.optional = optional;
    this.options = null;
  }

  public SparqlStatement(PrimitiveFormula arg1, String relation, PrimitiveFormula arg2, boolean optional, String options) {
    this(arg1, relation, arg2, optional);
    this.options = options;
  }

  public static boolean isIndependent(String relation) { return relation.equals(":"); }

  public static boolean isOperator(String relation) {
    return relation.equals("=") || relation.equals("!=") ||
        relation.equals("<") || relation.equals(">") ||
        relation.equals("<=") || relation.equals(">=");
  }

  public static boolean isSpecialFunction(String relation) {
    return relation.equals("STRSTARTS") || relation.equals("STRENDS");
  }

  public String simpleString() {
    // Workaround for annoying dates:
    // http://answers.semanticweb.com/questions/947/dbpedia-sparql-endpoint-xsddate-comparison-weirdness
    if (arg2 instanceof ValueFormula && ((ValueFormula) arg2).value instanceof DateValue) {
      if (isOperator(relation)) {
        if (relation.equals("=")) {
          // (= (date 2000 -1 -1)) really means (>= (2000 -1 -1)) and (< (2001 -1 -1))
          DateValue startDate = (DateValue) ((ValueFormula) arg2).value;
          DateValue endDate = advance(startDate);
          return SparqlUtils.dateTimeStr(arg1) + " >= " + SparqlUtils.dateTimeStr(new ValueFormula<DateValue>(startDate)) + ") . FILTER (" +
                 SparqlUtils.dateTimeStr(arg1) + " < " + SparqlUtils.dateTimeStr(new ValueFormula<DateValue>(endDate));
        }
        if (relation.equals("<=")) {
          // (<= (date 2000 -1 -1)) really means (< (date 2001 -1 -1))
          DateValue startDate = (DateValue) ((ValueFormula) arg2).value;
          DateValue endDate = advance(startDate);
          return SparqlUtils.dateTimeStr(arg1) + " < " + SparqlUtils.dateTimeStr(new ValueFormula<DateValue>(endDate));
        }
        if (relation.equals(">")) {
          // (> (date 2000 -1 -1)) really means >= (date 2001 -1 -1)
          DateValue startDate = (DateValue) ((ValueFormula) arg2).value;
          DateValue endDate = advance(startDate);
          return SparqlUtils.dateTimeStr(arg1) + " >= " + SparqlUtils.dateTimeStr(new ValueFormula<DateValue>(endDate));
        }
        if (relation.equals("<") || relation.equals(">="))
          return SparqlUtils.dateTimeStr(arg1) + " " + relation + " " + SparqlUtils.dateTimeStr(arg2);
        // Note: != is not treated specially
      }
    }
    if (relation.contains("rdfs:comment"))
        return SparqlUtils.plainStr(arg1) + " " + relation + " " + SparqlUtils.plainStr(arg2) + ". FILTER (lang(" + SparqlUtils.plainStr(arg2) + ") = 'en') ";
    return SparqlUtils.plainStr(arg1) + " " + relation + " " + SparqlUtils.plainStr(arg2);
  }

  private DateValue advance(DateValue date) {
    // TODO(pliang): deal with carrying over
    if (date.day != -1) return new DateValue(date.year, date.month, date.day + 1);
    if (date.month != -1) return new DateValue(date.year, date.month + 1, -1);
    return new DateValue(date.year + 1, -1, -1);
  }

  public String toString() {
    String result;
    if (isSpecialFunction(relation)) {  // Special functions
      result = "FILTER (" + relation + "(" + SparqlUtils.plainStr(arg1) + "," + SparqlUtils.plainStr(arg2) + "))";
    } else if (isOperator(relation)) {
      result = "FILTER (" + simpleString() + ")";
    } else if (optional) {
      result = "OPTIONAL { " + simpleString() + " }";
    } else {
      result = simpleString();
    }

    if (this.options != null)
      result += " " + this.options;

    return result;
  }
}

final class SparqlUtils {
  private SparqlUtils() { }

  public static String dateTimeStr(PrimitiveFormula formula) {
    return "xsd:datetime(" + plainStr(formula) + ")";
  }

  public static String plainStr(PrimitiveFormula formula) {
    if (formula instanceof VariableFormula) return ((VariableFormula) formula).name;

    Value value = ((ValueFormula) formula).value;

    if (value instanceof StringValue) {
      String s = ((StringValue) value).value;
      return "\"" + s.replaceAll("\"", "\\\\\"") + "\"" + (s.equals("en") ? "" : "@en");
    }
    if (value instanceof NameValue) return ((NameValue) value).id;
    if (value instanceof NumberValue) return ((NumberValue) value).value + "";
    if (value instanceof DateValue) {
      DateValue date = (DateValue) value;
      if (date.month == -1) return "\"" + date.year + "\"" + "^^xsd:datetime";
      if (date.day == -1) return "\"" + date.year + "-" + date.month + "\"" + "^^xsd:datetime";
      return "\"" + date.year + "-" + date.month + "-" + date.day + "\"" + "^^xsd:datetime";
    }
    throw new RuntimeException("Unhandled primitive: " + value);
  }
}

