// Copyright (c) 2009 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 "chrome/browser/thumbnail_store.h"

#include <string.h>
#include <algorithm>

#include "app/sql/statement.h"
#include "app/sql/transaction.h"
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/file_util.h"
#include "base/md5.h"
#include "base/string_util.h"
#include "base/thread.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/history/history_notifications.h"
#include "chrome/browser/pref_service.h"
#include "chrome/browser/profile.h"
#include "gfx/codec/jpeg_codec.h"
#include "googleurl/src/gurl.h"
#include "third_party/skia/include/core/SkBitmap.h"


ThumbnailStore::ThumbnailStore()
    : cache_(NULL),
      hs_(NULL),
      url_blacklist_(NULL),
      disk_data_loaded_(false) {
}

ThumbnailStore::~ThumbnailStore() {
  // Ensure that shutdown was called.
  DCHECK(hs_ == NULL);
}

void ThumbnailStore::Init(const FilePath& db_name, Profile* profile) {
  // Load thumbnails already in the database.
  g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
      NewRunnableMethod(this, &ThumbnailStore::InitializeFromDB,
                        db_name, MessageLoop::current()));

  // Take ownership of a reference to the HistoryService.
  hs_ = profile->GetHistoryService(Profile::EXPLICIT_ACCESS);

  // Store a pointer to a persistent table of blacklisted URLs.
  url_blacklist_ = profile->GetPrefs()->
    GetMutableDictionary(prefs::kNTPMostVisitedURLsBlacklist);
  DCHECK(url_blacklist_);

  // Get the list of most visited URLs and redirect information from the
  // HistoryService.
  most_visited_urls_.reset(new MostVisitedMap);
  timer_.Start(base::TimeDelta::FromSeconds(kUpdateIntervalSecs), this,
      &ThumbnailStore::UpdateURLData);
  UpdateURLData();

  // Register to get notified when the history is cleared.
  registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED,
                 Source<Profile>(profile));
}

bool ThumbnailStore::SetPageThumbnail(const GURL& url,
                                      const SkBitmap& thumbnail,
                                      const ThumbnailScore& score,
                                      bool fetch_redirects) {
  if (!cache_.get())
    return false;

  if (!ShouldStoreThumbnailForURL(url) ||
      (cache_->find(url) != cache_->end() &&
      !ShouldReplaceThumbnailWith((*cache_)[url].score_, score)))
    return true;

  base::TimeTicks encode_start = base::TimeTicks::Now();

  // Encode the SkBitmap to jpeg.
  scoped_refptr<RefCountedBytes> jpeg_data = new RefCountedBytes;
  SkAutoLockPixels thumbnail_lock(thumbnail);
  bool encoded = gfx::JPEGCodec::Encode(
      reinterpret_cast<unsigned char*>(thumbnail.getAddr32(0, 0)),
      gfx::JPEGCodec::FORMAT_BGRA, thumbnail.width(),
      thumbnail.height(),
      static_cast<int>(thumbnail.rowBytes()), 90,
      &jpeg_data->data);

  base::TimeDelta delta = base::TimeTicks::Now() - encode_start;
  HISTOGRAM_TIMES("Thumbnail.Encode", delta);

  if (!encoded)
    return false;

  // Update the cache_ with the new thumbnail.
  (*cache_)[url] = CacheEntry(jpeg_data, score, true);

  // Get redirects for this URL.
  if (fetch_redirects) {
    hs_->QueryRedirectsTo(url, &consumer_,
        NewCallback(this, &ThumbnailStore::OnRedirectsForURLAvailable));
  }

  return true;
}

bool ThumbnailStore::GetPageThumbnail(
    const GURL& url,
    RefCountedBytes** data) {
  if (!cache_.get() || IsURLBlacklisted(url))
    return false;

  // Look up the |url| in the redirect list to find the final destination
  // which is the key into the |cache_|.
  history::RedirectMap::iterator it = redirect_urls_->find(url);
  if (it != redirect_urls_->end()) {
    // Return the first available thumbnail starting at the end of the
    // redirect list.
    history::RedirectList::reverse_iterator rit;
    for (rit = it->second->data.rbegin();
        rit != it->second->data.rend(); ++rit) {
      if (cache_->find(*rit) != cache_->end()) {
        *data = (*cache_)[*rit].data_.get();
        (*data)->AddRef();
        return true;
      }
    }
  }

  // TODO(meelapshah) bug 14643: check past redirect lists

  if (cache_->find(url) == cache_->end())
    return false;

  *data = (*cache_)[url].data_.get();
  (*data)->AddRef();
  return true;
}

void ThumbnailStore::Shutdown() {
  // We must release our reference to the HistoryService here to prevent
  // shutdown issues. Please refer to the comment in HistoryService::Cleanup
  // for details.
  hs_ = NULL;

  // De-register for notifications.
  registrar_.RemoveAll();

  // Stop the timer to ensure that UpdateURLData is not called during shutdown.
  timer_.Stop();

  // Write the cache to disk.  This will schedule the disk operations to be run
  // on the file_thread.  Note that Join() does not need to be called with the
  // file_thread because when the disk operation is scheduled, it will hold a
  // reference to |this| keeping this object alive.
  CleanCacheData();
}

