// Copyright (c) 2015 The Chromium Embedded Framework 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 "include/wrapper/cef_resource_manager.h"

#include <algorithm>
#include <memory>
#include <vector>

#include "include/base/cef_callback.h"
#include "include/base/cef_weak_ptr.h"
#include "include/cef_parser.h"
#include "include/wrapper/cef_stream_resource_handler.h"
#include "include/wrapper/cef_zip_archive.h"

namespace {

#if defined(OS_WIN)
#define PATH_SEP '\\'
#else
#define PATH_SEP '/'
#endif

// Returns |url| without the query or fragment components, if any.
std::string GetUrlWithoutQueryOrFragment(const std::string& url) {
  // Find the first instance of '?' or '#'.
  const size_t pos = std::min(url.find('?'), url.find('#'));
  if (pos != std::string::npos) {
    return url.substr(0, pos);
  }

  return url;
}

// Determine the mime type based on the |url| file extension.
std::string GetMimeType(const std::string& url) {
  std::string mime_type;
  const std::string& url_without_query = GetUrlWithoutQueryOrFragment(url);
  size_t sep = url_without_query.find_last_of(".");
  if (sep != std::string::npos) {
    mime_type = CefGetMimeType(url_without_query.substr(sep + 1));
    if (!mime_type.empty()) {
      return mime_type;
    }
  }
  return "text/html";
}

// Default no-op filter.
std::string GetFilteredUrl(const std::string& url) {
  return url;
}

// Provider of fixed contents.
class ContentProvider : public CefResourceManager::Provider {
 public:
  ContentProvider(const std::string& url,
                  const std::string& content,
                  const std::string& mime_type)
      : url_(url), content_(content), mime_type_(mime_type) {
    DCHECK(!url.empty());
    DCHECK(!content.empty());
  }

  ContentProvider(const ContentProvider&) = delete;
  ContentProvider& operator=(const ContentProvider&) = delete;

  bool OnRequest(scoped_refptr<CefResourceManager::Request> request) override {
    CEF_REQUIRE_IO_THREAD();

    const std::string& url = request->url();
    if (url != url_) {
      // Not handled by this provider.
      return false;
    }

    CefRefPtr<CefStreamReader> stream = CefStreamReader::CreateForData(
        static_cast<void*>(const_cast<char*>(content_.data())),
        content_.length());

    // Determine the mime type a single time if it isn't already set.
    if (mime_type_.empty()) {
      mime_type_ = request->mime_type_resolver().Run(url);
    }

    request->Continue(new CefStreamResourceHandler(mime_type_, stream));
    return true;
  }

 private:
  std::string url_;
  std::string content_;
  std::string mime_type_;
};

// Provider of contents loaded from a directory on the file system.
class DirectoryProvider : public CefResourceManager::Provider {
 public:
  DirectoryProvider(const std::string& url_path,
                    const std::string& directory_path)
      : url_path_(url_path), directory_path_(directory_path) {
    DCHECK(!url_path_.empty());
    DCHECK(!directory_path_.empty());

    // Normalize the path values.
    if (url_path_[url_path_.size() - 1] != '/') {
      url_path_ += '/';
    }
    if (directory_path_[directory_path_.size() - 1] != PATH_SEP) {
      directory_path_ += PATH_SEP;
    }
  }

  DirectoryProvider(const DirectoryProvider&) = delete;
  DirectoryProvider& operator=(const DirectoryProvider&) = delete;

  bool OnRequest(scoped_refptr<CefResourceManager::Request> request) override {
    CEF_REQUIRE_IO_THREAD();

    const std::string& url = request->url();
    if (url.find(url_path_) != 0U) {
      return false;
    }

    const std::string& file_path = GetFilePath(url);

    // Open |file_path| on the FILE thread.
    CefPostTask(TID_FILE_USER_BLOCKING,
                base::BindOnce(&DirectoryProvider::OpenOnFileThread, file_path,
                               request));

    return true;
  }

