/*
 * unity-webapps-resource-db.c
 * Copyright (C) Canonical LTD 2011
 *
 * Author: Robert Carr <racarr@canonical.com>
 * 
 unity-webapps is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * unity-webapps is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.";
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <sqlite3.h>

#include "unity-webapps-dirs.h"
#include "unity-webapps-resource-db.h"

#include "../unity-webapps-debug.h"

static sqlite3 *resource_db = NULL;

#ifndef UNITY_WEBAPPS_TEST_BUILD
#define MAXIMUM_DIRECTORY_SIZE (1024*1024*10)
#else
#define MAXIMUM_DIRECTORY_SIZE (1024)
#endif

#define STALE_TIME (60*10)

#define CREATE_RESOURCES_TABLE_SQL "CREATE TABLE IF NOT EXISTS resources(id INTEGER PRIMARY KEY, nameHash TEXT, resourcePath TEXT, lru INTEGER, timestamp INTEGER);"

static goffset
get_resource_directory_size ()
{
  GFileEnumerator *children;
  GFile *resource_dir;
  GFileInfo *child;
  const gchar *resource_dir_path;
  GError *error;
  gsize ret;

  children = NULL;
  ret = 0;
  
  resource_dir_path = unity_webapps_dirs_get_resource_dir();
  resource_dir = g_file_new_for_path (resource_dir_path);
  
  g_assert (resource_dir);
  
  error = NULL;
  
  children = g_file_enumerate_children (resource_dir, "standard::name,standard::size", G_FILE_QUERY_INFO_NONE,
					NULL /* Cancellable */,
					&error);
  
  if (error != NULL)
    {
      g_critical ("Failed to enumerate resources: %s", error->message);
      
      g_error_free (error);
      
      goto out;
    }
  
  error = NULL;
  while ((child = g_file_enumerator_next_file (children, NULL /* Cancellable */, &error)))
    {
      if (error != NULL)
	{
	  g_critical ("Failed to enumerate resources: %s", error->message);
	  
	  g_error_free (error);
	  
	  goto out;
	}
      ret += g_file_info_get_size (child);
      g_object_unref (child);
    }
  
  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB total size computed: %lu", (unsigned long)ret);

 out:  
  g_object_unref (G_OBJECT (resource_dir));
  g_object_unref (G_OBJECT (children));
  
  return ret;

}

static void
create_resources_table ()
{
  int rc;
  gchar *error_message;
  
  error_message = NULL;
  
  rc = sqlite3_exec (resource_db, CREATE_RESOURCES_TABLE_SQL, NULL, 0, &error_message);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Error creating resources table %s \n", error_message);
      
      sqlite3_free (error_message);
      
      sqlite3_close (resource_db);
      
      resource_db = NULL;
    }
  
}


gboolean 
unity_webapps_resource_db_open ()
{
  const gchar *uw_dir;
  gchar *database_file;
  gboolean ret;
  int rc;
  
  ret = TRUE;
  
  uw_dir = unity_webapps_dirs_get_user_dir ();
  database_file = g_build_filename (uw_dir, "resources.db", NULL);

  UNITY_WEBAPPS_NOTE (RESOURCE, "Opening Resource DB (%s)", database_file);
  
  rc = sqlite3_open (database_file, &resource_db);
  
  if (rc != 0)
    {
      g_critical ("Failed to open resources database: %s", sqlite3_errmsg(resource_db));
      sqlite3_close (resource_db);
      
      ret = FALSE;
      goto out;
    }
  
  create_resources_table ();
  

 out:
  g_free (database_file);

  return ret;
}

#define LRU_LAST_STATEMENT_SQL_TEMPLATE "SELECT lru,nameHash FROM resources ORDER BY lru DESC;"