void ThumbnailStore::OnRedirectsForURLAvailable(
    HistoryService::Handle handle,
    GURL url,
    bool success,
    history::RedirectList* redirects) {
  if (!success)
    return;

  DCHECK(redirect_urls_.get());

  // If A -> B -> C is a redirect chain, then this function would be called
  // with url=C and redirects = {B, A}. This is entered into the RedirectMap as
  // A => {B -> C}.
  if (redirects->empty()) {
    (*redirect_urls_)[url] = new RefCountedVector<GURL>;
  } else {
    const GURL start_url = redirects->back();
    std::reverse(redirects->begin(), redirects->end() - 1);
    *(redirects->end() - 1) = url;
    (*redirect_urls_)[start_url] = new RefCountedVector<GURL>(*redirects);
  }
}

history::RedirectMap::iterator ThumbnailStore::GetRedirectIteratorForURL(
    const GURL& url) const {
  for (history::RedirectMap::iterator it = redirect_urls_->begin();
      it != redirect_urls_->end(); ++it) {
    if (it->first == url ||
        (!it->second->data.empty() && it->second->data.back() == url))
      return it;
  }
  return redirect_urls_->end();
}

void ThumbnailStore::Observe(NotificationType type,
    const NotificationSource& source,
    const NotificationDetails& details) {
  if (type.value != NotificationType::HISTORY_URLS_DELETED) {
    NOTREACHED();
    return;
  }

  Details<history::URLsDeletedDetails> url_details(details);
  // If all history was cleared, clear all of our data and reset the update
  // timer.
  if (url_details->all_history) {
    most_visited_urls_->clear();
    redirect_urls_->clear();
    cache_->clear();
    timer_.Reset();
  }
}

void ThumbnailStore::NotifyThumbnailStoreReady() {
  NotificationService::current()->Notify(
      NotificationType::THUMBNAIL_STORE_READY,
      Source<ThumbnailStore>(this),
      NotificationService::NoDetails());
}

void ThumbnailStore::UpdateURLData() {
  DCHECK(url_blacklist_);

  int result_count = ThumbnailStore::kMaxCacheSize + url_blacklist_->size();
  hs_->QueryTopURLsAndRedirects(result_count, &consumer_,
      NewCallback(this, &ThumbnailStore::OnURLDataAvailable));
}

void ThumbnailStore::OnURLDataAvailable(HistoryService::Handle handle,
                                        bool success,
                                        std::vector<GURL>* urls,
                                        history::RedirectMap* redirects) {
  if (!success)
    return;

  DCHECK(urls);
  DCHECK(redirects);

  // Each element of |urls| is the start of a redirect chain. When thumbnails
  // are stored from TabContents, the tails of the redirect chains are
  // associated with the image. Since SetPageThumbnail is called frequently, we
  // look up the tail end of each element in |urls| and insert that into the
  // MostVisitedMap. This way SetPageThumbnail can more quickly check if a
  // given url is in the most visited list.
  most_visited_urls_->clear();
  for (size_t i = 0; i < urls->size(); ++i) {
    history::RedirectMap::iterator it = redirects->find(urls->at(i));
    if (it->second->data.empty())
      (*most_visited_urls_)[urls->at(i)] = GURL();
    else
      (*most_visited_urls_)[it->second->data.back()] = urls->at(i);
  }
  redirect_urls_.reset(new history::RedirectMap(*redirects));

  if (IsReady())
    NotifyThumbnailStoreReady();

  CleanCacheData();
}

void ThumbnailStore::CleanCacheData() {
  if (!cache_.get())
    return;

  scoped_refptr<RefCountedVector<GURL> > urls_to_delete =
      new RefCountedVector<GURL>;
  Cache* data_to_save = new Cache;  // CommitCacheToDB will delete this

  // Iterate the cache, storing urls to be deleted and dirty cache entries to
  // be written to disk.
  for (Cache::iterator cache_it = cache_->begin();
       cache_it != cache_->end();) {
    history::RedirectMap::iterator redirect_it =
        GetRedirectIteratorForURL(cache_it->first);
    const GURL* url = redirect_it == redirect_urls_->end() ?
                          NULL : &redirect_it->first;

    // If this URL is blacklisted or not in the most visited list, mark it for
    // deletion. Otherwise, if the cache entry is dirty, mark it to be written
    // to disk.
    if (url == NULL || IsURLBlacklisted(*url) || !IsPopular(*url)) {
      urls_to_delete->data.push_back(cache_it->first);
      cache_->erase(cache_it++);
    } else {
      if (cache_it->second.dirty_) {
        data_to_save->insert(*cache_it);
        cache_it->second.dirty_ = false;
      }
      ++cache_it;
    }
  }

  g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE,
      NewRunnableMethod(this, &ThumbnailStore::CommitCacheToDB,
                        urls_to_delete, data_to_save));
}

