// Copyright (c) 2011 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/favicon_helper.h"

#include "build/build_config.h"

#include <vector>

#include "base/callback.h"
#include "base/memory/ref_counted_memory.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/icon_messages.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/tab_contents/navigation_controller.h"
#include "content/browser/tab_contents/navigation_entry.h"
#include "content/browser/tab_contents/tab_contents_delegate.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "skia/ext/image_operations.h"
#include "ui/gfx/codec/png_codec.h"

namespace {

// Returns history::IconType the given icon_type corresponds to.
history::IconType ToHistoryIconType(FaviconURL::IconType icon_type) {
  switch (icon_type) {
    case FaviconURL::FAVICON:
      return history::FAVICON;
    case FaviconURL::TOUCH_ICON:
      return history::TOUCH_ICON;
    case FaviconURL::TOUCH_PRECOMPOSED_ICON:
      return history::TOUCH_PRECOMPOSED_ICON;
    case FaviconURL::INVALID_ICON:
      return history::INVALID_ICON;
  }
  NOTREACHED();
  // Shouldn't reach here, just make compiler happy.
  return history::INVALID_ICON;
}

bool DoUrlAndIconMatch(const FaviconURL& favicon_url,
                       const GURL& url,
                       history::IconType icon_type) {
  return favicon_url.icon_url == url &&
      favicon_url.icon_type == static_cast<FaviconURL::IconType>(icon_type);
}

}  // namespace

FaviconHelper::DownloadRequest::DownloadRequest()
    : callback(NULL),
      icon_type(history::INVALID_ICON) {
}

FaviconHelper::DownloadRequest::DownloadRequest(const GURL& url,
                                                const GURL& image_url,
                                                ImageDownloadCallback* callback,
                                                history::IconType icon_type)
    : url(url),
      image_url(image_url),
      callback(callback),
      icon_type(icon_type) {
}

FaviconHelper::FaviconHelper(TabContents* tab_contents, Type icon_type)
    : TabContentsObserver(tab_contents),
      got_favicon_from_history_(false),
      favicon_expired_(false),
      icon_types_(icon_type == FAVICON ? history::FAVICON :
          history::TOUCH_ICON | history::TOUCH_PRECOMPOSED_ICON),
      current_url_index_(0) {
}

FaviconHelper::~FaviconHelper() {
  SkBitmap empty_image;

  // Call pending download callbacks with error to allow caller to clean up.
  for (DownloadRequests::iterator i = download_requests_.begin();
       i != download_requests_.end(); ++i) {
    if (i->second.callback) {
      i->second.callback->Run(i->first, true, empty_image);
    }
  }
}

void FaviconHelper::FetchFavicon(const GURL& url) {
  cancelable_consumer_.CancelAllRequests();

  url_ = url;

  favicon_expired_ = got_favicon_from_history_ = false;
  current_url_index_ = 0;
  urls_.clear();

  // Request the favicon from the history service. In parallel to this the
  // renderer is going to notify us (well TabContents) when the favicon url is
  // available.
  if (GetFaviconService()) {
    GetFaviconForURL(url_, icon_types_, &cancelable_consumer_,
        NewCallback(this, &FaviconHelper::OnFaviconDataForInitialURL));
  }
}

int FaviconHelper::DownloadImage(const GURL& image_url,
                                 int image_size,
                                 history::IconType icon_type,
                                 ImageDownloadCallback* callback) {
  DCHECK(callback);  // Must provide a callback.
  return ScheduleDownload(GURL(), image_url, image_size, icon_type, callback);
}

FaviconService* FaviconHelper::GetFaviconService() {
  return tab_contents()->profile()->GetFaviconService(Profile::EXPLICIT_ACCESS);
}

