// 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/drive/drive_api_util.h"

#include <string>

#include "base/files/file.h"
#include "base/logging.h"
#include "base/md5.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/drive/drive_api_parser.h"
#include "google_apis/drive/gdata_wapi_parser.h"
#include "net/base/escape.h"
#include "third_party/re2/re2/re2.h"
#include "url/gurl.h"

namespace drive {
namespace util {
namespace {

// Google Apps MIME types:
const char kGoogleDocumentMimeType[] = "application/vnd.google-apps.document";
const char kGoogleDrawingMimeType[] = "application/vnd.google-apps.drawing";
const char kGooglePresentationMimeType[] =
    "application/vnd.google-apps.presentation";
const char kGoogleSpreadsheetMimeType[] =
    "application/vnd.google-apps.spreadsheet";
const char kGoogleTableMimeType[] = "application/vnd.google-apps.table";
const char kGoogleFormMimeType[] = "application/vnd.google-apps.form";
const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder";

std::string GetMimeTypeFromEntryKind(google_apis::DriveEntryKind kind) {
  switch (kind) {
    case google_apis::ENTRY_KIND_DOCUMENT:
      return kGoogleDocumentMimeType;
    case google_apis::ENTRY_KIND_SPREADSHEET:
      return kGoogleSpreadsheetMimeType;
    case google_apis::ENTRY_KIND_PRESENTATION:
      return kGooglePresentationMimeType;
    case google_apis::ENTRY_KIND_DRAWING:
      return kGoogleDrawingMimeType;
    case google_apis::ENTRY_KIND_TABLE:
      return kGoogleTableMimeType;
    case google_apis::ENTRY_KIND_FORM:
      return kGoogleFormMimeType;
    default:
      return std::string();
  }
}

ScopedVector<std::string> CopyScopedVectorString(
    const ScopedVector<std::string>& source) {
  ScopedVector<std::string> result;
  result.reserve(source.size());
  for (size_t i = 0; i < source.size(); ++i)
    result.push_back(new std::string(*source[i]));

  return result.Pass();
}

// Converts AppIcon (of GData WAPI) to DriveAppIcon.
scoped_ptr<google_apis::DriveAppIcon>
ConvertAppIconToDriveAppIcon(const google_apis::AppIcon& app_icon) {
  scoped_ptr<google_apis::DriveAppIcon> resource(
      new google_apis::DriveAppIcon);
  switch (app_icon.category()) {
    case google_apis::AppIcon::ICON_UNKNOWN:
      resource->set_category(google_apis::DriveAppIcon::UNKNOWN);
      break;
    case google_apis::AppIcon::ICON_DOCUMENT:
      resource->set_category(google_apis::DriveAppIcon::DOCUMENT);
      break;
    case google_apis::AppIcon::ICON_APPLICATION:
      resource->set_category(google_apis::DriveAppIcon::APPLICATION);
      break;
    case google_apis::AppIcon::ICON_SHARED_DOCUMENT:
      resource->set_category(google_apis::DriveAppIcon::SHARED_DOCUMENT);
      break;
    default:
      NOTREACHED();
  }

  resource->set_icon_side_length(app_icon.icon_side_length());
  resource->set_icon_url(app_icon.GetIconURL());
  return resource.Pass();
}

// Converts InstalledApp to AppResource.
scoped_ptr<google_apis::AppResource>
ConvertInstalledAppToAppResource(
    const google_apis::InstalledApp& installed_app) {
  scoped_ptr<google_apis::AppResource> resource(new google_apis::AppResource);
  resource->set_application_id(installed_app.app_id());
  resource->set_name(installed_app.app_name());
  resource->set_object_type(installed_app.object_type());
  resource->set_supports_create(installed_app.supports_create());

  {
    ScopedVector<std::string> primary_mimetypes(
        CopyScopedVectorString(installed_app.primary_mimetypes()));
    resource->set_primary_mimetypes(primary_mimetypes.Pass());
  }
  {
    ScopedVector<std::string> secondary_mimetypes(
        CopyScopedVectorString(installed_app.secondary_mimetypes()));
    resource->set_secondary_mimetypes(secondary_mimetypes.Pass());
  }
  {
    ScopedVector<std::string> primary_file_extensions(
        CopyScopedVectorString(installed_app.primary_extensions()));
    resource->set_primary_file_extensions(primary_file_extensions.Pass());
  }
  {
    ScopedVector<std::string> secondary_file_extensions(
        CopyScopedVectorString(installed_app.secondary_extensions()));
    resource->set_secondary_file_extensions(secondary_file_extensions.Pass());
  }

  {
    const ScopedVector<google_apis::AppIcon>& app_icons =
        installed_app.app_icons();
    ScopedVector<google_apis::DriveAppIcon> icons;
    icons.reserve(app_icons.size());
    for (size_t i = 0; i < app_icons.size(); ++i) {
      icons.push_back(ConvertAppIconToDriveAppIcon(*app_icons[i]).release());
    }
    resource->set_icons(icons.Pass());
  }

  // supports_import, installed and authorized are not supported in
  // InstalledApp.

  return resource.Pass();
}

// Returns the argument string.
std::string Identity(const std::string& resource_id) { return resource_id; }

}  // namespace


std::string EscapeQueryStringValue(const std::string& str) {
  std::string result;
  result.reserve(str.size());
  for (size_t i = 0; i < str.size(); ++i) {
    if (str[i] == '\\' || str[i] == '\'') {
      result.push_back('\\');
    }
    result.push_back(str[i]);
  }
  return result;
}

std::string TranslateQuery(const std::string& original_query) {
  // In order to handle non-ascii white spaces correctly, convert to UTF16.
  base::string16 query = base::UTF8ToUTF16(original_query);
  const base::string16 kDelimiter(
      base::kWhitespaceUTF16 +
      base::string16(1, static_cast<base::char16>('"')));

  std::string result;
  for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
       index != base::string16::npos;
       index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
    bool is_exclusion = (query[index] == '-');
    if (is_exclusion)
      ++index;
    if (index == query.length()) {
      // Here, the token is '-' and it should be ignored.
      continue;
    }

    size_t begin_token = index;
    base::string16 token;
    if (query[begin_token] == '"') {
      // Quoted query.
      ++begin_token;
      size_t end_token = query.find('"', begin_token);
      if (end_token == base::string16::npos) {
        // This is kind of syntax error, since quoted string isn't finished.
        // However, the query is built by user manually, so here we treat
        // whole remaining string as a token as a fallback, by appending
        // a missing double-quote character.
        end_token = query.length();
        query.push_back('"');
      }

      token = query.substr(begin_token, end_token - begin_token);
      index = end_token + 1;  // Consume last '"', too.
    } else {
      size_t end_token = query.find_first_of(kDelimiter, begin_token);
      if (end_token == base::string16::npos) {
        end_token = query.length();
      }

      token = query.substr(begin_token, end_token - begin_token);
      index = end_token;
    }

    if (token.empty()) {
      // Just ignore an empty token.
      continue;
    }

    if (!result.empty()) {
      // If there are two or more tokens, need to connect with "and".
      result.append(" and ");
    }

    // The meaning of "fullText" should include title, description and content.
    base::StringAppendF(
        &result,
        "%sfullText contains \'%s\'",
        is_exclusion ? "not " : "",
        EscapeQueryStringValue(base::UTF16ToUTF8(token)).c_str());
  }