void ThumbnailStore::CommitCacheToDB(
    scoped_refptr<RefCountedVector<GURL> > urls_to_delete,
    Cache* data) {
  scoped_ptr<Cache> data_to_save(data);
  if (!db_.is_open())
    return;

  base::TimeTicks db_start = base::TimeTicks::Now();

  sql::Transaction transaction(&db_);
  if (!transaction.Begin())
    return;

  // Delete old thumbnails.
  if (urls_to_delete.get()) {
    for (std::vector<GURL>::iterator it = urls_to_delete->data.begin();
        it != urls_to_delete->data.end(); ++it) {
      sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
          "DELETE FROM thumbnails WHERE url=?"));
      if (!statement)
        return;
      statement.BindString(0, it->spec());
      if (!statement.Run())
        NOTREACHED();
    }
  }

  // Update cached thumbnails.
  if (data_to_save.get()) {
    for (Cache::iterator it = data_to_save->begin();
         it != data_to_save->end(); ++it) {
      sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
          "INSERT OR REPLACE INTO thumbnails "
          "(url, boring_score, good_clipping, "
          "at_top, time_taken, data) "
          "VALUES (?,?,?,?,?,?)"));
      statement.BindString(0, it->first.spec());
      statement.BindDouble(1, it->second.score_.boring_score);
      statement.BindBool(2, it->second.score_.good_clipping);
      statement.BindBool(3, it->second.score_.at_top);
      statement.BindInt64(4,
          it->second.score_.time_at_snapshot.ToInternalValue());
      statement.BindBlob(5, &it->second.data_->data[0],
                          static_cast<int>(it->second.data_->data.size()));
      if (!statement.Run())
        DLOG(WARNING) << "Unable to insert thumbnail for URL";
    }
  }

  transaction.Commit();

  base::TimeDelta delta = base::TimeTicks::Now() - db_start;
  HISTOGRAM_TIMES("ThumbnailStore.WriteDBToDisk", delta);
}

void ThumbnailStore::InitializeFromDB(const FilePath& db_name,
                                      MessageLoop* cb_loop) {
  db_.set_page_size(4096);
  db_.set_cache_size(64);
  db_.set_exclusive_locking();
  if (!db_.Open(db_name))
    return;

  if (!db_.DoesTableExist("thumbnails")) {
    if (!db_.Execute("CREATE TABLE thumbnails ("
          "url LONGVARCHAR PRIMARY KEY,"
          "boring_score DOUBLE DEFAULT 1.0,"
          "good_clipping INTEGER DEFAULT 0,"
          "at_top INTEGER DEFAULT 0,"
          "time_taken INTEGER DEFAULT 0,"
          "data BLOB)"))
      return;
  }

  if (cb_loop)
    GetAllThumbnailsFromDisk(cb_loop);
}

void ThumbnailStore::GetAllThumbnailsFromDisk(MessageLoop* cb_loop) {
  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
      "SELECT * FROM thumbnails"));
  if (!statement)
    return;

  Cache* cache = new Cache;
  while (statement.Step()) {
    // The URL
    GURL url(statement.ColumnString(0));

    // The score.
    ThumbnailScore score(statement.ColumnDouble(1),      // Boring score
                         statement.ColumnBool(2),        // Good clipping
                         statement.ColumnBool(3),        // At top
                         base::Time::FromInternalValue(
                            statement.ColumnInt64(4)));  // Time taken

    // The image.
    scoped_refptr<RefCountedBytes> data = new RefCountedBytes;
    statement.ColumnBlobAsVector(5, &data->data);
    (*cache)[url] = CacheEntry(data, score, false);
  }

  cb_loop->PostTask(FROM_HERE,
      NewRunnableMethod(this, &ThumbnailStore::OnDiskDataAvailable, cache));
}

void ThumbnailStore::OnDiskDataAvailable(Cache* cache) {
  if (cache)
    cache_.reset(cache);

  disk_data_loaded_ = true;
  if (IsReady())
    NotifyThumbnailStoreReady();
}

bool ThumbnailStore::ShouldStoreThumbnailForURL(const GURL& url) const {
  if (!cache_.get())
    return false;

  if (IsURLBlacklisted(url) || cache_->size() >= kMaxCacheSize)
    return false;

  return IsPopular(url);
}

bool ThumbnailStore::IsURLBlacklisted(const GURL& url) const {
  if (url_blacklist_)
    return url_blacklist_->HasKey(GetDictionaryKeyForURL(url.spec()));
  return false;
}

std::wstring ThumbnailStore::GetDictionaryKeyForURL(
    const std::string& url) const {
  return ASCIIToWide(MD5String(url));
}

bool ThumbnailStore::IsPopular(const GURL& url) const {
  return most_visited_urls_->size() < kMaxCacheSize ||
         most_visited_urls_->find(url) != most_visited_urls_->end();
}