/**
 * This offers the locally stored recordings (files) to the network
 */
package org.bucksch.zeipis.store;
import org.bucksch.httpserver.*;
import org.bucksch.zeipis.all.*;
import org.bucksch.zeipis.core.CoreServer;
import java.io.*;
import java.net.SocketException;
import java.sql.*;
import java.util.Map;

public class RecordingsListingProvider implements HTTPHandler
{
  public RecordingsListingProvider()
  {
    CoreServer.me.dispatchingServer().registerHandler(this, "recorded/list");
    populateFields();
  }
  public void handle(HTTPRequest request)
    throws HTTPException, IOException
  {
    request.getResponseHeaders().put("Content-Type", "text/xml");
    request.getResponseHeaders().put("Content-Encoding", "UTF-8");
    SimpleRFCParams params = new SimpleRFCParams().parseFromURL(request.getURL());
    boolean withDescription = "true".equals(params.param("details"));
    
    /* Define and parse our own expression syntax, because
     * a) we can't trust the client, so we can't just accept an SQL query and pass it to the DB
     * b) of the URL syntax
     */
    String where = "done = TRUE AND deleted = FALSE AND ";
    try {
      where += parseFilter(params.param("filter"));
    } catch (Exception e) { throw new HTTPException(400, "Could not parse filter parameter", e); }
    try {
      String groupBy = params.param("onlyOnePer");
      if (groupBy != null)
      {
        if ( ! Field.exists(groupBy))
          throw new Exception("'only one per' field not recognized: " + groupBy);
        where += " GROUP BY " + Field.getDBName(groupBy);
      }

      String sortBy = params.param("sortBy");
      if (sortBy != null)
      {
        if ( ! Field.exists(sortBy))
          throw new Exception("Sort not recognized: " + sortBy);
        where += " ORDER BY " + Field.getDBName(sortBy);
        if ("desc".equals(params.param("sortDir")))
          where += " DESC";
      }
    } catch (Exception e) { throw new HTTPException(400, "Could not parse parameter", e); }
    try {
      Util.debug("WHERE " + where);
      PreparedStatement findRecordings = DB.me.database().prepareStatement("SELECT * FROM recording LEFT JOIN film USING (filmid) WHERE " + where);
      ResultSet rs = findRecordings.executeQuery();
      request.sendResponseHeaders(200, false);
      Writer writer = new OutputStreamWriter(request.getResponseBody());
      writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
      writer.write("<recordings>\n");
      while (rs.next())
      {
        int recordingid = rs.getInt("recordid");
        StoredRecording recording;
        try {
          recording = new StoredRecording(recordingid);
          //Util.debug("found " + recording);
        } catch (Exception e) { Util.error("Could not get recording " + recordingid, e); continue; }
        writer.write(recordingXML(recording, withDescription));
      }
      writer.write("</recordings>\n");
      writer.close();
    }
    catch (SocketException e) { Util.error("Client disconnected prematurely", e); }
    catch (Exception e) { throw new HTTPException(500, "Getting list of recordings failed", e); }
  }
  protected String esc(String text)
  {
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
  }
  protected String escAttr(String text)
  {
    return esc(text).replace("\"", "&quot;");
  }
  
  /**
   * @param filterStr
   * @return SQL WHERE String
   */
  public String parseFilter(String filterStr)
    throws Exception
  {
    if (filterStr == null || filterStr.isEmpty())
      return "1=1";
    String where = "(";

    boolean firstOr = true;
    for (String orClause : filterStr.split("-or-"))
    {
      if ( ! firstOr)
        where += " OR ";
      firstOr = false;

      boolean firstAnd = true;
      for (String andClause : orClause.split("-and-"))
      {
        if ( ! firstAnd)
          where += " AND ";
        firstAnd = false;

        String[] expr = andClause.split("-", 3); // with limit, split() does insert empty trailing components (empty operator)
        if (expr.length < 3)
          throw new Exception("Operator in expression not found: " + andClause);

        // Subject
        String subject = expr[0];
        if ( ! Field.exists(subject))
          throw new Exception("Expression subject not recognized: " + subject);
        String subjectDB = Field.getDBName(subject);
        int type = Field.getType(subject);

        // Operator
        String operator = expr[1];
        String operatorDB;
        boolean needObject = true;
        if (operator.equals("eq"))
        {
          operatorDB = " = ";
        }
        else if (operator.equals("like"))
        {
          operatorDB = " LIKE ";
          if (type != 1)
            throw new Exception("Operator LIKE only supported for strings");
        }
        else if (operator.equals("lt"))
        {
          operatorDB = " < ";
          if (type != 2 && type != 4)
            throw new Exception("Operator lt only supported for integer and datetime");
        }
        else if (operator.equals("gt"))
        {
          operatorDB = " > ";
          if (type != 2 && type != 4)
            throw new Exception("Operator gt only supported for integer and datetime");
        }
        else if (operator.equals("future"))
        {
          operatorDB = " > NOW()";
          needObject = false;
          if (type != 4)
            throw new Exception("Operator 'future' only supported for date/time");
        }
        else
          throw new Exception("Expression operator not recognized: " + operator);

        // Object
        String object = expr[2]; // already URL decoded
        String objectDB;
        if ( ! needObject)
          objectDB = "";
        else if (type == 1 || type == 4)
          objectDB = "\"" + object.replace("\"", "\\\"") + "\"";
        else if (type == 2)
          objectDB = Integer.toString(Integer.parseInt(object));
        else if (type == 3)
        {
          if (object.equals("true") || object.equals("false"))
            objectDB = object;
          else
            throw new Exception("Boolean object expected for " + subject + ", but got: " + object);
        }
        else
          throw new Exception("Unexpected object type for " + subject);

        where += subjectDB + operatorDB + objectDB;
      }
    }
    where += ")";
    return where;
  }