  return result;
}

std::string ExtractResourceIdFromUrl(const GURL& url) {
  return net::UnescapeURLComponent(url.ExtractFileName(),
                                   net::UnescapeRule::URL_SPECIAL_CHARS);
}

std::string CanonicalizeResourceId(const std::string& resource_id) {
  // If resource ID is in the old WAPI format starting with a prefix like
  // "document:", strip it and return the remaining part.
  std::string stripped_resource_id;
  if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
                     &stripped_resource_id))
    return stripped_resource_id;
  return resource_id;
}

ResourceIdCanonicalizer GetIdentityResourceIdCanonicalizer() {
  return base::Bind(&Identity);
}

const char kDocsListScope[] = "https://docs.google.com/feeds/";
const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";

void ParseShareUrlAndRun(const google_apis::GetShareUrlCallback& callback,
                         google_apis::GDataErrorCode error,
                         scoped_ptr<base::Value> value) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  if (!value) {
    callback.Run(error, GURL());
    return;
  }

  // Parsing ResourceEntry is cheap enough to do on UI thread.
  scoped_ptr<google_apis::ResourceEntry> entry =
      google_apis::ResourceEntry::ExtractAndParse(*value);
  if (!entry) {
    callback.Run(google_apis::GDATA_PARSE_ERROR, GURL());
    return;
  }

  const google_apis::Link* share_link =
      entry->GetLinkByType(google_apis::Link::LINK_SHARE);
  callback.Run(error, share_link ? share_link->href() : GURL());
}

