#include "Cache.h"
#include <cpuinfo.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>

#include <iomanip>
#include <iostream>
#include <cstdio>

using namespace std;

// for smooth playing from CDROM with lower speed (setcd -x 4)
#define BUFFERING_SIZE 200

// how many bytes we will reuse when its possible
#define CACHE_BUFFER_REUSABLE 8196

// limit the maximum size of each prefetched stream in bytes
#define STREAM_SIZE_LIMIT 300000

#define __MODULE__ "Cache AVI"

#define Debug if(0)

struct req
{
    unsigned int id;//number of stream
    unsigned int position;//position in stream
    int64_t offset;
    unsigned int size;
    unsigned int alloc;
    unsigned char* memory;

    req() : alloc(0), memory(0) {}
    ~req() { delete memory; }

    unsigned char* getMemory()
    {
	if (alloc > CACHE_BUFFER_REUSABLE && (alloc - size) > 5000)
	{
	    // free large chunks
	    //cout << "freebuf " << alloc << "  m:" << (void *) memory << endl;
	    delete memory;
	    memory = 0;
	    alloc = 0;
	}

	// if our buffer could hold our data we will reuse old buffer
	if (size > alloc)
	{
	    // otherwice we will reallocate this block of memory
	    // rounding to the nearest 1KB boundary
	    delete memory;
	    alloc = (size + 0x3ff) & 0x7FFFFC00;
	    if (alloc > 1000000)
		printf("too large cache chunk: %d ??", alloc);
	    memory = new unsigned char[alloc];
	}

	return memory;
    }
};

//
// think twice before you try to change anything in this file
// all the mutex locks "should" be right in place
//

Cache::Cache(size_t size)
    :m_uiSize(size), m_bQuit(false), m_iFd(-1),
    cache_access(0), cache_right(0), cache_miss(0)
{
    pthread_mutex_init(&mutex_in, 0);
    pthread_mutex_init(&mutex_out, 0);

    int result = pthread_cond_init(&cond_in, NULL);
    if (result != 0)
	perror("Creating cond_in");

    result = pthread_cond_init(&cond_out, NULL);
    if (result != 0)
	perror("Creating cond_out");
}

Cache::~Cache()
{
    m_bQuit = true;

    if (m_iFd > 0)
    {
        clear();
	pthread_cond_broadcast(&cond_out);

	if (pthread_join(thread, NULL))
	    perror("Cache::~Cache pthread_join");

	for (unsigned i = 0; i < m_streams.size(); i++)
	{
	    stream_entry& stream = m_streams[i];

	    while (stream.freebuffers.size() > 0)
	    {
		req* r = stream.freebuffers.back();
		stream.freebuffers.pop_back();
		delete r;
	    }
	    while (stream.buffers.size() > 0)
	    {
		req* r = stream.buffers.back();
		stream.buffers.pop_back();
		delete r;
	    }
	}

        delete loaded;
    }

    pthread_cond_destroy(&cond_in);
    pthread_cond_destroy(&cond_out);
    pthread_mutex_destroy(&mutex_in);
    pthread_mutex_destroy(&mutex_out);

    if (cache_access != 0)
	printf("Destroying cache. Total accesses %d, hits %.2f%%, misses %.2f%%, errors %.2f%%\n",
	       cache_access, 100. * double(cache_right - cache_miss) / cache_access,
	       100. * double(cache_miss) / cache_access,
	       100. * double(cache_access - cache_right) / cache_access);
}

int Cache::addStream(Unsigned id, const vector<AVIINDEXENTRY2> &table)
{
    cout << "Cache: Adding stream " << id << ", "
	<< table.size() << " chunks" << endl;

    stream_entry se(table, 0);
    while (se.freebuffers.size() < m_uiSize)
        se.freebuffers.push_back(new req());

    pthread_mutex_lock(&mutex_out);
    m_streams.push_back(se);
    pthread_mutex_unlock(&mutex_out);

    pthread_mutex_lock(&mutex_in);
    pthread_cond_broadcast(&cond_in);
    pthread_mutex_unlock(&mutex_in);

    return 0;
}

