/* Copyright (C) 2019 MariaDB Corporation

   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; version 2 of
   the License.

   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. */

#include "ThreadPool.h"
#include <iostream>

using namespace std;

namespace storagemanager
{
ThreadPool::ThreadPool() : maxThreads(1000), die(false), processQueueOnExit(true), threadsWaiting(0)
{
  // Using this ctor effectively limits the # of threads here to the natural limit of the
  // context it's used in.  In the CRP class for example, the # of active threads would be
  // naturally limited by the # of concurrent operations.
  logger = SMLogging::get();
  pruner = boost::thread([this] { this->prune(); });
}

ThreadPool::ThreadPool(uint num_threads, bool _processQueueOnExit)
 : maxThreads(num_threads), die(false), processQueueOnExit(_processQueueOnExit), threadsWaiting(0)
{
  logger = SMLogging::get();
  pruner = boost::thread([this] { this->prune(); });
}

ThreadPool::~ThreadPool()
{
  boost::unique_lock<boost::mutex> s(mutex);
  die = true;
  if (!processQueueOnExit)
    jobs.clear();

  jobAvailable.notify_all();
  s.unlock();

  threads.join_all();
  pruner.interrupt();
  pruner.join();
}

void ThreadPool::setName(const string& _name)
{
  name = _name;
}

void ThreadPool::addJob(const boost::shared_ptr<Job>& j)
{
  boost::unique_lock<boost::mutex> s(mutex);
  if (die)
    return;

  jobs.push_back(j);
  // Start another thread if necessary
  if (threadsWaiting == 0 && (threads.size() - pruneable.size()) < maxThreads)
  {
    boost::thread* thread = threads.create_thread([this] { this->processingLoop(); });
    s_threads.insert(thread);
    // if (name == "Downloader")
    //    cout << "Threadpool (" << name << "): created a new thread.  live threads = " <<
    //        threads.size() - pruneable.size() << " job count = " << jobs.size() << endl;
  }
  else
    jobAvailable.notify_one();
}

void ThreadPool::prune()
{
  boost::unique_lock<boost::mutex> s(mutex);

  while (1)
  {
    while (pruneable.empty() && !die)
      somethingToPrune.wait(s);

    if (die)
      break;

    for (auto& id : pruneable)
    {
      auto it = s_threads.find(id);
      assert(it != s_threads.end());
      it->thrd->join();
      threads.remove_thread(it->thrd);
      s_threads.erase(it);
    }
    pruneable.clear();
  }
}

void ThreadPool::setMaxThreads(uint newMax)
{
  boost::unique_lock<boost::mutex> s(mutex);
  maxThreads = newMax;
}

int ThreadPool::currentQueueSize() const
{
  boost::unique_lock<boost::mutex> s(mutex);
  return jobs.size();
}

void ThreadPool::processingLoop()
{
  boost::unique_lock<boost::mutex> s(mutex);
  try
  {
    _processingLoop(s);
  }
  catch (...)
  {
  }
  //  _processingLoop returns with the lock held

  pruneable.push_back(boost::this_thread::get_id());
  somethingToPrune.notify_one();
  // if (name == "Downloader")
  //    cout << "threadpool " << name << " thread exiting.  jobs = " << jobs.size() <<
  //      " live thread count = " << threads.size() - pruneable.size() << endl;
}

void ThreadPool::_processingLoop(boost::unique_lock<boost::mutex>& s)
{
  // cout << "Thread started" << endl;
  while (threads.size() - pruneable.size() <=
         maxThreads)  // this is the scale-down mechanism when maxThreads is changed
  {
    while (jobs.empty() && !die)
    {
      threadsWaiting++;
      bool timedout = !jobAvailable.timed_wait<>(s, idleThreadTimeout);
      threadsWaiting--;
      if (timedout && jobs.empty())
      {
        // cout << "Thread exiting b/c of timeout" << endl;
        return;
      }
    }
    if (jobs.empty())
    {  // die was set, and there are no jobs left to process
      // cout << "Thread exiting b/c of die (?)" << endl;
      return;
    }
    boost::shared_ptr<Job> job = jobs.front();
    jobs.pop_front();

    s.unlock();
    try
    {
      (*job)();
    }
    catch (exception& e)
    {
      logger->log(LOG_CRIT, "ThreadPool (%s): caught '%s' from a job", name.c_str(), e.what());
    }
    s.lock();
  }
  // cout << "Thread exiting b/c of pruning logic" << endl;
}

inline bool ThreadPool::id_compare::operator()(const ID_Thread& t1, const ID_Thread& t2) const
{
  return t1.id < t2.id;
}

ThreadPool::ID_Thread::ID_Thread(boost::thread::id& i) : id(i)
{
}

ThreadPool::ID_Thread::ID_Thread(boost::thread* t) : id(t->get_id()), thrd(t)
{
}

}  // namespace storagemanager