scoped_ptr<google_apis::AboutResource>
ConvertAccountMetadataToAboutResource(
    const google_apis::AccountMetadata& account_metadata,
    const std::string& root_resource_id) {
  scoped_ptr<google_apis::AboutResource> resource(
      new google_apis::AboutResource);
  resource->set_largest_change_id(account_metadata.largest_changestamp());
  resource->set_quota_bytes_total(account_metadata.quota_bytes_total());
  resource->set_quota_bytes_used(account_metadata.quota_bytes_used());
  resource->set_root_folder_id(root_resource_id);
  return resource.Pass();
}

scoped_ptr<google_apis::AppList>
ConvertAccountMetadataToAppList(
    const google_apis::AccountMetadata& account_metadata) {
  scoped_ptr<google_apis::AppList> resource(new google_apis::AppList);

  const ScopedVector<google_apis::InstalledApp>& installed_apps =
      account_metadata.installed_apps();
  ScopedVector<google_apis::AppResource> app_resources;
  app_resources.reserve(installed_apps.size());
  for (size_t i = 0; i < installed_apps.size(); ++i) {
    app_resources.push_back(
        ConvertInstalledAppToAppResource(*installed_apps[i]).release());
  }
  resource->set_items(app_resources.Pass());

  // etag is not supported in AccountMetadata.

  return resource.Pass();
}


scoped_ptr<google_apis::FileResource> ConvertResourceEntryToFileResource(
    const google_apis::ResourceEntry& entry) {
  scoped_ptr<google_apis::FileResource> file(new google_apis::FileResource);

  file->set_file_id(entry.resource_id());
  file->set_title(entry.title());
  file->set_created_date(entry.published_time());

  if (std::find(entry.labels().begin(), entry.labels().end(),
                "shared-with-me") != entry.labels().end()) {
    // Set current time to mark the file is shared_with_me, since ResourceEntry
    // doesn't have |shared_with_me_date| equivalent.
    file->set_shared_with_me_date(base::Time::Now());
  }

  file->set_shared(std::find(entry.labels().begin(), entry.labels().end(),
                             "shared") != entry.labels().end());

  if (entry.is_folder()) {
    file->set_mime_type(kDriveFolderMimeType);
  } else {
    std::string mime_type = GetMimeTypeFromEntryKind(entry.kind());
    if (mime_type.empty())
      mime_type = entry.content_mime_type();
    file->set_mime_type(mime_type);
  }

  file->set_md5_checksum(entry.file_md5());
  file->set_file_size(entry.file_size());

  file->mutable_labels()->set_trashed(entry.deleted());
  file->set_etag(entry.etag());

  google_apis::ImageMediaMetadata* image_media_metadata =
    file->mutable_image_media_metadata();
  image_media_metadata->set_width(entry.image_width());
  image_media_metadata->set_height(entry.image_height());
  image_media_metadata->set_rotation(entry.image_rotation());

  std::vector<google_apis::ParentReference>* parents = file->mutable_parents();
  for (size_t i = 0; i < entry.links().size(); ++i) {
    using google_apis::Link;
    const Link& link = *entry.links()[i];
    switch (link.type()) {
      case Link::LINK_PARENT: {
        google_apis::ParentReference parent;
        parent.set_parent_link(link.href());

        std::string file_id =
            drive::util::ExtractResourceIdFromUrl(link.href());
        parent.set_file_id(file_id);
        parent.set_is_root(file_id == kWapiRootDirectoryResourceId);
        parents->push_back(parent);
        break;
      }
      case Link::LINK_ALTERNATE:
        file->set_alternate_link(link.href());
        break;
      default:
        break;
    }
  }

  file->set_modified_date(entry.updated_time());
  file->set_last_viewed_by_me_date(entry.last_viewed_time());

  return file.Pass();
}