void FaviconHelper::SetFavicon(
    const GURL& url,
    const GURL& image_url,
    const SkBitmap& image,
    history::IconType icon_type) {
  const SkBitmap& sized_image = (preferred_icon_size() == 0 ||
      (preferred_icon_size() == image.width() &&
       preferred_icon_size() == image.height())) ?
      image : ConvertToFaviconSize(image);

  if (GetFaviconService() && ShouldSaveFavicon(url)) {
    std::vector<unsigned char> image_data;
    gfx::PNGCodec::EncodeBGRASkBitmap(sized_image, false, &image_data);
    SetHistoryFavicon(url, image_url, image_data, icon_type);
  }

  if (url == url_ && icon_type == history::FAVICON) {
    NavigationEntry* entry = GetEntry();
    if (entry)
      UpdateFavicon(entry, sized_image);
  }
}

void FaviconHelper::UpdateFavicon(NavigationEntry* entry,
                                  scoped_refptr<RefCountedMemory> data) {
  SkBitmap image;
  gfx::PNGCodec::Decode(data->front(), data->size(), &image);
  UpdateFavicon(entry, image);
}

void FaviconHelper::UpdateFavicon(NavigationEntry* entry,
                                  const SkBitmap& image) {
  // No matter what happens, we need to mark the favicon as being set.
  entry->favicon().set_is_valid(true);

  if (image.empty())
    return;

  entry->favicon().set_bitmap(image);
  tab_contents()->NotifyNavigationStateChanged(TabContents::INVALIDATE_TAB);
}

void FaviconHelper::OnUpdateFaviconURL(
    int32 page_id,
    const std::vector<FaviconURL>& candidates) {
  NavigationEntry* entry = GetEntry();
  if (!entry)
    return;

  bool got_favicon_url_update = false;
  for (std::vector<FaviconURL>::const_iterator i = candidates.begin();
       i != candidates.end(); ++i) {
    if (!i->icon_url.is_empty() && (i->icon_type & icon_types_)) {
      if (!got_favicon_url_update) {
        got_favicon_url_update = true;
        urls_.clear();
        current_url_index_ = 0;
      }
      urls_.push_back(*i);
    }
  }

  // TODO(davemoore) Should clear on empty url. Currently we ignore it.
  // This appears to be what FF does as well.
  // No URL was added.
  if (!got_favicon_url_update)
    return;

  if (!GetFaviconService())
    return;

  // For FAVICON.
  if (current_candidate()->icon_type == FaviconURL::FAVICON) {
    if (!favicon_expired_ && entry->favicon().is_valid() &&
        DoUrlAndIconMatch(*current_candidate(), entry->favicon().url(),
                          history::FAVICON))
      return;

    entry->favicon().set_url(current_candidate()->icon_url);
  } else if (!favicon_expired_ && got_favicon_from_history_ &&
              history_icon_.is_valid() &&
              DoUrlAndIconMatch(
                  *current_candidate(),
                  history_icon_.icon_url, history_icon_.icon_type)) {
    return;
  }

  if (got_favicon_from_history_)
    DownloadFaviconOrAskHistory(entry->url(), current_candidate()->icon_url,
        ToHistoryIconType(current_candidate()->icon_type));
}

NavigationEntry* FaviconHelper::GetEntry() {
  NavigationEntry* entry = tab_contents()->controller().GetActiveEntry();
  if (entry && entry->url() == url_ &&
      tab_contents()->IsActiveEntry(entry->page_id())) {
    return entry;
  }
  // If the URL has changed out from under us (as will happen with redirects)
  // return NULL.
  return NULL;
}

int FaviconHelper::DownloadFavicon(const GURL& image_url, int image_size) {
  return tab_contents()->render_view_host()->DownloadFavicon(image_url,
                                                             image_size);
}

void FaviconHelper::UpdateFaviconMappingAndFetch(
    const GURL& page_url,
    const GURL& icon_url,
    history::IconType icon_type,
    CancelableRequestConsumerBase* consumer,
    FaviconService::FaviconDataCallback* callback) {
  GetFaviconService()->UpdateFaviconMappingAndFetch(page_url, icon_url,
      icon_type, consumer, callback);
}

void FaviconHelper::GetFavicon(
    const GURL& icon_url,
    history::IconType icon_type,
    CancelableRequestConsumerBase* consumer,
    FaviconService::FaviconDataCallback* callback) {
  GetFaviconService()->GetFavicon(icon_url, icon_type, consumer, callback);
}