 private:
  std::string GetFilePath(const std::string& url) {
    std::string path_part = url.substr(url_path_.length());
#if defined(OS_WIN)
    std::replace(path_part.begin(), path_part.end(), '/', '\\');
#endif
    return directory_path_ + path_part;
  }

  static void OpenOnFileThread(
      const std::string& file_path,
      scoped_refptr<CefResourceManager::Request> request) {
    CEF_REQUIRE_FILE_USER_BLOCKING_THREAD();

    CefRefPtr<CefStreamReader> stream =
        CefStreamReader::CreateForFile(file_path);

    // Continue loading on the IO thread.
    CefPostTask(TID_IO,
                base::BindOnce(&DirectoryProvider::ContinueOpenOnIOThread,
                               request, stream));
  }

  static void ContinueOpenOnIOThread(
      scoped_refptr<CefResourceManager::Request> request,
      CefRefPtr<CefStreamReader> stream) {
    CEF_REQUIRE_IO_THREAD();

    CefRefPtr<CefStreamResourceHandler> handler;
    if (stream.get()) {
      handler = new CefStreamResourceHandler(
          request->mime_type_resolver().Run(request->url()), stream);
    }
    request->Continue(handler);
  }

  std::string url_path_;
  std::string directory_path_;
};

// Provider of contents loaded from an archive file.
class ArchiveProvider : public CefResourceManager::Provider {
 public:
  ArchiveProvider(const std::string& url_path,
                  const std::string& archive_path,
                  const std::string& password)
      : url_path_(url_path),
        archive_path_(archive_path),
        password_(password),

        weak_ptr_factory_(this) {
    DCHECK(!url_path_.empty());
    DCHECK(!archive_path_.empty());

    // Normalize the path values.
    if (url_path_[url_path_.size() - 1] != '/') {
      url_path_ += '/';
    }
  }

  ArchiveProvider(const ArchiveProvider&) = delete;
  ArchiveProvider& operator=(const ArchiveProvider&) = delete;

  bool OnRequest(scoped_refptr<CefResourceManager::Request> request) override {
    CEF_REQUIRE_IO_THREAD();

    const std::string& url = request->url();
    if (url.find(url_path_) != 0U) {
      // Not handled by this provider.
      return false;
    }

    if (!archive_load_started_) {
      // Initiate archive loading and queue the pending request.
      archive_load_started_ = true;
      pending_requests_.push_back(request);

      // Load the archive file on the FILE thread.
      CefPostTask(TID_FILE_USER_BLOCKING,
                  base::BindOnce(&ArchiveProvider::LoadOnFileThread,
                                 weak_ptr_factory_.GetWeakPtr(), archive_path_,
                                 password_));
      return true;
    }

    if (archive_load_started_ && !archive_load_ended_) {
      // The archive load has already started. Queue the pending request.
      pending_requests_.push_back(request);
      return true;
    }

    // Archive loading is done.
    return ContinueRequest(request);
  }

 private:
  static void LoadOnFileThread(base::WeakPtr<ArchiveProvider> ptr,
                               const std::string& archive_path,
                               const std::string& password) {
    CEF_REQUIRE_FILE_USER_BLOCKING_THREAD();

    CefRefPtr<CefZipArchive> archive;

    CefRefPtr<CefStreamReader> stream =
        CefStreamReader::CreateForFile(archive_path);
    if (stream.get()) {
      archive = new CefZipArchive;
      if (archive->Load(stream, password, true) == 0) {
        DLOG(WARNING) << "Empty archive file: " << archive_path;
        archive = nullptr;
      }
    } else {
      DLOG(WARNING) << "Failed to load archive file: " << archive_path;
    }

    CefPostTask(TID_IO, base::BindOnce(&ArchiveProvider::ContinueOnIOThread,
                                       ptr, archive));
  }