static sqlite3_stmt *
db_get_lru_prepare_statement ()
{
  sqlite3_stmt *last_statement;
  int rc;
  
  last_statement = NULL;
  
  rc = sqlite3_prepare_v2 (resource_db, LRU_LAST_STATEMENT_SQL_TEMPLATE,
			   -1, &last_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile last LRU SQL statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
 out:
  return last_statement;
}

static gchar *
db_get_lru ()
{
  sqlite3_stmt *lru_statement;
  gchar *lru;
  gint rc;
  
  lru = NULL;
  
  lru_statement = db_get_lru_prepare_statement ();
  
  if (lru_statement == NULL)
    {
      return NULL;
    }
  
  rc = sqlite3_step (lru_statement);
  
  if (rc == SQLITE_ROW)
    {
      lru = g_strdup ((const gchar *)sqlite3_column_text (lru_statement, 1));
    }
  
  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB found LRU entry: %s", lru);
  

  sqlite3_finalize (lru_statement);
  return lru;
}

#define DROP_LRU_STATEMENT_SQL_TEMPLATE "DELETE FROM resources WHERE nameHash='%s';"

static sqlite3_stmt *
db_drop_name_prepare_statement (const gchar *name_hash)
{
  sqlite3_stmt *drop_statement;
  gchar *drop_sql;
  gint rc;
  
  drop_statement = NULL;
  
  if (name_hash == NULL)
    {
      UNITY_WEBAPPS_NOTE (CONTEXT, "Trying to drop a name that doesn't exist from the resource database. Probably means it was mangled with.");
      return NULL;
    }
  
  drop_sql = g_strdup_printf (DROP_LRU_STATEMENT_SQL_TEMPLATE, name_hash);
  
  rc = sqlite3_prepare_v2 (resource_db, drop_sql,
			   -1, &drop_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile drop LRU sql statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
 out:
  g_free (drop_sql);
  return drop_statement;
}

static void
db_drop_name_remove_file (const gchar *name_hash)
{
  GFile *resource_file;
  const gchar *resource_dir;
  gchar *resource_path;
  GError *error;
  
  resource_dir = unity_webapps_dirs_get_resource_dir();
  
  resource_path = g_build_filename (resource_dir, name_hash, NULL);
  
  resource_file = g_file_new_for_path (resource_path);

  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB about to delete file: %s", resource_path);
  
  if (g_file_test (resource_path, G_FILE_TEST_EXISTS) == FALSE)
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB found file already deleted");

      return;
    }
  
  if (resource_file == NULL)
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB couldn't open file for deletion: %s", resource_path);
      g_free (resource_path);
    }
  
  
  error = NULL;
  g_file_delete (resource_file, NULL /* Cancellable */, &error);
  
  if (error != NULL)
    {
      g_critical ("Error deleting resource file: %s", error->message);
      g_error_free (error);

      goto out;
    }

 out:  
  g_free (resource_path);
}