void FaviconHelper::GetFaviconForURL(
    const GURL& page_url,
    int icon_types,
    CancelableRequestConsumerBase* consumer,
    FaviconService::FaviconDataCallback* callback) {
  GetFaviconService()->GetFaviconForURL(page_url, icon_types, consumer,
                                        callback);
}

void FaviconHelper::SetHistoryFavicon(
    const GURL& page_url,
    const GURL& icon_url,
    const std::vector<unsigned char>& image_data,
    history::IconType icon_type) {
  GetFaviconService()->SetFavicon(page_url, icon_url, image_data, icon_type);
}

bool FaviconHelper::ShouldSaveFavicon(const GURL& url) {
  if (!tab_contents()->profile()->IsOffTheRecord())
    return true;

  // Otherwise store the favicon if the page is bookmarked.
  BookmarkModel* bookmark_model = tab_contents()->profile()->GetBookmarkModel();
  return bookmark_model && bookmark_model->IsBookmarked(url);
}

bool FaviconHelper::OnMessageReceived(const IPC::Message& message) {
  bool message_handled = true;
  IPC_BEGIN_MESSAGE_MAP(FaviconHelper, message)
    IPC_MESSAGE_HANDLER(IconHostMsg_DidDownloadFavicon, OnDidDownloadFavicon)
    IPC_MESSAGE_UNHANDLED(message_handled = false)
  IPC_END_MESSAGE_MAP()
  return message_handled;
}

void FaviconHelper::OnDidDownloadFavicon(int id,
                                         const GURL& image_url,
                                         bool errored,
                                         const SkBitmap& image) {
  DownloadRequests::iterator i = download_requests_.find(id);
  if (i == download_requests_.end()) {
    // Currently TabContents notifies us of ANY downloads so that it is
    // possible to get here.
    return;
  }

  if (i->second.callback) {
    i->second.callback->Run(id, errored, image);
  } else if (current_candidate() &&
             DoUrlAndIconMatch(*current_candidate(), image_url,
                               i->second.icon_type)) {
    // The downloaded icon is still valid when there is no FaviconURL update
    // during the downloading.
    if (!errored) {
      SetFavicon(i->second.url, image_url, image, i->second.icon_type);
    } else if (GetEntry() && ++current_url_index_ < urls_.size()) {
      // Copies all candidate except first one and notifies the FaviconHelper,
      // so the next candidate can be processed.
      std::vector<FaviconURL> new_candidates(++urls_.begin(), urls_.end());
      OnUpdateFaviconURL(0, new_candidates);
    }
  }
  download_requests_.erase(i);
}

void FaviconHelper::OnFaviconDataForInitialURL(
    FaviconService::Handle handle,
    history::FaviconData favicon) {
  NavigationEntry* entry = GetEntry();
  if (!entry)
    return;

  got_favicon_from_history_ = true;
  history_icon_ = favicon;

  favicon_expired_ = (favicon.known_icon && favicon.expired);

  if (favicon.known_icon && favicon.icon_type == history::FAVICON &&
      !entry->favicon().is_valid() &&
      (!current_candidate() ||
       DoUrlAndIconMatch(
           *current_candidate(), favicon.icon_url, favicon.icon_type))) {
    // The db knows the favicon (although it may be out of date) and the entry
    // doesn't have an icon. Set the favicon now, and if the favicon turns out
    // to be expired (or the wrong url) we'll fetch later on. This way the
    // user doesn't see a flash of the default favicon.
    entry->favicon().set_url(favicon.icon_url);
    if (favicon.is_valid())
      UpdateFavicon(entry, favicon.image_data);
    entry->favicon().set_is_valid(true);
  }

  if (favicon.known_icon && !favicon.expired) {
    if (current_candidate() &&
        !DoUrlAndIconMatch(
             *current_candidate(), favicon.icon_url, favicon.icon_type)) {
      // Mapping in the database is wrong. DownloadFavIconOrAskHistory will
      // update the mapping for this url and download the favicon if we don't
      // already have it.
      DownloadFaviconOrAskHistory(entry->url(), current_candidate()->icon_url,
          static_cast<history::IconType>(current_candidate()->icon_type));
    }
  } else if (current_candidate()) {
    // We know the official url for the favicon, by either don't have the
    // favicon or its expired. Continue on to DownloadFaviconOrAskHistory to
    // either download or check history again.
    DownloadFaviconOrAskHistory(entry->url(), current_candidate()->icon_url,
        ToHistoryIconType(current_candidate()->icon_type));
  }
  // else we haven't got the icon url. When we get it we'll ask the
  // renderer to download the icon.
}