  void ContinueOnIOThread(CefRefPtr<CefZipArchive> archive) {
    CEF_REQUIRE_IO_THREAD();

    archive_load_ended_ = true;
    archive_ = archive;

    if (!pending_requests_.empty()) {
      // Continue all pending requests.
      for (const auto& request : pending_requests_) {
        ContinueRequest(request);
      }
      pending_requests_.clear();
    }
  }

  bool ContinueRequest(scoped_refptr<CefResourceManager::Request> request) {
    CefRefPtr<CefResourceHandler> handler;

    // |archive_| will be NULL if the archive file failed to load or was empty.
    if (archive_.get()) {
      const std::string& url = request->url();
      const std::string& relative_path = url.substr(url_path_.length());
      CefRefPtr<CefZipArchive::File> file = archive_->GetFile(relative_path);
      if (file.get()) {
        handler = new CefStreamResourceHandler(
            request->mime_type_resolver().Run(url), file->GetStreamReader());
      }
    }

    if (!handler.get()) {
      return false;
    }

    request->Continue(handler);
    return true;
  }

  std::string url_path_;
  std::string archive_path_;
  std::string password_;

  bool archive_load_started_ = false;
  bool archive_load_ended_ = false;
  CefRefPtr<CefZipArchive> archive_;

  // List of requests that are pending while the archive is being loaded.
  using PendingRequests =
      std::vector<scoped_refptr<CefResourceManager::Request>>;
  PendingRequests pending_requests_;

  // Must be the last member.
  base::WeakPtrFactory<ArchiveProvider> weak_ptr_factory_;
};

}  // namespace

// CefResourceManager::ProviderEntry implementation.

struct CefResourceManager::ProviderEntry {
  ProviderEntry(Provider* provider, int order, const std::string& identifier)
      : provider_(provider), order_(order), identifier_(identifier) {}

  std::unique_ptr<Provider> provider_;
  int order_;
  std::string identifier_;

  // List of pending requests currently associated with this provider.
  RequestList pending_requests_;

  // True if deletion of this provider is pending.
  bool deletion_pending_ = false;
};

// CefResourceManager::RequestState implementation.

CefResourceManager::RequestState::~RequestState() {
  // Always execute the callback.
  if (callback_.get()) {
    callback_->Continue();
  }
}

// CefResourceManager::Request implementation.

void CefResourceManager::Request::Continue(
    CefRefPtr<CefResourceHandler> handler) {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO, base::BindOnce(&CefResourceManager::Request::Continue,
                                       this, handler));
    return;
  }

  if (!state_.get()) {
    return;
  }

  // Disassociate |state_| immediately so that Provider::OnRequestCanceled is
  // not called unexpectedly if Provider::OnRequest calls this method and then
  // calls CefResourceManager::Remove*.
  CefPostTask(TID_IO,
              base::BindOnce(&CefResourceManager::Request::ContinueOnIOThread,
                             std::move(state_), handler));
}

void CefResourceManager::Request::Stop() {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO,
                base::BindOnce(&CefResourceManager::Request::Stop, this));
    return;
  }

  if (!state_.get()) {
    return;
  }

  // Disassociate |state_| immediately so that Provider::OnRequestCanceled is
  // not called unexpectedly if Provider::OnRequest calls this method and then
  // calls CefResourceManager::Remove*.
  CefPostTask(TID_IO,
              base::BindOnce(&CefResourceManager::Request::StopOnIOThread,
                             std::move(state_)));
}

CefResourceManager::Request::Request(std::unique_ptr<RequestState> state)
    : state_(std::move(state)), params_(state_->params_) {
  CEF_REQUIRE_IO_THREAD();

  ProviderEntry* entry = *(state_->current_entry_pos_);
  // Should not be on a deleted entry.
  DCHECK(!entry->deletion_pending_);

  // Add this request to the entry's pending request list.
  entry->pending_requests_.push_back(this);
  state_->current_request_pos_ = --entry->pending_requests_.end();
}