// starts caching thread once we know file descriptor
int Cache::create(int fd)
{
    m_iFd = fd;
    cout << "Cache:   Creating cache for file descriptor: " << m_iFd << endl;
    if (m_streams.size() > 0)
    {
	int result = pthread_create(&thread, 0, &startReadfuncThread, this);
	if (result != 0)
	    perror("Creating thread");
    }
    else
	cout << "Cache: WARNING no stream for caching! " << endl;

    return 0;
}

// return true if this stream should be waken and read new data
inline bool Cache::isCachable(stream_entry& stream, streamid_t id)
{
    // trick to allow precaching even very large image sizes
    return ((stream.sum < STREAM_SIZE_LIMIT
	     // assuming id=0 is video stream
             // uncompressed video could be really huge!
	     || ((id == 0) && stream.buffers.size() < 10))
	    && stream.last < stream.table->size()
	    && stream.freebuffers.size() > 0
	    && (stream.filling
                || (stream.sum < STREAM_SIZE_LIMIT/2
		    && stream.buffers.size() < m_uiSize/2)));
}

//
// currently preffered picking alghorithm
//
// seems to be having good seek strategy
unsigned int Cache::pickChunk()
{
    streamid_t id = m_uiId;

    do
    {
	stream_entry& stream = m_streams[id];

	// determine next needed chunk in this stream
	stream.last = (stream.buffers.size() > 0) ?
	    stream.buffers.back()->position + 1 : stream.position;

	if (isCachable(stream, id))
	    return id;

	// try next channel from streams
	id++;
	if (id >= m_streams.size())
	    id = 0; // wrap around

    }
    while (id != m_uiId);

    return WAIT; // nothing for caching found
}

// caching thread
void* Cache::readfunc()
{
    bool changed = false;

    m_uiId = 0;
    pthread_mutex_lock(&mutex_out);

    while (!m_bQuit)
    {
        m_uiId = pickChunk();

	if (m_uiId == WAIT)
	{
	    m_uiId = 0;
	    pthread_mutex_lock(&mutex_in);
	    pthread_mutex_unlock(&mutex_out);

	    // one could be trying to send signal to this thread
	    Debug cout << "full buffers - waiting for read"
		<< " s#1: " << m_streams[0].buffers.size()
		<< " s#2: " << m_streams[1].buffers.size()
		<< endl;
	    pthread_cond_wait(&cond_in, &mutex_in);
	    Debug cout << "full buffers - waiting done" << endl;
	    //cout << "full buffers - waiting done" << endl;
	    pthread_mutex_unlock(&mutex_in);
	    pthread_mutex_lock(&mutex_out);
            continue;
	}

	stream_entry& stream = m_streams[m_uiId];

        // get free buffer
	loaded = stream.freebuffers.back();
	stream.freebuffers.pop_back();

	loaded->id = m_uiId;
	loaded->position = stream.last;
	loaded->offset = (*stream.table)[stream.last].qwChunkOffset;
	loaded->size = (*stream.table)[stream.last].dwChunkLength + 8;

	Debug cout << "id: " << m_uiId
	    << " buffered: " << (double)(stream.last - stream.position)
	    << " sum: " << m_streams[0].sum << " - " << m_streams[1].sum << endl;

	pthread_mutex_unlock(&mutex_out);

	//cout << "BUFFER Waiting Id:" << loaded->id
	//    << " Pos:" << loaded->position
	//    << " seek:" << loaded->offset << endl;
	off_t scur = lseek64(m_iFd, 0, SEEK_CUR);
	Debug cout << setw(1) << loaded->id
	    << " pos:" << setw(6) << loaded->position
	    << " seek:" << setw(7) << loaded->offset
	    << " df:" << setw(8) << loaded->offset - scur
	    << " lsz" << setw(6) << loaded->size
	    << " sum:" << setw(7) << stream.sum
	    << " buf:" << setw(3) << stream.buffers.size()
	    << endl;

	lseek64(m_iFd, loaded->offset & 0xFFFFFFFFFFFFLL, SEEK_SET);
	int r = ::read(m_iFd, loaded->getMemory(), loaded->size & 0x7FFFFFFF);

	//cout << "BUFFER Read    Id:" << loaded->id
	//    << " Pos:" << loaded->position
	//    << " size:" << loaded->size << "  readed: " << r << endl;
	pthread_mutex_lock(&mutex_out);

        // check if we still want same buffer
	stream.last = (stream.buffers.size() > 0) ?
	    stream.buffers.back()->position + 1 : stream.position;

	if (stream.last == loaded->position)
	{
	    stream.buffers.push_back(loaded);
	    stream.sum += loaded->size;
	    stream.filling = !(stream.sum > STREAM_SIZE_LIMIT);
	    Debug cout << "----------------      "
                << " id: " << loaded->id
		<< " pos: " << loaded->position
		<< " sum: " << stream.sum << "   size: " << loaded->size
		<< " filling: " << stream.filling << endl;
	}
	else
	{
	    Debug cout << " CHANGED LAST!!!" << endl;
	    stream.freebuffers.push_back(loaded);
	}
        loaded = 0;

	// anounce update
	pthread_cond_broadcast(&cond_out);
    }

    pthread_mutex_unlock(&mutex_out);

    return 0;
}