static void
db_drop_name (const gchar *name_hash)
{
  sqlite3_stmt *drop_statement;
  gint rc;
  
  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB dropping name: %s", name_hash);
  
  drop_statement = db_drop_name_prepare_statement (name_hash);
  
  if (drop_statement == NULL)
    {
      return;
    }
  
  rc = sqlite3_step (drop_statement);
  
  if (rc != SQLITE_DONE)
    {
      UNITY_WEBAPPS_NOTE (CONTEXT, "Error executing Drop statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
  db_drop_name_remove_file (name_hash);
    
 out:
  sqlite3_finalize (drop_statement);
}

static void
db_drop_lru ()
{
  gchar *lru;
  
  lru = db_get_lru ();
  db_drop_name (lru);
  
  g_free (lru);
}

#define LRU_STATEMENT_SQL_TEMPLATE "SELECT lru FROM resources ORDER BY LRU DESC;"

static sqlite3_stmt *
db_get_next_lru_prepare_statement ()
{
  sqlite3_stmt *lru_statement;
  int rc;
  
  lru_statement = NULL;
  
  rc = sqlite3_prepare_v2 (resource_db, LRU_STATEMENT_SQL_TEMPLATE,
				      -1, &lru_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile next lru SQL statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
 out:
  return lru_statement;
}

static gint
db_get_next_lru ()
{
  sqlite3_stmt *lru_statement;
  gint max_lru, rc;
  
  lru_statement = db_get_next_lru_prepare_statement ();
  
  if (lru_statement == NULL)
    {
      return 0;
    }
  
  rc = sqlite3_step (lru_statement);
  
  max_lru = 0;
  
  if (rc == SQLITE_ROW)
    {
      max_lru = sqlite3_column_int (lru_statement, 0);
    }
  
  sqlite3_finalize (lru_statement);
  
  return max_lru + 1;
}

#define UPDATE_LRU_STATEMENT_SQL_TEMPLATE "UPDATE resources SET lru=%d WHERE nameHash='%s';"
static sqlite3_stmt *
db_update_lru_prepare_statement (const gchar *name_hash)
{
  sqlite3_stmt *update_statement;
  gchar *update_sql;
  gint next_lru, rc;

  
  update_statement = NULL;
  
  next_lru = db_get_next_lru ();
  
  update_sql = g_strdup_printf (UPDATE_LRU_STATEMENT_SQL_TEMPLATE, next_lru, name_hash);
  
  rc = sqlite3_prepare_v2 (resource_db, update_sql,
			   -1, &update_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile update LRU SQL statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }

 out:
  g_free (update_sql);
  return update_statement;
}

static void
db_update_lru (const gchar *name_hash)
{
  sqlite3_stmt *update_statement;
  gint rc;

  update_statement = db_update_lru_prepare_statement (name_hash);
  
  if (update_statement == NULL)
    {
      return;
    }
  
  rc = sqlite3_step (update_statement);
  
  if (rc != SQLITE_DONE)
    {
      UNITY_WEBAPPS_NOTE (CONTEXT, "Error updating LRU for resource: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
 out:
  sqlite3_finalize (update_statement);
}






#define LOOKUP_STATEMENT_SQL_TEMPLATE "SELECT resourcePath,timestamp FROM resources where nameHash='%s';"
static sqlite3_stmt *
db_lookup_prepare_statement (const gchar *name_hash)
{
  sqlite3_stmt *lookup_statement = NULL;
  gchar *lookup_sql;
  int rc;
  
  lookup_sql = g_strdup_printf (LOOKUP_STATEMENT_SQL_TEMPLATE, name_hash);
  
  rc = sqlite3_prepare_v2 (resource_db, lookup_sql, -1, &lookup_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile lookup SQL statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }
  
 out:
  g_free (lookup_sql);
  
  return lookup_statement;
}

static gboolean
timestamp_is_stale (glong timestamp)
{
  time_t current_time;
  
  current_time = time (NULL);
  
  if (current_time - timestamp > STALE_TIME)
    {
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

gchar *
unity_webapps_resource_db_lookup_path (const gchar *name_hash)
{
  sqlite3_stmt *lookup_statement;
  gchar *resource_path;
  int rc;
  
  resource_path = NULL;
  
  lookup_statement = db_lookup_prepare_statement (name_hash);
  
  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB looking up entry: %s", name_hash);
  
  if (lookup_statement == NULL)
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB couldn't find an entry for %s", name_hash);
      return resource_path;
    }
  
  rc = sqlite3_step (lookup_statement);
  
  if (rc == SQLITE_ROW)
    {
      glong timestamp;
      
      timestamp = sqlite3_column_int (lookup_statement, 1);
      
      if (timestamp_is_stale (timestamp))
	{
	  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB found entry for %s but timestamp (%ld) is stale", name_hash, timestamp);
	  db_drop_name (name_hash);
	}
      else
	{
	  resource_path = g_strdup ((const gchar *)sqlite3_column_text (lookup_statement, 0));
	  db_update_lru (name_hash);
	}
    }

  sqlite3_finalize (lookup_statement);

  // TODO: FIXME: Maybe should use GIO this is a path not a URI but should it really be? WHO KNOWS -racarr?
  if (resource_path && g_file_test (resource_path, G_FILE_TEST_EXISTS) == FALSE)
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Resource DB found entry, but file %s is deleted, purging it from the database",
			  resource_path);

      db_drop_name (name_hash);
      
      g_free (resource_path);
      
      return NULL;
    }

  return resource_path;
}

#define INSERT_STATEMENT_SQL_TEMPLATE "INSERT INTO resources (nameHash, resourcePath, timestamp) VALUES ('%s', '%s', %ld)"
static sqlite3_stmt *
db_insert_prepare_statement (const gchar *name_hash,
			     const gchar *resource_path)
{
  sqlite3_stmt *insert_statement;
  gchar *insert_sql;
  GTimeVal current_time;
  int rc;
  
  insert_statement = NULL;
  
  g_get_current_time (&current_time);
  
  insert_sql = g_strdup_printf (INSERT_STATEMENT_SQL_TEMPLATE, name_hash, resource_path, time (NULL));
  
  rc = sqlite3_prepare_v2 (resource_db, insert_sql, -1, &insert_statement, NULL);
  
  if (rc != SQLITE_OK)
    {
      g_critical ("Failed to compile insert SQL statement: %s", sqlite3_errmsg (resource_db));
      goto out;
    }

 out:
  g_free (insert_sql);
  
  return insert_statement;

}

static void
unity_webapps_resource_db_ensure_within_size ()
{
  goffset size;
  
  size = get_resource_directory_size ();
  
  if (size < MAXIMUM_DIRECTORY_SIZE)
    {
      return;
    }
  else
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Resource database too large, dropping LRU");
      db_drop_lru ();
    }
}

gboolean
unity_webapps_resource_db_store_entry (const gchar *name_hash,
				       const gchar *resource_path)
{
  sqlite3_stmt *insert_statement;
  gint rc;
  gboolean ret;
  
  ret = TRUE;
  
  unity_webapps_resource_db_ensure_within_size ();
  
  insert_statement = db_insert_prepare_statement (name_hash, resource_path);
  
  if (insert_statement == NULL)
    {
      return FALSE;
    }
  
  rc = sqlite3_step (insert_statement);
  
  if (rc != SQLITE_DONE)
    {
      UNITY_WEBAPPS_NOTE (RESOURCE, "Error storing entry in resource db: %s", sqlite3_errmsg (resource_db));
      ret = FALSE;
      goto out;
    }
  
  UNITY_WEBAPPS_NOTE (RESOURCE, "Resource database stored resource: %s at %s", name_hash, resource_path);
  
  db_update_lru (name_hash);
  
 out:
  sqlite3_finalize (insert_statement);
  return ret;
}