google_apis::DriveEntryKind GetKind(
    const google_apis::FileResource& file_resource) {
  if (file_resource.IsDirectory())
    return google_apis::ENTRY_KIND_FOLDER;

  const std::string& mime_type = file_resource.mime_type();
  if (mime_type == kGoogleDocumentMimeType)
    return google_apis::ENTRY_KIND_DOCUMENT;
  if (mime_type == kGoogleSpreadsheetMimeType)
    return google_apis::ENTRY_KIND_SPREADSHEET;
  if (mime_type == kGooglePresentationMimeType)
    return google_apis::ENTRY_KIND_PRESENTATION;
  if (mime_type == kGoogleDrawingMimeType)
    return google_apis::ENTRY_KIND_DRAWING;
  if (mime_type == kGoogleTableMimeType)
    return google_apis::ENTRY_KIND_TABLE;
  if (mime_type == kGoogleFormMimeType)
    return google_apis::ENTRY_KIND_FORM;
  if (mime_type == "application/pdf")
    return google_apis::ENTRY_KIND_PDF;
  return google_apis::ENTRY_KIND_FILE;
}

scoped_ptr<google_apis::ResourceEntry>
ConvertFileResourceToResourceEntry(
    const google_apis::FileResource& file_resource) {
  scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);

  // ResourceEntry
  entry->set_resource_id(file_resource.file_id());
  entry->set_id(file_resource.file_id());
  entry->set_kind(GetKind(file_resource));
  entry->set_title(file_resource.title());
  entry->set_published_time(file_resource.created_date());

  std::vector<std::string> labels;
  if (!file_resource.shared_with_me_date().is_null())
    labels.push_back("shared-with-me");
  if (file_resource.shared())
    labels.push_back("shared");
  entry->set_labels(labels);

  // This should be the url to download the file_resource.
  {
    google_apis::Content content;
    content.set_mime_type(file_resource.mime_type());
    entry->set_content(content);
  }
  // TODO(kochi): entry->resource_links_

  // For file entries
  entry->set_filename(file_resource.title());
  entry->set_suggested_filename(file_resource.title());
  entry->set_file_md5(file_resource.md5_checksum());
  entry->set_file_size(file_resource.file_size());

  // If file is removed completely, that information is only available in
  // ChangeResource, and is reflected in |removed_|. If file is trashed, the
  // file entry still exists but with its "trashed" label true.
  entry->set_deleted(file_resource.labels().is_trashed());

  // ImageMediaMetadata
  entry->set_image_width(file_resource.image_media_metadata().width());
  entry->set_image_height(file_resource.image_media_metadata().height());
  entry->set_image_rotation(file_resource.image_media_metadata().rotation());

  // CommonMetadata
  entry->set_etag(file_resource.etag());
  // entry->authors_
  // entry->links_.
  ScopedVector<google_apis::Link> links;
  for (size_t i = 0; i < file_resource.parents().size(); ++i) {
    google_apis::Link* link = new google_apis::Link;
    link->set_type(google_apis::Link::LINK_PARENT);
    link->set_href(file_resource.parents()[i].parent_link());
    links.push_back(link);
  }
  if (!file_resource.alternate_link().is_empty()) {
    google_apis::Link* link = new google_apis::Link;
    link->set_type(google_apis::Link::LINK_ALTERNATE);
    link->set_href(file_resource.alternate_link());
    links.push_back(link);
  }
  entry->set_links(links.Pass());

  // entry->categories_
  entry->set_updated_time(file_resource.modified_date());
  entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());

  entry->FillRemainingFields();
  return entry.Pass();
}