  public String recordingXML(StoredRecording recording, boolean withDescription)
  {
    String xml = "";
    xml += "<recording done=\"true\" recordingid=\"" + recording.id() + "\">\n"; 
    try {
      xml += "  <title>" + esc(recording.title()) + "</title>\n";
      if (recording.subtitle() != null && recording.subtitle().length() > 0)
        xml += "  <subtitle>" + esc(recording.subtitle()) + "</subtitle>\n";
      if (recording.originalTitle() != null && recording.originalTitle().length() > 0)
        xml += "  <originaltitle>" + esc(recording.originalTitle()) + "</originaltitle>\n";
      if (recording.originalSubtitle() != null && recording.originalSubtitle().length() > 0)
        xml += "  <originalsubtitle>" + esc(recording.originalSubtitle()) + "</originalsubtitle>\n";
      if (withDescription && recording.description() != null && recording.description().length() > 0)
        xml += "  <description>" + esc(recording.description()) + "</description>\n";
      if (recording.genre() != null && recording.genre().length() > 0)
        xml += "  <genre>" + esc(recording.genre()) + "</genre>\n";
      if (recording.showType() != null && recording.showType().length() > 0)
        xml += "  <showtype name=\"" + escAttr(recording.showType()) + "\" />\n";
      if (recording.firstShownInYear() != 0)
        xml += "  <firstshown year=\"" + recording.firstShownInYear() + "\" />\n";
      if (recording.store() == null)
        throw new Exception("Need store");
      xml += "  <location storename=\"" + escAttr(recording.store().storeName()) + "\"\n";
      xml += "            url=\"" + escAttr(CoreServer.me.baseURLForHost(recording.store().host()).toString()) + "/recorded/video/" + recording.id() + "\" />\n";
      xml += "  <preview type=\"image\" sizetype=\"full\" mimetype=\"image/jpeg\" storename=\"" + escAttr(recording.store().storeName()) + "\"\n";
      xml += "            url=\"" + escAttr(CoreServer.me.baseURLForHost(recording.store().host()).toString()) + "/recorded/preview/image/full/" + recording.id() + "\" />\n";
      xml += "  <management seen=\"" + (recording.seen() ? "true" : "false") + "\" " +
                                  "keep=\"" + (recording.keep() ? "true" : "false") + "\"/>\n";
    } catch (Exception e) { Util.error("Could not output details of recording " + recording.id(), e); }
    xml += "</recording>\n";
    return xml;
  }

  protected void populateFields()
  {
    // Collection of Fields will be kept statically in Field
    new Field("title", null, 1);
    new Field("originaltitle", "original_title", 1);
    new Field("originalsubtitle", "original_subtitle", 1);
    new Field("showtype", null, 1);
    new Field("genre", null, 1);
    //new Field("starttime", null, 4);
    //new Field("endtime", null, 4);
    new Field("seen", null, 3);
    new Field("deleted", null, 3);
    new Field("firstshown", null, 2);
    new Field("qualityrating", "quality_rating", 2);
    new Field("orderrecorded", "recordid", 1);
    new Field("id", "recordid", 1);
  }
}

class Field
{
  /**
   * 
   * @param apiName The name in the protocol (URL parameter value)
   * @param DBNameIfDifferent  The column name in the SQL database
   *     If null, will be same as apiName
   * @param type enum: 1 = string, 2 = int, 3 = bool, 4 = datetime
   */
  public Field(String apiName, String DBNameIfDifferent, int type)
  {
    if (apiName == null || apiName.length() == 0)
      throw new RuntimeException("Need API name");
    //myAPIName = apiName;
    if (DBNameIfDifferent != null)
      myDBName = DBNameIfDifferent;
    else
      myDBName = apiName;
    myType = type;
    myFields.put(apiName, this);
  }

  //public String myAPIName;
  public String myDBName;
  public int myType;


  // Collection

  static boolean exists(String apiName) { return myFields.containsKey(apiName); }
  static String getDBName(String apiName) { return myFields.get(apiName).myDBName; }
  static int getType(String apiName) { return myFields.get(apiName).myType; }

  static protected Map<String, Field> myFields = Util.newMap(); // apiName -> Field
}