void FaviconHelper::DownloadFaviconOrAskHistory(
    const GURL& page_url,
    const GURL& icon_url,
    history::IconType icon_type) {
  if (favicon_expired_) {
    // We have the mapping, but the favicon is out of date. Download it now.
    ScheduleDownload(page_url, icon_url, preferred_icon_size(), icon_type,
                     NULL);
  } else if (GetFaviconService()) {
    // We don't know the favicon, but we may have previously downloaded the
    // favicon for another page that shares the same favicon. Ask for the
    // favicon given the favicon URL.
    if (tab_contents()->profile()->IsOffTheRecord()) {
      GetFavicon(icon_url, icon_type, &cancelable_consumer_,
          NewCallback(this, &FaviconHelper::OnFaviconData));
    } else {
      // Ask the history service for the icon. This does two things:
      // 1. Attempts to fetch the favicon data from the database.
      // 2. If the favicon exists in the database, this updates the database to
      //    include the mapping between the page url and the favicon url.
      // This is asynchronous. The history service will call back when done.
      // Issue the request and associate the current page ID with it.
      UpdateFaviconMappingAndFetch(page_url, icon_url, icon_type,
          &cancelable_consumer_,
          NewCallback(this, &FaviconHelper::OnFaviconData));
    }
  }
}

void FaviconHelper::OnFaviconData(FaviconService::Handle handle,
                                  history::FaviconData favicon) {
  NavigationEntry* entry = GetEntry();
  if (!entry)
    return;

  // No need to update the favicon url. By the time we get here
  // UpdateFaviconURL will have set the favicon url.
  if (favicon.icon_type == history::FAVICON) {
    if (favicon.is_valid()) {
      // There is a favicon, set it now. If expired we'll download the current
      // one again, but at least the user will get some icon instead of the
      // default and most likely the current one is fine anyway.
      UpdateFavicon(entry, favicon.image_data);
    }
    if (!favicon.known_icon || favicon.expired) {
      // We don't know the favicon, or it is out of date. Request the current
      // one.
      ScheduleDownload(entry->url(), entry->favicon().url(),
                       preferred_icon_size(),
                       history::FAVICON, NULL);
    }
  } else if (current_candidate() && (!favicon.known_icon || favicon.expired ||
      !(DoUrlAndIconMatch(
            *current_candidate(), favicon.icon_url, favicon.icon_type)))) {
    // We don't know the favicon, it is out of date or its type is not same as
    // one got from page. Request the current one.
    ScheduleDownload(entry->url(), current_candidate()->icon_url,
        preferred_icon_size(),
        ToHistoryIconType(current_candidate()->icon_type), NULL);
  }
  history_icon_ = favicon;
}

int FaviconHelper::ScheduleDownload(const GURL& url,
                                    const GURL& image_url,
                                    int image_size,
                                    history::IconType icon_type,
                                    ImageDownloadCallback* callback) {
  const int download_id = DownloadFavicon(image_url, image_size);
  if (download_id) {
    // Download ids should be unique.
    DCHECK(download_requests_.find(download_id) == download_requests_.end());
    download_requests_[download_id] =
        DownloadRequest(url, image_url, callback, icon_type);
  }

  return download_id;
}

SkBitmap FaviconHelper::ConvertToFaviconSize(const SkBitmap& image) {
  int width = image.width();
  int height = image.height();
  if (width > 0 && height > 0) {
    calc_favicon_target_size(&width, &height);
    return skia::ImageOperations::Resize(
          image, skia::ImageOperations::RESIZE_LANCZOS3,
          width, height);
  }
  return image;
}