/*
 *  Copyright (C) 2005-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "CircularCache.h"

#include "threads/SystemClock.h"
#include "utils/log.h"

#include <algorithm>
#include <mutex>
#include <string.h>

using namespace XFILE;
using namespace std::chrono_literals;

CCircularCache::CCircularCache(size_t front, size_t back)
 : CCacheStrategy()
 , m_beg(0)
 , m_end(0)
 , m_cur(0)
 , m_buf(NULL)
 , m_size(front + back)
 , m_size_back(back)
#ifdef TARGET_WINDOWS
 , m_handle(NULL)
#endif
{
}

CCircularCache::~CCircularCache()
{
  Close();
}

int CCircularCache::Open()
{
#ifdef TARGET_WINDOWS
  m_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, m_size, NULL);
  if(m_handle == NULL)
    return CACHE_RC_ERROR;
  m_buf = (uint8_t*)MapViewOfFile(m_handle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
#else
  m_buf = new uint8_t[m_size];
#endif
  if (m_buf == NULL)
    return CACHE_RC_ERROR;
  m_beg = 0;
  m_end = 0;
  m_cur = 0;
  return CACHE_RC_OK;
}

void CCircularCache::Close()
{
#ifdef TARGET_WINDOWS
  if (m_buf != NULL)
    UnmapViewOfFile(m_buf);
  if (m_handle != NULL)
    CloseHandle(m_handle);
  m_handle = NULL;
#else
  delete[] m_buf;
#endif
  m_buf = NULL;
}

size_t CCircularCache::GetMaxWriteSize(const size_t& iRequestSize)
{
  std::unique_lock<CCriticalSection> lock(m_sync);

  size_t back  = (size_t)(m_cur - m_beg); // Backbuffer size
  size_t front = (size_t)(m_end - m_cur); // Frontbuffer size
  size_t limit = m_size - std::min(back, m_size_back) - front;

  // Never return more than limit and size requested by caller
  return std::min(iRequestSize, limit);
}

/**
 * Function will write to m_buf at m_end % m_size location
 * it will write at maximum m_size, but it will only write
 * as much it can without wrapping around in the buffer
 *
 * It will always leave m_size_back of the backbuffer intact
 * but if the back buffer is less than that, that space is
 * usable to write.
 *
 * If back buffer is filled to an larger extent than
 * m_size_back, it will allow it to be overwritten
 * until only m_size_back data remains.
 *
 * The following always apply:
 *  * m_end <= m_cur <= m_end
 *  * m_end - m_beg <= m_size
 *
 * Multiple calls may be needed to fill buffer completely.
 */
int CCircularCache::WriteToCache(const char *buf, size_t len)
{
  std::unique_lock<CCriticalSection> lock(m_sync);

  // where are we in the buffer
  size_t pos   = m_end % m_size;
  size_t back  = (size_t)(m_cur - m_beg);
  size_t front = (size_t)(m_end - m_cur);

  size_t limit = m_size - std::min(back, m_size_back) - front;
  size_t wrap  = m_size - pos;

  // limit by max forward size
  if(len > limit)
    len = limit;

  // limit to wrap point
  if(len > wrap)
    len = wrap;

  if(len == 0)
    return 0;

  if (m_buf == NULL)
    return 0;

  // write the data
  memcpy(m_buf + pos, buf, len);
  m_end += len;

  // drop history that was overwritten
  if(m_end - m_beg > (int64_t)m_size)
    m_beg = m_end - m_size;

  m_written.Set();

  return len;
}

/**
 * Reads data from cache. Will only read up till
 * the buffer wrap point. So multiple calls
 * may be needed to empty the whole cache
 */
int CCircularCache::ReadFromCache(char *buf, size_t len)
{
  std::unique_lock<CCriticalSection> lock(m_sync);

  size_t pos   = m_cur % m_size;
  size_t front = (size_t)(m_end - m_cur);
  size_t avail = std::min(m_size - pos, front);

  if(avail == 0)
  {
    if(IsEndOfInput())
      return 0;
    else
      return CACHE_RC_WOULD_BLOCK;
  }

  if(len > avail)
    len = avail;

  if(len == 0)
    return 0;

  if (m_buf == NULL)
    return 0;

  memcpy(buf, m_buf + pos, len);
  m_cur += len;

  m_space.Set();

  return len;
}

/* Wait "millis" milliseconds for "minimum" amount of data to come in.
 * Note that caller needs to make sure there's sufficient space in the forward
 * buffer for "minimum" bytes else we may block the full timeout time
 */
int64_t CCircularCache::WaitForData(uint32_t minimum, std::chrono::milliseconds timeout)
{
  std::unique_lock<CCriticalSection> lock(m_sync);
  int64_t avail = m_end - m_cur;

  if (timeout == 0ms || IsEndOfInput())
    return avail;

  if(minimum > m_size - m_size_back)
    minimum = m_size - m_size_back;

  XbmcThreads::EndTime<> endtime{timeout};
  while (!IsEndOfInput() && avail < minimum && !endtime.IsTimePast() )
  {
    lock.unlock();
    m_written.Wait(50ms); // may miss the deadline. shouldn't be a problem.
    lock.lock();
    avail = m_end - m_cur;
  }

  return avail;
}

int64_t CCircularCache::Seek(int64_t pos)
{
  std::unique_lock<CCriticalSection> lock(m_sync);

  // if seek is a bit over what we have, try to wait a few seconds for the data to be available.
  // we try to avoid a (heavy) seek on the source
  if (pos >= m_end && pos < m_end + 100000)
  {
    /* Make everything in the cache (back & forward) back-cache, to make sure
     * there's sufficient forward space. Increasing it with only 100000 may not be
     * sufficient due to variable filesystem chunksize
     */
    m_cur = m_end;

    lock.unlock();
    WaitForData((size_t)(pos - m_cur), 5s);
    lock.lock();

    if (pos < m_beg || pos > m_end)
      CLog::Log(LOGDEBUG,
                "CCircularCache::{} - ({}) Wait for data failed for pos {}, ended up at {}",
                __FUNCTION__, fmt::ptr(this), pos, m_cur);
  }

  if (pos >= m_beg && pos <= m_end)
  {
    m_cur = pos;
    return pos;
  }

  return CACHE_RC_ERROR;
}

bool CCircularCache::Reset(int64_t pos)
{
  std::unique_lock<CCriticalSection> lock(m_sync);
  if (IsCachedPosition(pos))
  {
    m_cur = pos;
    return false;
  }
  m_end = pos;
  m_beg = pos;
  m_cur = pos;

  return true;
}

int64_t CCircularCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition)
{
  if (IsCachedPosition(iFilePosition))
    return m_end;
  return iFilePosition;
}

int64_t CCircularCache::CachedDataStartPos()
{
  return m_beg;
}

int64_t CCircularCache::CachedDataEndPos()
{
  return m_end;
}

bool CCircularCache::IsCachedPosition(int64_t iFilePosition)
{
  return iFilePosition >= m_beg && iFilePosition <= m_end;
}

CCacheStrategy *CCircularCache::CreateNew()
{
  return new CCircularCache(m_size - m_size_back, m_size_back);
}

