// Copyright (C) 2004-2021 Artifex Software, Inc.
//
// This file is part of MuPDF.
//
// MuPDF is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// MuPDF 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 Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
//
// Alternative licensing terms are available from the licensor.
// For commercial licensing, see <https://www.artifex.com/> or contact
// Artifex Software, Inc., 1305 Grant Avenue - Suite 200, Novato,
// CA 94945, U.S.A., +1(415)492-9861, for further information.

#include "mupdf/fitz.h"

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

#include "curl_stream.h"

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
#endif

/* File stream - progressive reading to simulate http download */

typedef struct prog_state
{
	FILE *file;
	int64_t length;
	int64_t available; /* guarded by lock below */
	int kbps;
	int64_t start_time; /* in milliseconds since epoch */
	unsigned char buffer[4096];
	void (*on_data)(void*,int);
	void *on_data_arg;

	/* We assume either Windows threads or pthreads here. */
#ifdef _WIN32
	void *thread;
	DWORD thread_id;
	HANDLE mutex;
#else
	pthread_t thread;
	pthread_mutex_t mutex;
#endif
} prog_state;

static int64_t get_current_time(void)
{
#ifdef _WIN32
	return (int)GetTickCount();
#else
	struct timeval now;
	gettimeofday(&now, NULL);
	return (int64_t)now.tv_sec*1000 + now.tv_usec/1000;
#endif
}

#ifdef _WIN32
static int locked;

static void
lock(prog_state *state)
{
	WaitForSingleObject(state->mutex, INFINITE);
	assert(locked == 0);
	locked = 1;
}

static void
unlock(prog_state *state)
{
	assert(locked == 1);
	locked = 0;
	ReleaseMutex(state->mutex);
}
#else
static void
lock(prog_state *state)
{
	pthread_mutex_lock(&state->mutex);
}

static void
unlock(prog_state *state)
{
	pthread_mutex_unlock(&state->mutex);
}
#endif

static int next_prog(fz_context *ctx, fz_stream *stm, size_t len)
{
	prog_state *ps = (prog_state *)stm->state;
	size_t n;

	if (len > sizeof(ps->buffer))
		len = sizeof(ps->buffer);

	/* Simulate not all data available yet. */
	lock(ps);
	if (ps->available < ps->length)
	{
		if (stm->pos > ps->available || ps->available - stm->pos <= 0)
		{
			unlock(ps);
			fz_throw(ctx, FZ_ERROR_TRYLATER, "Not enough data yet");
		}
		len = fz_mini(len, ps->available - stm->pos);
	}
	unlock(ps);

	n = fread(ps->buffer, 1, len, ps->file);
	stm->rp = ps->buffer;
	stm->wp = ps->buffer + n;
	stm->pos += (int64_t)n;

	if (n < len)
	{
		if (ferror(ps->file))
			fz_throw(ctx, FZ_ERROR_GENERIC, "prog read error: %s", strerror(errno));
		if (n == 0)
			return EOF;
	}
	return *stm->rp++;
}

static void seek_prog(fz_context *ctx, fz_stream *stm, int64_t offset, int whence)
{
	prog_state *ps = (prog_state *)stm->state;

	if (whence == SEEK_END)
	{
		whence = SEEK_SET;
		offset += ps->length;
	}
	else if (whence == SEEK_CUR)
	{
		whence = SEEK_SET;
		offset += stm->pos;
	}

	if (fseek(ps->file, offset, whence) != 0)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot seek: %s", strerror(errno));
	stm->pos = offset;
	stm->wp = stm->rp;
}

static void close_prog(fz_context *ctx, void *state)
{
	prog_state *ps = (prog_state *)state;
	int n = fclose(ps->file);
	if (n < 0)
		fz_warn(ctx, "cannot fclose: %s", strerror(errno));
	fz_free(ctx, state);
}

static void fetcher_thread(prog_state *ps)
{
	int complete = 0;

	ps->start_time = get_current_time();

	while (!complete)
	{
		/* Wait a while. */
#ifdef _WIN32
		Sleep(200);
#else
		usleep(200000);
#endif

		lock(ps);

		/* Simulate more data having arrived. */
		if (ps->available < ps->length)
		{
			int64_t av = (get_current_time() - ps->start_time) * ps->kbps;
			if (av > ps->length)
				av = ps->length;
			ps->available = av;
		}
		else
		{
			complete = 1;
		}

		unlock(ps);

		/* Ping callback with new data. */
		if (ps->on_data)
			ps->on_data(ps->on_data_arg, complete);
	}
}

#ifdef _WIN32
static DWORD WINAPI win_thread(void *lparam)
{
	fetcher_thread((prog_state *)lparam);
	return 0;
}
#else
static void *pthread_thread(void *arg)
{
	fetcher_thread((prog_state *)arg);
	return NULL;
}
#endif

static fz_stream *
fz_open_file_ptr_progressive(fz_context *ctx, FILE *file, int kbps, void (*on_data)(void*,int), void *opaque)
{
	fz_stream *stm;
	prog_state *state;

	state = fz_malloc_struct(ctx, prog_state);
	state->file = file;
	state->kbps = kbps;
	state->available = 0;

	state->on_data = on_data;
	state->on_data_arg = opaque;

	fseek(state->file, 0, SEEK_END);
	state->length = ftell(state->file);
	fseek(state->file, 0, SEEK_SET);

#ifdef _WIN32
	state->mutex = CreateMutex(NULL, FALSE, NULL);
	if (state->mutex == NULL)
		fz_throw(ctx, FZ_ERROR_GENERIC, "mutex creation failed");

	state->thread = CreateThread(NULL, 0, win_thread, state, 0, &state->thread_id);
	if (state->thread == NULL)
		fz_throw(ctx, FZ_ERROR_GENERIC, "thread creation failed");
#else
	if (pthread_mutex_init(&state->mutex, NULL))
		fz_throw(ctx, FZ_ERROR_GENERIC, "mutex creation failed");

	if (pthread_create(&state->thread, NULL, pthread_thread, state))
		fz_throw(ctx, FZ_ERROR_GENERIC, "thread creation failed");
#endif

	stm = fz_new_stream(ctx, state, next_prog, close_prog);
	stm->progressive = 1;
	stm->seek = seek_prog;

	return stm;
}

fz_stream *
fz_open_file_progressive(fz_context *ctx, const char *name, int kbps, void (*on_data)(void*,int), void *opaque)
{
	FILE *f;
#ifdef _WIN32
	f = fz_fopen_utf8(name, "rb");
#else
	f = fopen(name, "rb");
#endif
	if (f == NULL)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cannot open %s", name);
	return fz_open_file_ptr_progressive(ctx, f, kbps, on_data, opaque);
}
