// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/indexed_db/indexed_db_factory_impl.h"

#include <utility>
#include <vector>

#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "content/browser/indexed_db/indexed_db_backing_store.h"
#include "content/browser/indexed_db/indexed_db_context_impl.h"
#include "content/browser/indexed_db/indexed_db_database_error.h"
#include "content/browser/indexed_db/indexed_db_tracing.h"
#include "content/browser/indexed_db/indexed_db_transaction_coordinator.h"
#include "storage/common/database/database_identifier.h"
#include "third_party/WebKit/public/platform/WebIDBDatabaseException.h"
#include "third_party/leveldatabase/env_chromium.h"

using base::ASCIIToUTF16;

namespace content {

const int64 kBackingStoreGracePeriodMs = 2000;

IndexedDBFactoryImpl::IndexedDBFactoryImpl(IndexedDBContextImpl* context)
    : context_(context) {
}

IndexedDBFactoryImpl::~IndexedDBFactoryImpl() {
}

void IndexedDBFactoryImpl::RemoveDatabaseFromMaps(
    const IndexedDBDatabase::Identifier& identifier) {
  IndexedDBDatabaseMap::iterator it = database_map_.find(identifier);
  DCHECK(it != database_map_.end());
  IndexedDBDatabase* database = it->second;
  database_map_.erase(it);

  std::pair<OriginDBMap::iterator, OriginDBMap::iterator> range =
      origin_dbs_.equal_range(database->identifier().first);
  DCHECK(range.first != range.second);
  for (OriginDBMap::iterator it2 = range.first; it2 != range.second; ++it2) {
    if (it2->second == database) {
      origin_dbs_.erase(it2);
      break;
    }
  }
}

void IndexedDBFactoryImpl::ReleaseDatabase(
    const IndexedDBDatabase::Identifier& identifier,
    bool forcedClose) {
  DCHECK(!database_map_.find(identifier)->second->backing_store());

  RemoveDatabaseFromMaps(identifier);

  // No grace period on a forced-close, as the initiator is
  // assuming the backing store will be released once all
  // connections are closed.
  ReleaseBackingStore(identifier.first, forcedClose);
}

void IndexedDBFactoryImpl::ReleaseBackingStore(const GURL& origin_url,
                                               bool immediate) {
  if (immediate) {
    IndexedDBBackingStoreMap::iterator it =
        backing_stores_with_active_blobs_.find(origin_url);
    if (it != backing_stores_with_active_blobs_.end()) {
      it->second->active_blob_registry()->ForceShutdown();
      backing_stores_with_active_blobs_.erase(it);
    }
  }

  // Only close if this is the last reference.
  if (!HasLastBackingStoreReference(origin_url))
    return;

  // If this factory does hold the last reference to the backing store, it can
  // be closed - but unless requested to close it immediately, keep it around
  // for a short period so that a re-open is fast.
  if (immediate) {
    CloseBackingStore(origin_url);
    return;
  }

  // Start a timer to close the backing store, unless something else opens it
  // in the mean time.
  DCHECK(!backing_store_map_[origin_url]->close_timer()->IsRunning());
  backing_store_map_[origin_url]->close_timer()->Start(
      FROM_HERE,
      base::TimeDelta::FromMilliseconds(kBackingStoreGracePeriodMs),
      base::Bind(
          &IndexedDBFactoryImpl::MaybeCloseBackingStore, this, origin_url));
}

void IndexedDBFactoryImpl::MaybeCloseBackingStore(const GURL& origin_url) {
  // Another reference may have opened since the maybe-close was posted, so it
  // is necessary to check again.
  if (HasLastBackingStoreReference(origin_url))
    CloseBackingStore(origin_url);
}

void IndexedDBFactoryImpl::CloseBackingStore(const GURL& origin_url) {
  IndexedDBBackingStoreMap::iterator it = backing_store_map_.find(origin_url);
  DCHECK(it != backing_store_map_.end());
  // Stop the timer (if it's running) - this may happen if the timer was started
  // and then a forced close occurs.
  it->second->close_timer()->Stop();
  backing_store_map_.erase(it);
}

bool IndexedDBFactoryImpl::HasLastBackingStoreReference(
    const GURL& origin_url) const {
  IndexedDBBackingStore* ptr;
  {
    // Scope so that the implicit scoped_refptr<> is freed.
    IndexedDBBackingStoreMap::const_iterator it =
        backing_store_map_.find(origin_url);
    DCHECK(it != backing_store_map_.end());
    ptr = it->second.get();
  }
  return ptr->HasOneRef();
}

void IndexedDBFactoryImpl::ForceClose(const GURL& origin_url) {
  OriginDBs range = GetOpenDatabasesForOrigin(origin_url);

  while (range.first != range.second) {
    IndexedDBDatabase* db = range.first->second;
    ++range.first;
    db->ForceClose();
  }

  if (backing_store_map_.find(origin_url) != backing_store_map_.end())
    ReleaseBackingStore(origin_url, true /* immediate */);
}

void IndexedDBFactoryImpl::ContextDestroyed() {
  // Timers on backing stores hold a reference to this factory. When the
  // context (which nominally owns this factory) is destroyed during thread
  // termination the timers must be stopped so that this factory and the
  // stores can be disposed of.
  for (const auto& it : backing_store_map_)
    it.second->close_timer()->Stop();
  backing_store_map_.clear();
  backing_stores_with_active_blobs_.clear();
  context_ = NULL;
}

void IndexedDBFactoryImpl::ReportOutstandingBlobs(const GURL& origin_url,
                                                  bool blobs_outstanding) {
  if (!context_)
    return;
  if (blobs_outstanding) {
    DCHECK(!backing_stores_with_active_blobs_.count(origin_url));
    IndexedDBBackingStoreMap::iterator it = backing_store_map_.find(origin_url);
    if (it != backing_store_map_.end())
      backing_stores_with_active_blobs_.insert(*it);
    else
      DCHECK(false);
  } else {
    IndexedDBBackingStoreMap::iterator it =
        backing_stores_with_active_blobs_.find(origin_url);
    if (it != backing_stores_with_active_blobs_.end()) {
      backing_stores_with_active_blobs_.erase(it);
      ReleaseBackingStore(origin_url, false /* immediate */);
    }
  }
}

void IndexedDBFactoryImpl::GetDatabaseNames(
    scoped_refptr<IndexedDBCallbacks> callbacks,
    const GURL& origin_url,
    const base::FilePath& data_directory,
    net::URLRequestContext* request_context) {
  IDB_TRACE("IndexedDBFactoryImpl::GetDatabaseNames");
  // TODO(dgrogan): Plumb data_loss back to script eventually?
  blink::WebIDBDataLoss data_loss;
  std::string data_loss_message;
  bool disk_full;
  leveldb::Status s;
  // TODO(cmumford): Handle this error
  scoped_refptr<IndexedDBBackingStore> backing_store =
      OpenBackingStore(origin_url,
                       data_directory,
                       request_context,
                       &data_loss,
                       &data_loss_message,
                       &disk_full,
                       &s);
  if (!backing_store.get()) {
    callbacks->OnError(
        IndexedDBDatabaseError(blink::WebIDBDatabaseExceptionUnknownError,
                               "Internal error opening backing store for "
                               "indexedDB.webkitGetDatabaseNames."));
    return;
  }

  std::vector<base::string16> names = backing_store->GetDatabaseNames(&s);
  if (!s.ok()) {
    DLOG(ERROR) << "Internal error getting database names";
    IndexedDBDatabaseError error(blink::WebIDBDatabaseExceptionUnknownError,
                                 "Internal error opening backing store for "
                                 "indexedDB.webkitGetDatabaseNames.");
    callbacks->OnError(error);
    backing_store = NULL;
    if (s.IsCorruption())
      HandleBackingStoreCorruption(origin_url, error);
    return;
  }
  callbacks->OnSuccess(names);
  backing_store = NULL;
  ReleaseBackingStore(origin_url, false /* immediate */);
}

void IndexedDBFactoryImpl::DeleteDatabase(
    const base::string16& name,
    net::URLRequestContext* request_context,
    scoped_refptr<IndexedDBCallbacks> callbacks,
    const GURL& origin_url,
    const base::FilePath& data_directory) {
  IDB_TRACE("IndexedDBFactoryImpl::DeleteDatabase");
  IndexedDBDatabase::Identifier unique_identifier(origin_url, name);
  IndexedDBDatabaseMap::iterator it = database_map_.find(unique_identifier);
  if (it != database_map_.end()) {
    // If there are any connections to the database, directly delete the
    // database.
    it->second->DeleteDatabase(callbacks);
    return;
  }

  // TODO(dgrogan): Plumb data_loss back to script eventually?
  blink::WebIDBDataLoss data_loss;
  std::string data_loss_message;
  bool disk_full = false;
  leveldb::Status s;
  scoped_refptr<IndexedDBBackingStore> backing_store =
      OpenBackingStore(origin_url,
                       data_directory,
                       request_context,
                       &data_loss,
                       &data_loss_message,
                       &disk_full,
                       &s);
  if (!backing_store.get()) {
    IndexedDBDatabaseError error(blink::WebIDBDatabaseExceptionUnknownError,
                                 ASCIIToUTF16(
                                     "Internal error opening backing store "
                                     "for indexedDB.deleteDatabase."));
    callbacks->OnError(error);
    if (s.IsCorruption()) {
      HandleBackingStoreCorruption(origin_url, error);
    }
    return;
  }

  scoped_refptr<IndexedDBDatabase> database = IndexedDBDatabase::Create(
      name, backing_store.get(), this, unique_identifier, &s);
  if (!database.get()) {
    IndexedDBDatabaseError error(
        blink::WebIDBDatabaseExceptionUnknownError,
        ASCIIToUTF16(
            "Internal error creating database backend for "
            "indexedDB.deleteDatabase."));
    callbacks->OnError(error);
    if (leveldb_env::IsCorruption(s)) {
      backing_store = NULL;
      HandleBackingStoreCorruption(origin_url, error);
    }
    return;
  }

  database_map_[unique_identifier] = database.get();
  origin_dbs_.insert(std::make_pair(origin_url, database.get()));
  database->DeleteDatabase(callbacks);
  RemoveDatabaseFromMaps(unique_identifier);
  database = NULL;
  backing_store = NULL;
  ReleaseBackingStore(origin_url, false /* immediate */);
}

void IndexedDBFactoryImpl::DatabaseDeleted(
    const IndexedDBDatabase::Identifier& identifier) {
  // NULL after ContextDestroyed() called, and in some unit tests.
  if (!context_)
    return;
  context_->DatabaseDeleted(identifier.first);
}

void IndexedDBFactoryImpl::HandleBackingStoreFailure(const GURL& origin_url) {
  // NULL after ContextDestroyed() called, and in some unit tests.
  if (!context_)
    return;
  context_->ForceClose(origin_url,
                       IndexedDBContextImpl::FORCE_CLOSE_BACKING_STORE_FAILURE);
}

void IndexedDBFactoryImpl::HandleBackingStoreCorruption(
    const GURL& origin_url,
    const IndexedDBDatabaseError& error) {
  // Make a copy of origin_url as this is likely a reference to a member of a
  // backing store which this function will be deleting.
  GURL saved_origin_url(origin_url);
  DCHECK(context_);
  base::FilePath path_base = context_->data_path();
  IndexedDBBackingStore::RecordCorruptionInfo(
      path_base, saved_origin_url, base::UTF16ToUTF8(error.message()));
  HandleBackingStoreFailure(saved_origin_url);
  // Note: DestroyBackingStore only deletes LevelDB files, leaving all others,
  //       so our corruption info file will remain.
  leveldb::Status s =
      IndexedDBBackingStore::DestroyBackingStore(path_base, saved_origin_url);
  if (!s.ok())
    DLOG(ERROR) << "Unable to delete backing store: " << s.ToString();
}

bool IndexedDBFactoryImpl::IsDatabaseOpen(const GURL& origin_url,
                                          const base::string16& name) const {
  return !!database_map_.count(IndexedDBDatabase::Identifier(origin_url, name));
}

bool IndexedDBFactoryImpl::IsBackingStoreOpen(const GURL& origin_url) const {
  return backing_store_map_.find(origin_url) != backing_store_map_.end();
}

bool IndexedDBFactoryImpl::IsBackingStorePendingClose(
    const GURL& origin_url) const {
  IndexedDBBackingStoreMap::const_iterator it =
      backing_store_map_.find(origin_url);
  if (it == backing_store_map_.end())
    return false;
  return it->second->close_timer()->IsRunning();
}

scoped_refptr<IndexedDBBackingStore>
IndexedDBFactoryImpl::OpenBackingStoreHelper(
    const GURL& origin_url,
    const base::FilePath& data_directory,
    net::URLRequestContext* request_context,
    blink::WebIDBDataLoss* data_loss,
    std::string* data_loss_message,
    bool* disk_full,
    bool first_time,
    leveldb::Status* status) {
  return IndexedDBBackingStore::Open(this,
                                     origin_url,
                                     data_directory,
                                     request_context,
                                     data_loss,
                                     data_loss_message,
                                     disk_full,
                                     context_->TaskRunner(),
                                     first_time,
                                     status);
}

scoped_refptr<IndexedDBBackingStore> IndexedDBFactoryImpl::OpenBackingStore(
    const GURL& origin_url,
    const base::FilePath& data_directory,
    net::URLRequestContext* request_context,
    blink::WebIDBDataLoss* data_loss,
    std::string* data_loss_message,
    bool* disk_full,
    leveldb::Status* status) {
  const bool open_in_memory = data_directory.empty();

  IndexedDBBackingStoreMap::iterator it2 = backing_store_map_.find(origin_url);
  if (it2 != backing_store_map_.end()) {
    it2->second->close_timer()->Stop();
    return it2->second;
  }

  scoped_refptr<IndexedDBBackingStore> backing_store;
  bool first_time = false;
  if (open_in_memory) {
    backing_store = IndexedDBBackingStore::OpenInMemory(
        origin_url, context_->TaskRunner(), status);
  } else {
    first_time = !backends_opened_since_boot_.count(origin_url);

    backing_store = OpenBackingStoreHelper(origin_url,
                                           data_directory,
                                           request_context,
                                           data_loss,
                                           data_loss_message,
                                           disk_full,
                                           first_time,
                                           status);
  }

  if (backing_store.get()) {
    if (first_time)
      backends_opened_since_boot_.insert(origin_url);
    backing_store_map_[origin_url] = backing_store;
    // If an in-memory database, bind lifetime to this factory instance.
    if (open_in_memory)
      session_only_backing_stores_.insert(backing_store);

    // All backing stores associated with this factory should be of the same
    // type.
    DCHECK_NE(session_only_backing_stores_.empty(), open_in_memory);

    return backing_store;
  }

  return 0;
}

void IndexedDBFactoryImpl::Open(const base::string16& name,
                                const IndexedDBPendingConnection& connection,
                                net::URLRequestContext* request_context,
                                const GURL& origin_url,
                                const base::FilePath& data_directory) {
  IDB_TRACE("IndexedDBFactoryImpl::Open");
  scoped_refptr<IndexedDBDatabase> database;
  IndexedDBDatabase::Identifier unique_identifier(origin_url, name);
  IndexedDBDatabaseMap::iterator it = database_map_.find(unique_identifier);
  blink::WebIDBDataLoss data_loss =
      blink::WebIDBDataLossNone;
  std::string data_loss_message;
  bool disk_full = false;
  bool was_open = (it != database_map_.end());
  if (!was_open) {
    leveldb::Status s;
    scoped_refptr<IndexedDBBackingStore> backing_store =
        OpenBackingStore(origin_url,
                         data_directory,
                         request_context,
                         &data_loss,
                         &data_loss_message,
                         &disk_full,
                         &s);
    if (!backing_store.get()) {
      if (disk_full) {
        connection.callbacks->OnError(
            IndexedDBDatabaseError(blink::WebIDBDatabaseExceptionQuotaError,
                                   ASCIIToUTF16(
                                       "Encountered full disk while opening "
                                       "backing store for indexedDB.open.")));
        return;
      }
      IndexedDBDatabaseError error(blink::WebIDBDatabaseExceptionUnknownError,
                                   ASCIIToUTF16(
                                       "Internal error opening backing store"
                                       " for indexedDB.open."));
      connection.callbacks->OnError(error);
      if (s.IsCorruption()) {
        HandleBackingStoreCorruption(origin_url, error);
      }
      return;
    }

    database = IndexedDBDatabase::Create(
        name, backing_store.get(), this, unique_identifier, &s);
    if (!database.get()) {
      DLOG(ERROR) << "Unable to create the database";
      IndexedDBDatabaseError error(blink::WebIDBDatabaseExceptionUnknownError,
                                   ASCIIToUTF16(
                                       "Internal error creating "
                                       "database backend for "
                                       "indexedDB.open."));
      connection.callbacks->OnError(error);
      if (leveldb_env::IsCorruption(s)) {
        backing_store = NULL;  // Closes the LevelDB so that it can be deleted
        HandleBackingStoreCorruption(origin_url, error);
      }
      return;
    }
  } else {
    database = it->second;
  }

  if (data_loss != blink::WebIDBDataLossNone)
    connection.callbacks->OnDataLoss(data_loss, data_loss_message);

  database->OpenConnection(connection);

  if (!was_open && database->ConnectionCount() > 0) {
    database_map_[unique_identifier] = database.get();
    origin_dbs_.insert(std::make_pair(origin_url, database.get()));
  }
}

std::pair<IndexedDBFactoryImpl::OriginDBMapIterator,
          IndexedDBFactoryImpl::OriginDBMapIterator>
IndexedDBFactoryImpl::GetOpenDatabasesForOrigin(const GURL& origin_url) const {
  return origin_dbs_.equal_range(origin_url);
}

size_t IndexedDBFactoryImpl::GetConnectionCount(const GURL& origin_url) const {
  size_t count(0);

  OriginDBs range = GetOpenDatabasesForOrigin(origin_url);
  for (OriginDBMapIterator it = range.first; it != range.second; ++it)
    count += it->second->ConnectionCount();

  return count;
}

}  // namespace content