// Copyright (c) 2012 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/intents/web_intents_registry.h"

#include <algorithm>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/string_util.h"
#include "base/string16.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/intents/default_web_intent_service.h"
#include "chrome/browser/intents/web_intents_util.h"
#include "chrome/browser/webdata/web_data_service.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_set.h"
#include "googleurl/src/gurl.h"
#include "net/base/mime_util.h"

using extensions::Extension;
using net::IsMimeType;

namespace {

// TODO(hshi): Temporary workaround for http://crbug.com/134197.
// If no user-set default service is found, use built-in QuickOffice Viewer as
// default for MS office files. Remove this once full defaults is in place.
const char* kQuickOfficeViewerMimeType[] = {
  "application/msword",
  "application/vnd.ms-powerpoint",
  "application/vnd.ms-excel",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
};

typedef base::Callback<void(const WDTypedResult* result)> ResultsHandler;
typedef WebIntentsRegistry::IntentServiceList IntentServiceList;

// Compares two web intents type specifiers to see if there is a match.
// First checks if both are MIME types. If so, uses MatchesMimeType.
// If not, uses exact string equality.
bool WebIntentsTypesMatch(const string16& type1, const string16& type2) {
  return (IsMimeType(UTF16ToUTF8(type1)) && IsMimeType(UTF16ToUTF8(type2)))
      ? web_intents::MimeTypesMatch(type1, type2)
      : type1 == type2;
}

// Adds any intent services of |extension| that match |action| to
// |matching_services|.
void AddMatchingServicesForExtension(const Extension& extension,
                                     const string16& action,
                                     IntentServiceList* matching_services) {
  const IntentServiceList& services = extension.intents_services();
  for (IntentServiceList::const_iterator i = services.begin();
       i != services.end(); ++i) {
    if (action.empty() || action == i->action)
      matching_services->push_back(*i);
  }
}

// Removes all services from |matching_services| that do not match |type|.
// Wildcards are supported, of the form '<type>/*' or '*'.
void FilterServicesByType(const string16& type,
                          IntentServiceList* matching_services) {
  // Filter out all services not matching the query type.
  IntentServiceList::iterator iter(matching_services->begin());
  while (iter != matching_services->end()) {
    if (WebIntentsTypesMatch(iter->type, type))
      ++iter;
    else
      iter = matching_services->erase(iter);
  }
}

// Callback for existence checks. Converts a callback for a list of services
// into a callback that returns true if the list contains a specific service.
void ExistenceCallback(const webkit_glue::WebIntentServiceData& service,
                       const base::Callback<void(bool)>& callback,
                       const WDTypedResult* result) {

  WebIntentsRegistry::IntentServiceList list = static_cast<
      const WDResult<IntentServiceList>*>(result)->GetValue();

  for (WebIntentsRegistry::IntentServiceList::const_iterator i = list.begin();
       i != list.end(); ++i) {
    if (*i == service) {
      callback.Run(true);
      return;
    }
  }

  callback.Run(false);
}

// Functor object for intent ordering.
struct IntentOrdering {
  // Implements StrictWeakOrdering for intents, based on intent-equivalence.
  // Order by |service_url|, |action|, |title|, and |disposition| in order.
  bool operator()(const webkit_glue::WebIntentServiceData& lhs,
                  const webkit_glue::WebIntentServiceData& rhs) {
    if (lhs.service_url != rhs.service_url)
      return lhs.service_url < rhs.service_url;

    if (lhs.action != rhs.action)
      return lhs.action < rhs.action;

    if (lhs.title != rhs.title)
      return lhs.title < rhs.title;

    if (lhs.disposition != rhs.disposition)
      return lhs.disposition < rhs.disposition;

    // At this point, we consider intents to be equal, even if |type| differs.
    return false;
  }
};

// Two intents are equivalent iff all fields except |type| are equal.
bool IntentsAreEquivalent(const webkit_glue::WebIntentServiceData& lhs,
                          const webkit_glue::WebIntentServiceData& rhs) {
  return !((lhs.service_url != rhs.service_url) ||
      (lhs.action != rhs.action) ||
      (lhs.title != rhs.title) ||
      (lhs.disposition != rhs.disposition));
}

}  // namespace

