/* <!-- copyright */
/*
 * aria2 - The high speed download utility
 *
 * Copyright (C) 2013 Tatsuhiro Tsujikawa
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.  If you delete this exception statement from all source
 * files in the program, then also delete it here.
 */
/* copyright --> */
#include "CookieStorage.h"

#include <cstring>
#include <cstdio>
#include <algorithm>

#include "util.h"
#include "LogFactory.h"
#include "Logger.h"
#include "DlAbortEx.h"
#include "fmt.h"
#include "NsCookieParser.h"
#include "File.h"
#include "a2functional.h"
#include "A2STR.h"
#include "message.h"
#include "cookie_helper.h"
#include "BufferedFile.h"
#ifdef HAVE_SQLITE3
# include "Sqlite3CookieParserImpl.h"
#endif // HAVE_SQLITE3

namespace aria2 {

DomainNode::DomainNode(std::string label, DomainNode* parent)
  : label_{std::move(label)}, parent_{parent}, lastAccessTime_{0},
    lruAccessTime_{0}, inLru_{false}
{}

void DomainNode::findCookie
(std::vector<const Cookie*>& out,
 const std::string& requestHost,
 const std::string& requestPath,
 time_t now, bool secure)
{
  if(cookies_) {
    for(auto& c : *cookies_) {
      if(c->match(requestHost, requestPath, now, secure)) {
        c->setLastAccessTime(now);
        out.push_back(c.get());
      }
    }
  }
}

bool DomainNode::addCookie(std::unique_ptr<Cookie> cookie, time_t now)
{
  using namespace std::placeholders;
  setLastAccessTime(now);
  if(!cookies_) {
    if(cookie->isExpired(now)) {
      return false;
    } else {
      cookies_ = make_unique<std::deque<std::unique_ptr<Cookie>>>();
      cookies_->push_back(std::move(cookie));
      return true;
    }
  }

  auto i = std::find_if(std::begin(*cookies_), std::end(*cookies_),
                        [&](const std::unique_ptr<Cookie>& c)
                        {
                          return *c == *cookie;
                        });
  if(i == std::end(*cookies_)) {
    if(cookie->isExpired(now)) {
      return false;
    } else {
      if(cookies_->size() >= CookieStorage::MAX_COOKIE_PER_DOMAIN) {
        cookies_->erase
          (std::remove_if(std::begin(*cookies_), std::end(*cookies_),
                          std::bind(&Cookie::isExpired, _1, now)),
           std::end(*cookies_));
        if(cookies_->size() >= CookieStorage::MAX_COOKIE_PER_DOMAIN) {
          auto m = std::min_element(std::begin(*cookies_), std::end(*cookies_),
                                    [](const std::unique_ptr<Cookie>& lhs,
                                       const std::unique_ptr<Cookie>& rhs)
                                    {
                                      return lhs->getLastAccessTime() <
                                      rhs->getLastAccessTime();
                                    });
          *m = std::move(cookie);
        } else {
          cookies_->push_back(std::move(cookie));
        }
      } else {
        cookies_->push_back(std::move(cookie));
      }
      return true;
    }
  } else if(cookie->isExpired(now)) {
    cookies_->erase(i);
    return false;
  } else {
    cookie->setCreationTime((*i)->getCreationTime());
    *i = std::move(cookie);
    return true;
  }
}

bool DomainNode::contains(const Cookie& cookie) const
{
  if(cookies_) {
    for(auto& i : *cookies_) {
      if(*i == cookie) {
        return true;
      }
    }
  }
  return false;
}

bool DomainNode::writeCookie(BufferedFile& fp) const
{
  if(cookies_) {
    for(const auto& c : *cookies_) {
      std::string data = c->toNsCookieFormat();
      data += "\n";
      if(fp.write(data.data(), data.size()) != data.size()) {
        return false;
      }
    }
  }
  return true;
}

size_t DomainNode::countCookie() const
{
  if(cookies_) {
    return cookies_->size();
  } else {
    return 0;
  }
}

void DomainNode::clearCookie()
{
  cookies_->clear();
}

void DomainNode::setLastAccessTime(time_t lastAccessTime)
{
  lastAccessTime_ = lastAccessTime;
}

time_t DomainNode::getLastAccessTime() const
{
  return lastAccessTime_;
}

void DomainNode::setLruAccessTime(time_t t)
{
  lruAccessTime_ = t;
}

time_t DomainNode::getLruAccessTime() const
{
  return lruAccessTime_;
}

bool DomainNode::empty() const
{
  return !cookies_ || cookies_->empty();
}

bool DomainNode::hasNext() const
{
  return !next_.empty();
}

DomainNode* DomainNode::getParent() const
{
  return parent_;
}

void DomainNode::removeNode(DomainNode* node)
{
  next_.erase(node->getLabel());
}

DomainNode* DomainNode::findNext(const std::string& label) const
{
  auto i = next_.find(label);
  if(i == std::end(next_)) {
    return nullptr;
  } else {
    return (*i).second.get();
  }
}

DomainNode* DomainNode::addNext(std::string label,
                                std::unique_ptr<DomainNode> node)
{
  auto& res = next_[std::move(label)] = std::move(node);
  return res.get();
}

const std::string& DomainNode::getLabel() const
{
  return label_;
}

bool DomainNode::getInLru() const
{
  return inLru_;
}

void DomainNode::setInLru(bool f)
{
  inLru_ = f;
}

CookieStorage::CookieStorage()
  : rootNode_{make_unique<DomainNode>("", nullptr)}
{}

namespace {
// See CookieStorageTest::testDomainIsFull() in CookieStorageTest.cc
const size_t DOMAIN_EVICTION_TRIGGER = 2000;

const double DOMAIN_EVICTION_RATE = 0.1;
} // namespace

namespace {
std::vector<std::string> splitDomainLabel(const std::string& domain)
{
  auto labels = std::vector<std::string>{};
  if(util::isNumericHost(domain)) {
    labels.push_back(domain);
  } else {
    util::split(std::begin(domain), std::end(domain),
                std::back_inserter(labels), '.');
  }
  return labels;
}
} // namespace


size_t CookieStorage::getLruTrackerSize() const
{
  return lruTracker_.size();
}

void CookieStorage::evictNode(size_t delnum)
{
  for(; delnum > 0 && !lruTracker_.empty(); --delnum) {
    auto node = (*lruTracker_.begin()).second;
    lruTracker_.erase(lruTracker_.begin());
    node->setInLru(false);
    node->clearCookie();
    while(node->empty() && !node->hasNext()) {
      auto parent = node->getParent();
      parent->removeNode(node);
      if(!parent->empty() || parent->hasNext() ||
         parent == rootNode_.get()) {
        break;
      }
      node = parent;
      if(node->getInLru()) {
        lruTracker_.erase({node->getLruAccessTime(), node});
        node->setInLru(false);
      }
    }
  }
}

const DomainNode* CookieStorage::getRootNode() const
{
  return rootNode_.get();
}

bool CookieStorage::store(std::unique_ptr<Cookie> cookie, time_t now)
{
  if(lruTracker_.size() >= DOMAIN_EVICTION_TRIGGER) {
    auto delnum = size_t(lruTracker_.size()*DOMAIN_EVICTION_RATE);
    evictNode(delnum);
  }
  auto labels = splitDomainLabel(cookie->getDomain());
  auto node = rootNode_.get();
  for(auto i = labels.rbegin(), eoi = labels.rend(); i != eoi; ++i) {
    auto nextNode = node->findNext(*i);
    if(nextNode) {
      node = nextNode;
    } else {
      node = node->addNext(*i, make_unique<DomainNode>(*i, node));
    }
  }
  bool ok = node->addCookie(std::move(cookie), now);
  if(ok) {
    updateLru(node, now);
  }
  return ok;
}

void CookieStorage::updateLru(DomainNode* node, time_t now)
{
  if(node->getInLru()) {
    lruTracker_.erase({node->getLruAccessTime(), node});
  } else {
    node->setInLru(true);
  }
  node->setLruAccessTime(now);
  lruTracker_.insert({node->getLruAccessTime(), node});
}

bool CookieStorage::parseAndStore
(const std::string& setCookieString,
 const std::string& requestHost,
 const std::string& defaultPath,
 time_t now)
{
  auto cookie = cookie::parse(setCookieString, requestHost, defaultPath, now);
  return cookie && store(std::move(cookie), now);
}

namespace {
struct CookiePathDivider {
  const Cookie* cookie_;
  int pathDepth_;
  CookiePathDivider(const Cookie* cookie):cookie_(cookie), pathDepth_(0)
  {
    const std::string& path = cookie_->getPath();
    if(!path.empty()) {
      for(size_t i = 1, len = path.size(); i < len; ++i) {
        if(path[i] == '/' && path[i-1] != '/') {
          ++pathDepth_;
        }
      }
      if(path[path.size()-1] != '/') {
        ++pathDepth_;
      }
    }
  }
};
} // namespace

namespace {
class CookiePathDividerConverter {
public:
  CookiePathDivider operator()(const Cookie* cookie) const
  {
    return CookiePathDivider(cookie);
  }