// Detaches and returns |state_| if the provider indicates that it will not
// handle the request. Note that |state_| may already be NULL if OnRequest
// executes a callback before returning, in which case execution will continue
// asynchronously in any case.
std::unique_ptr<CefResourceManager::RequestState>
CefResourceManager::Request::SendRequest() {
  CEF_REQUIRE_IO_THREAD();
  Provider* provider = (*state_->current_entry_pos_)->provider_.get();
  if (!provider->OnRequest(this)) {
    return std::move(state_);
  }
  return std::unique_ptr<RequestState>();
}

bool CefResourceManager::Request::HasState() {
  CEF_REQUIRE_IO_THREAD();
  return (state_.get() != nullptr);
}

// static
void CefResourceManager::Request::ContinueOnIOThread(
    std::unique_ptr<RequestState> state,
    CefRefPtr<CefResourceHandler> handler) {
  CEF_REQUIRE_IO_THREAD();
  // The manager may already have been deleted.
  base::WeakPtr<CefResourceManager> manager = state->manager_;
  if (manager) {
    manager->ContinueRequest(std::move(state), handler);
  }
}

// static
void CefResourceManager::Request::StopOnIOThread(
    std::unique_ptr<RequestState> state) {
  CEF_REQUIRE_IO_THREAD();
  // The manager may already have been deleted.
  base::WeakPtr<CefResourceManager> manager = state->manager_;
  if (manager) {
    manager->StopRequest(std::move(state));
  }
}

// CefResourceManager implementation.

CefResourceManager::CefResourceManager()
    : url_filter_(base::BindRepeating(GetFilteredUrl)),
      mime_type_resolver_(base::BindRepeating(GetMimeType)) {}

CefResourceManager::~CefResourceManager() {
  CEF_REQUIRE_IO_THREAD();
  RemoveAllProviders();

  // Delete all entryies now. Requests may still be pending but they will not
  // call back into this manager due to the use of WeakPtr.
  if (!providers_.empty()) {
    for (auto* entry : providers_) {
      delete entry;
    }
    providers_.clear();
  }
}

void CefResourceManager::AddContentProvider(const std::string& url,
                                            const std::string& content,
                                            const std::string& mime_type,
                                            int order,
                                            const std::string& identifier) {
  AddProvider(new ContentProvider(url, content, mime_type), order, identifier);
}

void CefResourceManager::AddDirectoryProvider(const std::string& url_path,
                                              const std::string& directory_path,
                                              int order,
                                              const std::string& identifier) {
  AddProvider(new DirectoryProvider(url_path, directory_path), order,
              identifier);
}

void CefResourceManager::AddArchiveProvider(const std::string& url_path,
                                            const std::string& archive_path,
                                            const std::string& password,
                                            int order,
                                            const std::string& identifier) {
  AddProvider(new ArchiveProvider(url_path, archive_path, password), order,
              identifier);
}

void CefResourceManager::AddProvider(Provider* provider,
                                     int order,
                                     const std::string& identifier) {
  DCHECK(provider);
  if (!provider) {
    return;
  }

  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO, base::BindOnce(&CefResourceManager::AddProvider, this,
                                       provider, order, identifier));
    return;
  }

  std::unique_ptr<ProviderEntry> new_entry(
      new ProviderEntry(provider, order, identifier));

  if (providers_.empty()) {
    providers_.push_back(new_entry.release());
    return;
  }

  // Insert before the first entry with a higher |order| value.
  ProviderEntryList::iterator it = providers_.begin();
  for (; it != providers_.end(); ++it) {
    if ((*it)->order_ > order) {
      break;
    }
  }

  providers_.insert(it, new_entry.release());
}