using webkit_glue::WebIntentServiceData;

// Internal object containing arguments to be used in post processing
// WDS results.
struct WebIntentsRegistry::QueryParams {

  // The particular action to filter for while searching through extensions.
  // If |action_| is empty, return all extension-provided services.
  string16 action_;

  // The MIME type that was requested for this service query.
  // Suppports wild cards.
  string16 type_;

  // The url of the invoking page.
  GURL url_;

  // Create a new QueryParams for all intent services or for existence checks.
  QueryParams() : type_(ASCIIToUTF16("*")) {}

  QueryParams(const string16& action, const string16& type)
      : action_(action), type_(type) {}

  // Create a new QueryParams for default services.
  QueryParams(const string16& action, const string16& type, const GURL& url)
      : action_(action), type_(type), url_(url) {}
};

// Internal object adapting the WDS consumer interface to base::Bind
// callback way of doing business.
class WebIntentsRegistry::QueryAdapter : public WebDataServiceConsumer {

 public:
  // Underlying data query.
  WebDataService::Handle query_handle_;

  // Create a new QueryAdapter that delegates results to |handler|.
  QueryAdapter(WebIntentsRegistry* registry, const ResultsHandler& handler)
      : registry_(registry), handler_(handler) {
    registry_->TrackQuery(this);
  }

  void OnWebDataServiceRequestDone(
      WebDataService::Handle h,
      const WDTypedResult* result) OVERRIDE {

    handler_.Run(result);
    registry_->ReleaseQuery(this);
  }

 private:
  // Handle so we can call back into the WebIntentsRegistry when
  // processing query results. The registry is guaranteed to be
  // valid for the life of this object. We do not own this object.
  WebIntentsRegistry* registry_;

  // The callback for this particular query.
  ResultsHandler handler_;
};

WebIntentsRegistry::WebIntentsRegistry() {}

WebIntentsRegistry::~WebIntentsRegistry() {

  // Cancel all pending queries, since we can't handle them any more.
  for (QueryVector::iterator it = pending_queries_.begin();
      it != pending_queries_.end(); ++it) {
    QueryAdapter* query = *it;
    wds_->CancelRequest(query->query_handle_);
    delete query;
  }
}

void WebIntentsRegistry::Initialize(
    scoped_refptr<WebDataService> wds,
    ExtensionServiceInterface* extension_service) {
  wds_ = wds;
  extension_service_ = extension_service;
}

void WebIntentsRegistry::OnWebIntentsResultReceived(
    const QueryParams& params,
    const QueryCallback& callback,
    const WDTypedResult* result) {
  DCHECK(result);
  DCHECK(result->GetType() == WEB_INTENTS_RESULT);

  IntentServiceList matching_services = static_cast<
      const WDResult<IntentServiceList>*>(result)->GetValue();

  // Loop over all services in all extensions, collect ones
  // matching the query.
  if (extension_service_) {
    const ExtensionSet* extensions = extension_service_->extensions();
    if (extensions) {
      for (ExtensionSet::const_iterator i(extensions->begin());
           i != extensions->end(); ++i) {
        AddMatchingServicesForExtension(**i, params.action_,
                                        &matching_services);
      }
    }
  }

  // Filter out all services not matching the query type.
  FilterServicesByType(params.type_, &matching_services);

  // Collapse intents that are equivalent for all but |type|.
  CollapseIntents(&matching_services);

  callback.Run(matching_services);
}

void WebIntentsRegistry::OnAllDefaultIntentServicesReceived(
    const DefaultIntentServicesCallback& callback,
    const WDTypedResult* result) {
  DCHECK(result);
  DCHECK(result->GetType() == WEB_INTENTS_DEFAULTS_RESULT);

  const std::vector<DefaultWebIntentService> services = static_cast<
      const WDResult<std::vector<DefaultWebIntentService> >*>(result)->
          GetValue();

  callback.Run(services);
}