  const Cookie* operator()(const CookiePathDivider& cookiePathDivider) const
  {
    return cookiePathDivider.cookie_;
  }
};
} // namespace

namespace {
class OrderByPathDepthDesc:public std::binary_function<Cookie, Cookie, bool> {
public:
  bool operator()
  (const CookiePathDivider& lhs, const CookiePathDivider& rhs) const
  {
    // From http://tools.ietf.org/html/rfc6265#section-5.4:
    // 2.  The user agent SHOULD sort the cookie-list in the following
    //    order:
    //
    //    *  Cookies with longer paths are listed before cookies with
    //       shorter paths.
    //
    //    *  Among cookies that have equal-length path fields, cookies with
    //       earlier creation-times are listed before cookies with later
    //       creation-times.
    return lhs.pathDepth_ > rhs.pathDepth_ ||
      (!(rhs.pathDepth_ > lhs.pathDepth_) &&
       lhs.cookie_->getCreationTime() < rhs.cookie_->getCreationTime());
  }
};
} // namespace

namespace {
DomainNode* findNode(const std::string& domain, DomainNode* node)
{
  auto labels = splitDomainLabel(domain);
  for(auto i = labels.rbegin(), eoi = labels.rend(); i != eoi && node; ++i) {
    node = node->findNext(*i);
  }
  return node;
}
} // namespace

bool CookieStorage::contains(const Cookie& cookie) const
{
  auto node = findNode(cookie.getDomain(), rootNode_.get());
  return node && node->contains(cookie);
}

std::vector<const Cookie*> CookieStorage::criteriaFind
(const std::string& requestHost,
 const std::string& requestPath,
 time_t now,
 bool secure)
{
  auto res = std::vector<const Cookie*>{};
  if(requestPath.empty()) {
    return res;
  }
  auto labels = splitDomainLabel(requestHost);
  auto node = rootNode_.get();
  for (auto i = labels.rbegin(), eoi = labels.rend(); i != eoi; ++i) {
    auto nextNode = node->findNext(*i);
    if(!nextNode) {
      break;
    }
    nextNode->setLastAccessTime(now);
    if(nextNode->getInLru()) {
      updateLru(nextNode, now);
    }
    nextNode->findCookie(res, requestHost, requestPath, now, secure);
    node = nextNode;
  }
  auto divs = std::vector<CookiePathDivider>{};
  std::transform(std::begin(res), std::end(res), std::back_inserter(divs),
                 CookiePathDividerConverter{});
  std::sort(std::begin(divs), std::end(divs), OrderByPathDepthDesc{});
  std::transform(std::begin(divs), std::end(divs), std::begin(res),
                 CookiePathDividerConverter{});
  return res;
}

size_t CookieStorage::size() const
{
  size_t n = 0;
  for(auto& p : lruTracker_) {
    n += p.second->countCookie();
  }
  return n;
}

bool CookieStorage::load(const std::string& filename, time_t now)
{
  char header[16]; // "SQLite format 3" plus \0
  size_t headlen;
  {
    BufferedFile fp{filename.c_str(), BufferedFile::READ};
    if(!fp) {
      A2_LOG_ERROR(fmt("Failed to open cookie file %s", filename.c_str()));
      return false;
    }
    headlen = fp.read(header, sizeof(header));
  }
  try {
    if(headlen == 16 && memcmp(header, "SQLite format 3\0", 16) == 0) {
#ifdef HAVE_SQLITE3
      try {
        auto cookies = Sqlite3MozCookieParser(filename).parse();
        storeCookies(std::make_move_iterator(std::begin(cookies)),
                     std::make_move_iterator(std::end(cookies)), now);
      } catch(RecoverableException& e) {
        A2_LOG_INFO_EX(EX_EXCEPTION_CAUGHT, e);
        A2_LOG_INFO("This does not look like Firefox3 cookie file."
                    " Retrying, assuming it is Chromium cookie file.");
        // Try chrome cookie format
        auto cookies = Sqlite3ChromiumCookieParser(filename).parse();
        storeCookies(std::make_move_iterator(std::begin(cookies)),
                     std::make_move_iterator(std::end(cookies)), now);
      }
#else // !HAVE_SQLITE3
      throw DL_ABORT_EX
        ("Cannot read SQLite3 database because SQLite3 support is disabled by"
         " configuration.");
#endif // !HAVE_SQLITE3
    } else {
      auto cookies = NsCookieParser().parse(filename, now);
      storeCookies(std::make_move_iterator(std::begin(cookies)),
                   std::make_move_iterator(std::end(cookies)), now);
    }
    return true;
  } catch(RecoverableException& e) {
    A2_LOG_ERROR(fmt("Failed to load cookies from %s", filename.c_str()));
    return false;
  }
}

bool CookieStorage::saveNsFormat(const std::string& filename)
{
  auto tempfilename = filename;
  tempfilename += "__temp";
  {
    BufferedFile fp{tempfilename.c_str(), BufferedFile::WRITE};
    if(!fp) {
      A2_LOG_ERROR(fmt("Cannot create cookie file %s", filename.c_str()));
      return false;
    }
    for(auto& p : lruTracker_) {
      if(!p.second->writeCookie(fp)) {
        A2_LOG_ERROR(fmt("Failed to save cookies to %s", filename.c_str()));
        return false;
      }
    }
    if(fp.close() == EOF) {
      A2_LOG_ERROR(fmt("Failed to save cookies to %s", filename.c_str()));
      return false;
    }
  }
  if(File(tempfilename).renameTo(filename)) {
    return true;
  } else {
    A2_LOG_ERROR(fmt("Could not rename file %s as %s",
                     tempfilename.c_str(),
                     filename.c_str()));
    return false;
  }
}

} // namespace aria2
