/* Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.getstarted.util;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Transaction;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

// [START init]
public class DatastoreSessionFilter implements Filter {

  private static DatastoreService datastore;
  private static final DateTimeFormatter DTF = DateTimeFormat.forPattern("yyyyMMddHHmmssSSS");
  private static final String SESSION_KIND = "SessionVariable";

  @Override
  public void init(FilterConfig config) throws ServletException {
    // initialize local copy of datastore session variables

    datastore = DatastoreServiceFactory.getDatastoreService();
    // Delete all sessions unmodified for over two days
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    Query query = new Query(SESSION_KIND).setFilter(new FilterPredicate(
            "lastModified", FilterOperator.LESS_THAN_OR_EQUAL, dt.minusDays(2).toString(DTF)));
    Iterator<Entity> results = datastore.prepare(query).asIterator();
    while (results.hasNext()) {
      Entity stateEntity = results.next();
      datastore.delete(stateEntity.getKey());
    }
  }
  // [END init]

  @Override
  public void doFilter(ServletRequest servletReq, ServletResponse servletResp, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletReq;
    HttpServletResponse resp = (HttpServletResponse) servletResp;

    // Check if the session cookie is there, if not there, make a session cookie using a unique
    // identifier.
    String sessionId = getCookieValue(req, "bookshelfSessionId");
    if (sessionId.equals("")) {
      String sessionNum = new BigInteger(130, new SecureRandom()).toString(32);
      Cookie session = new Cookie("bookshelfSessionId", sessionNum);
      session.setPath("/");
      resp.addCookie(session);
    }

    Map<String,String> datastoreMap = loadSessionVariables(req);  // session variables for request

    chain.doFilter(servletReq, servletResp);  // Allow the servlet to process request and response

    HttpSession session = req.getSession();   // Create session map
    Map<String, String> sessionMap = new HashMap<>();
    Enumeration<String> attrNames = session.getAttributeNames();
    while (attrNames.hasMoreElements()) {
      String attrName = attrNames.nextElement();
      sessionMap.put(attrName, (String) session.getAttribute(attrName));
    }

    // Create a diff between the new session variables and the existing session variables
    // to minimize datastore access
    MapDifference<String, String> diff = Maps.difference(sessionMap, datastoreMap);
    Map<String, String> setMap = diff.entriesOnlyOnLeft();
    Map<String, String> deleteMap = diff.entriesOnlyOnRight();

    // Apply the diff
    setSessionVariables(sessionId, setMap);
    deleteSessionVariables(
        sessionId,
        FluentIterable.from(deleteMap.keySet()).toArray(String.class));
  }

  @SuppressWarnings({"unused", "JdkObsolete"})
  private String mapToString(Map<String, String> map) {
    StringBuffer names = new StringBuffer();
    for (String name : map.keySet()) {
      names.append(name + " ");
    }
    return names.toString();
  }

  @Override
  public void destroy() {
  }

  protected String getCookieValue(HttpServletRequest req, String cookieName) {
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
      for (Cookie cookie : cookies) {
        if (cookie.getName().equals(cookieName)) {
          return cookie.getValue();
        }
      }
    }
    return "";
  }

  // [START deleteSessionVariables]
  /**
   * Delete a value stored in the project's datastore.
   * @param sessionId Request from which the session is extracted.
   */
  protected void deleteSessionVariables(String sessionId, String... varNames) {
    if (sessionId.equals("")) {
      return;
    }
    Key key = KeyFactory.createKey(SESSION_KIND, sessionId);
    Transaction transaction = datastore.beginTransaction();
    try {
      Entity stateEntity = datastore.get(transaction, key);
      for (String varName : varNames) {
        stateEntity.removeProperty(varName);
      }
      datastore.put(transaction, stateEntity);
      transaction.commit();
    } catch (EntityNotFoundException e) {
      // Ignore - if there's no session, there's nothing to delete.
    } finally {
      if (transaction.isActive()) {
        transaction.rollback();
      }
    }
  }
  // [END deleteSessionVariables]

  protected void deleteSessionWithValue(String varName, String varValue) {
    Transaction transaction = datastore.beginTransaction();
    try {
      Query query = new Query(SESSION_KIND)
          .setFilter(new FilterPredicate(varName, FilterOperator.EQUAL, varValue));
      Iterator<Entity> results = datastore.prepare(transaction, query).asIterator();
      while (results.hasNext()) {
        Entity stateEntity = results.next();
        datastore.delete(transaction, stateEntity.getKey());
      }
      transaction.commit();
    } finally {
      if (transaction.isActive()) {
        transaction.rollback();
      }
    }
  }

  // [START setSessionVariables]
  /**
   * Stores the state value in each key-value pair in the project's datastore.
   * @param sessionId Request from which to extract session.
   * @param varName the name of the desired session variable
   * @param varValue the value of the desired session variable
   */
  protected void setSessionVariables(String sessionId, Map<String, String> setMap) {
    if (sessionId.equals("")) {
      return;
    }
    Key key = KeyFactory.createKey(SESSION_KIND, sessionId);
    Transaction transaction = datastore.beginTransaction();
    DateTime dt = DateTime.now(DateTimeZone.UTC);
    dt.toString(DTF);
    try {
      Entity stateEntity;
      try {
        stateEntity = datastore.get(transaction, key);
      } catch (EntityNotFoundException e) {
        stateEntity = new Entity(key);
      }
      for (String varName : setMap.keySet()) {
        stateEntity.setProperty(varName, setMap.get(varName));
      }
      stateEntity.setProperty("lastModified", dt.toString(DTF));
      datastore.put(transaction, stateEntity);
      transaction.commit();
    } finally {
      if (transaction.isActive()) {
        transaction.rollback();
      }
    }
  }
  // [END setSessionVariables]

  // [START loadSessionVariables]
  /**
   * Take an HttpServletRequest, and copy all of the current session variables over to it
   * @param req Request from which to extract session.
   * @return a map of strings containing all the session variables loaded or an empty map.
   */
  protected Map<String, String> loadSessionVariables(HttpServletRequest req)
      throws ServletException {
    Map<String, String> datastoreMap = new HashMap<>();
    String sessionId = getCookieValue(req, "bookshelfSessionId");
    if (sessionId.equals("")) {
      return datastoreMap;
    }
    Key key = KeyFactory.createKey(SESSION_KIND, sessionId);
    Transaction transaction = datastore.beginTransaction();
    try {
      Entity stateEntity = datastore.get(transaction, key);
      Map<String, Object> properties = stateEntity.getProperties();
      for (Map.Entry<String, Object> property : properties.entrySet()) {
        req.getSession().setAttribute(property.getKey(), property.getValue());
        datastoreMap.put(property.getKey(), (String)property.getValue());
      }
      transaction.commit();
    } catch (EntityNotFoundException e) {
      // Ignore - if there's no session, there's nothing to delete.
    } finally {
      if (transaction.isActive()) {
        transaction.rollback();
      }
    }
    return datastoreMap;
  }
  // [END loadSessionVariables]
}