void WebIntentsRegistry::OnWebIntentsDefaultsResultReceived(
    const QueryParams& params,
    const DefaultQueryCallback& callback,
    const WDTypedResult* result) {
  DCHECK(result);
  DCHECK(result->GetType() == WEB_INTENTS_DEFAULTS_RESULT);

  std::vector<DefaultWebIntentService> services = static_cast<
      const WDResult<std::vector<DefaultWebIntentService> >*>(result)->
          GetValue();

  DefaultWebIntentService default_service;
  std::vector<DefaultWebIntentService>::iterator iter(services.begin());
  for (; iter != services.end(); ++iter) {
    if (!WebIntentsTypesMatch(iter->type, params.type_)) {
      continue;
    }
    if (!iter->url_pattern.MatchesURL(params.url_)) {
      continue;
    }
    const Extension* extension = ExtensionForURL(iter->service_url);
    if (extension != NULL &&
        !extension_service_->IsExtensionEnabled(extension->id())) {
      continue;
    }

    // Found a match. If it is better than default_service, use it.
    // Currently the metric is that if the new value is user-set,
    // prefer it. If the new value has a more specific pattern, prefer it.
    // If the new value has a more recent date, prefer it.
    if (default_service.user_date <= 0 && iter->user_date >= 0) {
      default_service = *iter;
    } else if (default_service.url_pattern.match_all_urls() &&
             !iter->url_pattern.match_all_urls()) {
      default_service = *iter;
    } else if (iter->url_pattern < default_service.url_pattern) {
      default_service = *iter;
    } else if (default_service.user_date < iter->user_date) {
      default_service = *iter;
    }
  }

  // TODO(hshi): Temporary workaround for http://crbug.com/134197.
  // If no user-set default service is found, use built-in QuickOffice Viewer as
  // default for MS office files. Remove this once full defaults is in place.
  if (default_service.user_date <= 0) {
    for (size_t i = 0; i < sizeof(kQuickOfficeViewerMimeType) / sizeof(char*);
         ++i) {
      DefaultWebIntentService qoviewer_service;
      qoviewer_service.action = ASCIIToUTF16(web_intents::kActionView);
      qoviewer_service.type = ASCIIToUTF16(kQuickOfficeViewerMimeType[i]);
      qoviewer_service.service_url = web_intents::kQuickOfficeViewerServiceURL;
      if (WebIntentsTypesMatch(qoviewer_service.type, params.type_)) {
        default_service = qoviewer_service;
        break;
      }
    }
  }

  callback.Run(default_service);
}

void WebIntentsRegistry::GetIntentServices(
    const string16& action, const string16& type,
    const QueryCallback& callback) {
  DCHECK(wds_.get());
  DCHECK(!callback.is_null());

  const QueryParams params(action, type);
  const ResultsHandler handler = base::Bind(
      &WebIntentsRegistry::OnWebIntentsResultReceived,
      base::Unretained(this),
      params,
      callback);

  QueryAdapter* query = new QueryAdapter(this, handler);
  query->query_handle_ = wds_->GetWebIntentServices(action, query);
}

void WebIntentsRegistry::GetAllIntentServices(
    const QueryCallback& callback) {
  DCHECK(wds_.get());
  DCHECK(!callback.is_null());

  const QueryParams params;
  const ResultsHandler handler = base::Bind(
      &WebIntentsRegistry::OnWebIntentsResultReceived,
      base::Unretained(this),
      params,
      callback);

  QueryAdapter* query = new QueryAdapter(this, handler);
  query->query_handle_ = wds_->GetAllWebIntentServices(query);
}

void WebIntentsRegistry::GetAllDefaultIntentServices(
    const DefaultIntentServicesCallback& callback) {
  DCHECK(!callback.is_null());

  ResultsHandler handler = base::Bind(
      &WebIntentsRegistry::OnAllDefaultIntentServicesReceived,
      base::Unretained(this),
      callback);

  QueryAdapter* query = new QueryAdapter(this, handler);
  query->query_handle_ =
      wds_->GetAllDefaultWebIntentServices(query);
}

void WebIntentsRegistry::IntentServiceExists(
    const WebIntentServiceData& service,
    const base::Callback<void(bool)>& callback) {
  DCHECK(!callback.is_null());

  ResultsHandler handler = base::Bind(
      &ExistenceCallback,
      service,
      callback);

  QueryAdapter* query = new QueryAdapter(this, handler);
  query->query_handle_ = wds_->GetWebIntentServicesForURL(
      UTF8ToUTF16(service.service_url.spec()), query);
}