void CefResourceManager::RemoveProviders(const std::string& identifier) {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO, base::BindOnce(&CefResourceManager::RemoveProviders,
                                       this, identifier));
    return;
  }

  if (providers_.empty()) {
    return;
  }

  ProviderEntryList::iterator it = providers_.begin();
  while (it != providers_.end()) {
    if ((*it)->identifier_ == identifier) {
      DeleteProvider(it, false);
    } else {
      ++it;
    }
  }
}

void CefResourceManager::RemoveAllProviders() {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO,
                base::BindOnce(&CefResourceManager::RemoveAllProviders, this));
    return;
  }

  if (providers_.empty()) {
    return;
  }

  ProviderEntryList::iterator it = providers_.begin();
  while (it != providers_.end()) {
    DeleteProvider(it, true);
  }
}

void CefResourceManager::SetMimeTypeResolver(const MimeTypeResolver& resolver) {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO, base::BindOnce(&CefResourceManager::SetMimeTypeResolver,
                                       this, resolver));
    return;
  }

  if (!resolver.is_null()) {
    mime_type_resolver_ = resolver;
  } else {
    mime_type_resolver_ = base::BindRepeating(GetMimeType);
  }
}

void CefResourceManager::SetUrlFilter(const UrlFilter& filter) {
  if (!CefCurrentlyOn(TID_IO)) {
    CefPostTask(TID_IO, base::BindOnce(&CefResourceManager::SetUrlFilter, this,
                                       filter));
    return;
  }

  if (!filter.is_null()) {
    url_filter_ = filter;
  } else {
    url_filter_ = base::BindRepeating(GetFilteredUrl);
  }
}

cef_return_value_t CefResourceManager::OnBeforeResourceLoad(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefRequest> request,
    CefRefPtr<CefCallback> callback) {
  CEF_REQUIRE_IO_THREAD();

  // Find the first provider that is not pending deletion.
  ProviderEntryList::iterator current_entry_pos = providers_.begin();
  GetNextValidProvider(current_entry_pos);

  if (current_entry_pos == providers_.end()) {
    // No providers so continue the request immediately.
    return RV_CONTINUE;
  }

  auto state = std::make_unique<RequestState>();

  if (!weak_ptr_factory_.get()) {
    // WeakPtrFactory instances need to be created and destroyed on the same
    // thread. This object performs most of its work on the IO thread and will
    // be destroyed on the IO thread so, now that we're on the IO thread,
    // properly initialize the WeakPtrFactory.
    weak_ptr_factory_ =
        std::make_unique<base::WeakPtrFactory<CefResourceManager>>(this);
  }

  state->manager_ = weak_ptr_factory_->GetWeakPtr();
  state->callback_ = callback;

  state->params_.url_ =
      GetUrlWithoutQueryOrFragment(url_filter_.Run(request->GetURL()));
  state->params_.browser_ = browser;
  state->params_.frame_ = frame;
  state->params_.request_ = request;
  state->params_.url_filter_ = url_filter_;
  state->params_.mime_type_resolver_ = mime_type_resolver_;

  state->current_entry_pos_ = current_entry_pos;

  // If the request is potentially handled we need to continue asynchronously.
  return SendRequest(std::move(state)) ? RV_CONTINUE_ASYNC : RV_CONTINUE;
}

CefRefPtr<CefResourceHandler> CefResourceManager::GetResourceHandler(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefRequest> request) {
  CEF_REQUIRE_IO_THREAD();

  if (pending_handlers_.empty()) {
    return nullptr;
  }

  CefRefPtr<CefResourceHandler> handler;

  PendingHandlersMap::iterator it =
      pending_handlers_.find(request->GetIdentifier());
  if (it != pending_handlers_.end()) {
    handler = it->second;
    pending_handlers_.erase(it);
  }

  return handler;
}