scoped_ptr<google_apis::ResourceEntry>
ConvertChangeResourceToResourceEntry(
    const google_apis::ChangeResource& change_resource) {
  scoped_ptr<google_apis::ResourceEntry> entry;
  if (change_resource.file())
    entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
  else
    entry.reset(new google_apis::ResourceEntry);

  entry->set_resource_id(change_resource.file_id());
  // If |is_deleted()| returns true, the file is removed from Drive.
  entry->set_removed(change_resource.is_deleted());
  entry->set_changestamp(change_resource.change_id());
  entry->set_modification_date(change_resource.modification_date());

  return entry.Pass();
}

scoped_ptr<google_apis::ResourceList>
ConvertFileListToResourceList(const google_apis::FileList& file_list) {
  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);

  const ScopedVector<google_apis::FileResource>& items = file_list.items();
  ScopedVector<google_apis::ResourceEntry> entries;
  for (size_t i = 0; i < items.size(); ++i)
    entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
  feed->set_entries(entries.Pass());

  ScopedVector<google_apis::Link> links;
  if (!file_list.next_link().is_empty()) {
    google_apis::Link* link = new google_apis::Link;
    link->set_type(google_apis::Link::LINK_NEXT);
    link->set_href(file_list.next_link());
    links.push_back(link);
  }
  feed->set_links(links.Pass());

  return feed.Pass();
}

scoped_ptr<google_apis::ResourceList>
ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
  scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);

  const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
  ScopedVector<google_apis::ResourceEntry> entries;
  for (size_t i = 0; i < items.size(); ++i) {
    entries.push_back(
        ConvertChangeResourceToResourceEntry(*items[i]).release());
  }
  feed->set_entries(entries.Pass());

  feed->set_largest_changestamp(change_list.largest_change_id());

  ScopedVector<google_apis::Link> links;
  if (!change_list.next_link().is_empty()) {
    google_apis::Link* link = new google_apis::Link;
    link->set_type(google_apis::Link::LINK_NEXT);
    link->set_href(change_list.next_link());
    links.push_back(link);
  }
  feed->set_links(links.Pass());

  return feed.Pass();
}

std::string GetMd5Digest(const base::FilePath& file_path) {
  const int kBufferSize = 512 * 1024;  // 512kB.

  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid())
    return std::string();

  base::MD5Context context;
  base::MD5Init(&context);

  int64 offset = 0;
  scoped_ptr<char[]> buffer(new char[kBufferSize]);
  while (true) {
    int result = file.Read(offset, buffer.get(), kBufferSize);
    if (result < 0) {
      // Found an error.
      return std::string();
    }

    if (result == 0) {
      // End of file.
      break;
    }

    offset += result;
    base::MD5Update(&context, base::StringPiece(buffer.get(), result));
  }

  base::MD5Digest digest;
  base::MD5Final(&digest, &context);
  return MD5DigestToBase16(digest);
}

const char kWapiRootDirectoryResourceId[] = "folder:root";

}  // namespace util
}  // namespace drive