// called by stream reader - most of the time this read should
// be satisified from already precached chunks
int Cache::read(void *buffer, streamid_t id, framepos_t position, size_t size, Unsigned in_pos)
{
    //printf("Cache: read(buffer %x, id %d, pos %d, size %d, in_pos %d)\n",
    //       buffer, id, position, size, in_pos);
    cache_access++;

    if (id >= m_streams.size())
	return E_ERROR;

    stream_entry& stream = m_streams[id];
    if (position >= stream.table->size())
	return E_ERROR;

    //cout << "Before " << endl;
    pthread_mutex_lock(&mutex_out);

    stream.position = position;

    //cout << "Wait: " << id << " for " << position << endl;
    //while (stream.actual != position || stream.buffers.size() == 0)
    req* r = 0;

    while (!m_bQuit)
    {
	if (stream.buffers.size() > 0)
	{
	    r = stream.buffers.front();
	    if (r->position == position)
	    {
		cache_right++;
                break;
	    }

	    Debug cout << "position " << r->position << "   want: " << position << endl;
            // remove this chunk
            stream.buffers.pop_front();
	    stream.freebuffers.push_back(r);
            stream.sum -= r->size;
            continue;
	}
	cache_miss++;

	pthread_mutex_lock(&mutex_in);
	pthread_cond_broadcast(&cond_in);
	pthread_mutex_unlock(&mutex_in);

	m_uiId = id;

	Debug cout << "--- actual: " << id << " size: " << stream.buffers.size() << endl;
	pthread_cond_wait(&cond_out, &mutex_out);
	Debug cout << "--- actual: " << id << "  done - size:"
	    << stream.buffers.size() << endl;
    }

    pthread_mutex_lock(&mutex_in);
    pthread_cond_broadcast(&cond_in);
    pthread_mutex_unlock(&mutex_in);

    Debug cout << "id: " << id << " bsize: " << stream.buffers.size()
	<< "  memory: " << (void*) r->memory << "  pp:" << r->position << endl;

    fourcc_t chunk_id = *(fourcc_t*) r->memory;

    if (StreamFromFOURCC(chunk_id) != id)
	cout << "WARNING: Read(): FOURCC mismatch ( received " << hex
            << chunk_id << dec << " )" << endl;

    //if (size > max_size - in_pos - 8)
    //	size = max_size - in_pos - 8;

    memcpy(buffer, r->memory + in_pos + 8, size);

    pthread_mutex_unlock(&mutex_out);

    return size;
}

int Cache::clear()
{
    Debug cout << "*** CACHE CLEAR ***" << endl;

    pthread_mutex_lock(&mutex_out);
    m_uiId = 0;
    pthread_mutex_unlock(&mutex_out);

    pthread_mutex_lock(&mutex_in);
    pthread_cond_broadcast(&cond_in);
    pthread_mutex_unlock(&mutex_in);

#ifndef QUIET
    // DumpBuffers("Cache::Clear", false);
#endif
    return 0;
}