// Send the request to providers in order until one potentially handles it or we
// run out of providers. Returns true if the request is potentially handled.
bool CefResourceManager::SendRequest(std::unique_ptr<RequestState> state) {
  bool potentially_handled = false;

  do {
    // Should not be on the last provider entry.
    DCHECK(state->current_entry_pos_ != providers_.end());
    scoped_refptr<Request> request = new Request(std::move(state));

    // Give the provider an opportunity to handle the request.
    state = request->SendRequest();
    if (state.get()) {
      // The provider will not handle the request. Move to the next provider if
      // any.
      if (!IncrementProvider(state.get())) {
        StopRequest(std::move(state));
      }
    } else {
      potentially_handled = true;
    }
  } while (state.get());

  return potentially_handled;
}

void CefResourceManager::ContinueRequest(
    std::unique_ptr<RequestState> state,
    CefRefPtr<CefResourceHandler> handler) {
  CEF_REQUIRE_IO_THREAD();

  if (handler.get()) {
    // The request has been handled. Associate the request ID with the handler.
    pending_handlers_.insert(
        std::make_pair(state->params_.request_->GetIdentifier(), handler));
    StopRequest(std::move(state));
  } else {
    // Move to the next provider if any.
    if (IncrementProvider(state.get())) {
      SendRequest(std::move(state));
    } else {
      StopRequest(std::move(state));
    }
  }
}

void CefResourceManager::StopRequest(std::unique_ptr<RequestState> state) {
  CEF_REQUIRE_IO_THREAD();

  // Detach from the current provider.
  DetachRequestFromProvider(state.get());

  // Delete the state object and execute the callback.
  state.reset();
}

// Move state to the next provider if any and return true if there are more
// providers.
bool CefResourceManager::IncrementProvider(RequestState* state) {
  // Identify the next provider.
  ProviderEntryList::iterator next_entry_pos = state->current_entry_pos_;
  GetNextValidProvider(++next_entry_pos);

  // Detach from the current provider.
  DetachRequestFromProvider(state);

  if (next_entry_pos != providers_.end()) {
    // Update the state to reference the new provider entry.
    state->current_entry_pos_ = next_entry_pos;
    return true;
  }

  return false;
}

// The new provider, if any, should be determined before calling this method.
void CefResourceManager::DetachRequestFromProvider(RequestState* state) {
  if (state->current_entry_pos_ != providers_.end()) {
    // Remove the association from the current provider entry.
    ProviderEntryList::iterator current_entry_pos = state->current_entry_pos_;
    ProviderEntry* current_entry = *(current_entry_pos);
    current_entry->pending_requests_.erase(state->current_request_pos_);

    if (current_entry->deletion_pending_ &&
        current_entry->pending_requests_.empty()) {
      // Delete the current provider entry now.
      providers_.erase(current_entry_pos);
      delete current_entry;
    }

    // Set to the end for error checking purposes.
    state->current_entry_pos_ = providers_.end();
  }
}

// Move to the next provider that is not pending deletion.
void CefResourceManager::GetNextValidProvider(
    ProviderEntryList::iterator& iterator) {
  while (iterator != providers_.end() && (*iterator)->deletion_pending_) {
    ++iterator;
  }
}

void CefResourceManager::DeleteProvider(ProviderEntryList::iterator& iterator,
                                        bool stop) {
  CEF_REQUIRE_IO_THREAD();

  ProviderEntry* current_entry = *(iterator);

  if (current_entry->deletion_pending_) {
    ++iterator;
    return;
  }

  if (!current_entry->pending_requests_.empty()) {
    // Don't delete the provider entry until all pending requests have cleared.
    current_entry->deletion_pending_ = true;

    // Continue pending requests immediately.
    for (const auto& request : current_entry->pending_requests_) {
      if (request->HasState()) {
        if (stop) {
          request->Stop();
        } else {
          request->Continue(nullptr);
        }
        current_entry->provider_->OnRequestCanceled(request);
      }
    }

    ++iterator;
  } else {
    // Delete the provider entry now.
    iterator = providers_.erase(iterator);
    delete current_entry;
  }
}