void WebIntentsRegistry::GetIntentServicesForExtensionFilter(
        const string16& action,
        const string16& type,
        const std::string& extension_id,
        IntentServiceList* services) {
  if (extension_service_) {
    const Extension* extension =
        extension_service_->GetExtensionById(extension_id, false);
    AddMatchingServicesForExtension(*extension,
                                    action,
                                    services);
    FilterServicesByType(type, services);
  }
}

void WebIntentsRegistry::RegisterDefaultIntentService(
    const DefaultWebIntentService& default_service) {
  DCHECK(wds_.get());
  wds_->AddDefaultWebIntentService(default_service);
}

void WebIntentsRegistry::UnregisterDefaultIntentService(
    const DefaultWebIntentService& default_service) {
  DCHECK(wds_.get());
  wds_->RemoveDefaultWebIntentService(default_service);
}

void WebIntentsRegistry::UnregisterServiceDefaults(const GURL& service_url) {
  DCHECK(wds_.get());
  wds_->RemoveWebIntentServiceDefaults(service_url);
}

void WebIntentsRegistry::GetDefaultIntentService(
    const string16& action,
    const string16& type,
    const GURL& invoking_url,
    const DefaultQueryCallback& callback) {
  DCHECK(!callback.is_null());

  const QueryParams params(action, type);

  ResultsHandler handler = base::Bind(
      &WebIntentsRegistry::OnWebIntentsDefaultsResultReceived,
      base::Unretained(this),
      params,
      callback);

  QueryAdapter* query = new QueryAdapter(this, handler);
  query->query_handle_ =
      wds_->GetDefaultWebIntentServicesForAction(action, query);
}

void WebIntentsRegistry::RegisterIntentService(
    const WebIntentServiceData& service) {
  DCHECK(wds_.get());
  wds_->AddWebIntentService(service);
}

void WebIntentsRegistry::UnregisterIntentService(
    const WebIntentServiceData& service) {
  DCHECK(wds_.get());
  wds_->RemoveWebIntentService(service);
}

void WebIntentsRegistry::CollapseIntents(IntentServiceList* services) {
  DCHECK(services);

  // No need to do anything for no services/single service.
  if (services->size() < 2)
    return;

  // Sort so that intents that can be collapsed must be adjacent.
  std::sort(services->begin(), services->end(), IntentOrdering());

  // Combine adjacent services if they are equivalent.
  IntentServiceList::iterator write_iter = services->begin();
  IntentServiceList::iterator read_iter = write_iter + 1;
  while (read_iter != services->end()) {
    if (IntentsAreEquivalent(*write_iter, *read_iter)) {
      // If the two intents are equivalent, join types and collapse.
      write_iter->type += ASCIIToUTF16(",") + read_iter->type;
    } else {
      // Otherwise, keep both intents.
      ++write_iter;
      if (write_iter != read_iter)
        *write_iter = *read_iter;
    }
    ++read_iter;
  }

  // Cut off everything after the last intent copied to the list.
  if (++write_iter != services->end())
    services->erase(write_iter, services->end());
}

const Extension* WebIntentsRegistry::ExtensionForURL(const std::string& url) {
  const ExtensionSet* extensions = extension_service_->extensions();
  if (!extensions)
    return NULL;

  // Use the unsafe ExtensionURLInfo constructor: we don't care if the extension
  // is running or not.
  GURL gurl(url);
  ExtensionURLInfo info(gurl);
  return extensions->GetExtensionOrAppByURL(info);
}

void WebIntentsRegistry::TrackQuery(QueryAdapter* query) {
  DCHECK(query);
  pending_queries_.push_back(query);
}

void WebIntentsRegistry::ReleaseQuery(QueryAdapter* query) {
  QueryVector::iterator it = std::find(
      pending_queries_.begin(), pending_queries_.end(), query);
  if (it != pending_queries_.end()) {
    pending_queries_.erase(it);
    delete query;
  } else {
    NOTREACHED();
  }
}