double Cache::getSize()
{
    /*
       pthread_mutex_lock(&mutex_out);
       int status=0;
       for(int i=0; i<m_uiSize; i++)
       if(req_buf[i].st==req::BUFFER_READY)status++;
       pthread_mutex_unlock(&mutex_out);
       return (double)status/m_uiSize;
     */
    return 1.;
}

void* Cache::startReadfuncThread(void* arg)
{
    Cache* c = (Cache*)arg;
    return c->readfunc();
}


void Cache::dumpBuffers(const char *txt, bool show_table)
{

}

// pick next chunk for precaching - good alghorithm here could improve
// load speed - however good strategy is not the obvisious
// this is test of different alghorithm
unsigned int Cache::pickChunk1()
{
    unsigned didx = ~0U;
    int64_t dmin = 0;
    int64_t coffset = lseek64(m_iFd, 0, SEEK_CUR);

    unsigned i;

    // pick some initial stream which could be filled
    for (i = 0; i < m_streams.size(); i++)
    {
	stream_entry& stream = m_streams[i];

	// determine next needed chunk in this stream
	stream.last = (stream.buffers.size() > 0) ?
	    stream.buffers.back()->position + 1 : stream.position;

	//printf("id: %d  sum: %d     last: %d   tsize: %d  fsize: %d\n",
	//       i, stream.sum, stream.last, stream.table->size(),
	//       stream.freebuffers.size());
	if (stream.sum < STREAM_SIZE_LIMIT
	    && stream.last < stream.table->size()
	    && stream.freebuffers.size() > 0)
	{
	    didx = i;
	    dmin = (*stream.table)[stream.last].qwChunkOffset;

	    //cout << i <<  "  LAST " << stream.last << " coffset " << coffset << "  dmin: " << dmin << endl;
	    break;
	}
    }

    // pick channel with smallest positive offset
    // -> nearest chunk in the stream flow
    for (i++; i < m_streams.size(); i++)
    {
	stream_entry& stream = m_streams[i];

	// determine next needed chunk in this stream
	stream.last = (stream.buffers.size() > 0) ?
	    stream.buffers.back()->position + 1 : stream.position;

	//printf("id: %d  sum: %d     last: %d   tsize: %d  fsize: %d\n",
	//       i, stream.sum, stream.last, stream.table->size(),
	//       stream.freebuffers.size());
	if (stream.sum > STREAM_SIZE_LIMIT
	    || stream.last >= stream.table->size()
	    || stream.freebuffers.size() == 0)
	    continue;

	int64_t d = (*stream.table)[stream.last].qwChunkOffset;
	if (d < dmin)
	{
	    //cout << i <<  "  XLAST " << stream.last << " newdmin " << dmin << endl;
	    dmin = d;
	    didx = i;
	}
    }

    return didx;
}

/*************************************************************/

InputStream::InputStream(const char *pszFile) throw(FatalError)
    :cache(new Cache(BUFFERING_SIZE))
{
    m_iFd = open(pszFile, O_RDONLY);
    if (m_iFd < 0)
	throw FATAL("Could not open file");
    unsigned int spos = lseek64(m_iFd, 0, SEEK_CUR);

    ulLength = lseek64(m_iFd, 0, SEEK_END);
    seek(spos);
}

InputStream::~InputStream()
{
    delete cache;

    if (m_iFd >= 0)
	close(m_iFd);
}

int InputStream::read(void *buffer, size_t size)
{
    int ans = ::read(m_iFd, buffer, size);

    return ans;
}

offset_t InputStream::seek(int64_t offset)
{
    return lseek64(m_iFd, offset, SEEK_SET);
}

offset_t InputStream::seekCur(int64_t offset)
{
    return lseek64(m_iFd, offset, SEEK_CUR);
}

offset_t InputStream::pos()
{
    return lseek64(m_iFd, 0, SEEK_CUR);
}
