/**
 *  Copyright 2009-2013 10gen, Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 * Author: César D. Rodas <crodas@php.net>
 */
#include <php.h>
#ifdef WIN32
# ifndef int64_t
typedef __int64 int64_t;
# endif
#endif

#include <php_globals.h>
#include <ext/standard/file.h>
#include <ext/standard/flock_compat.h>
#ifdef HAVE_SYS_FILE_H
# include <sys/file.h>
#endif

#include <zend_exceptions.h>

#include "../php_mongo.h"
#include "gridfs.h"
#include "gridfs_stream.h"
#include "../collection.h"
#include "../cursor.h"
#include "../db.h"

extern zend_class_entry *mongo_ce_BinData, *mongo_ce_GridFS;
extern zend_class_entry *mongo_ce_GridFSFile, *mongo_ce_GridFSException;

ZEND_EXTERN_MODULE_GLOBALS(mongo)

static size_t gridfs_read(php_stream *stream, char *buf, size_t count TSRMLS_DC);
static int gridfs_close(php_stream *stream, int close_handle TSRMLS_DC);
static int gridfs_stat(php_stream *stream, php_stream_statbuf *ssb TSRMLS_DC);
static int gridfs_option(php_stream *stream, int option, int value, void *ptrparam TSRMLS_DC);
static int gridfs_seek(php_stream *stream, off_t offset, int whence, off_t *newoffs TSRMLS_DC);

typedef struct _gridfs_stream_data {
	zval * fileObj; /* MongoGridFSFile Object */
	zval * chunkObj; /* Chunk collection object */
	zval * id; /* File ID  */
	zval * query; /* Query array */

	/* file current position */
	size_t offset;

	/* file size */
	int size;

	/* chunk size */
	int chunkSize;
	int totalChunks;

	/* which chunk is loaded? */
	int chunkId;


	/* mongo current chunk is kept in memory */
	unsigned char * buffer;

	/* chunk size */
	int buffer_size;

	/* where we are in the chunk? */
	size_t buffer_offset;
} gridfs_stream_data;

php_stream_ops gridfs_stream_ops = {
	NULL, /* write */
	gridfs_read, /* read */
	gridfs_close, /* close */
	NULL, /* flush */
	"gridfs-wrapper",
	gridfs_seek, /* seek */
	NULL, /* cast */
	gridfs_stat, /* stat */
	gridfs_option, /* set_option */
};

/* some handy macros {{{ */
#ifndef MIN
# define MIN(a, b) a > b ? b : a
#endif

#if 0
# define DEBUG(x)  printf x;fflush(stdout);
#else
# define DEBUG(x)
#endif

/* returns 0 on failure */
#define READ_ARRAY_PROP_PTR(dest, name, toVariable) \
	if (zend_hash_find(HASH_P(dest), name, strlen(name) + 1, (void**)&toVariable) == FAILURE) { \
		zend_throw_exception(mongo_ce_GridFSException, "couldn't find " name, 19 TSRMLS_CC); \
		return 0; \
	}

/* returns FAILURE on failure */
#define READ_ARRAY_PROP(dest, name, toVariable) \
	if (zend_hash_find(HASH_P(dest), name, strlen(name) + 1, (void**)&toVariable) == FAILURE) { \
		zend_throw_exception(mongo_ce_GridFSException, "couldn't find " name, 19 TSRMLS_CC); \
		return FAILURE; \
	}

#define READ_OBJ_PROP(type, obj, name)  \
	zend_read_property(mongo_ce_##type, obj, name, strlen(name), NOISY TSRMLS_CC);

#define TO_INT(size, len) { \
	if (Z_TYPE_PP(size) == IS_DOUBLE) { \
		len = (int)Z_DVAL_PP(size); \
	} else {  \
		len = Z_LVAL_PP(size); \
	} \
}

#define ASSERT_SIZE(size) \
	if (size > self->chunkSize) { \
		char * err; \
		spprintf(&err, 0, "chunk %d has wrong size (%d) when the max is %d", chunk_id, size, self->chunkSize); \
		zend_throw_exception(mongo_ce_GridFSException, err, 20 TSRMLS_CC); \
		zval_ptr_dtor(&chunk); \
		return FAILURE; \
	} \
/* }}} */

/* {{{ php_stream* gridfs_stream_init(zval* file_object TSRMLS_DC) */
php_stream* gridfs_stream_init(zval *file_object TSRMLS_DC)
{
	gridfs_stream_data *self;
	php_stream *stream;
	zval *file, **id, **size, **chunkSize, *gridfs;

	file = READ_OBJ_PROP(GridFSFile, file_object, "file");
	READ_ARRAY_PROP_PTR(file, "_id", id);
	READ_ARRAY_PROP_PTR(file, "length", size);
	READ_ARRAY_PROP_PTR(file, "chunkSize", chunkSize);

	gridfs = READ_OBJ_PROP(GridFSFile, file_object, "gridfs");

	/* allocate memory and init the stream resource */
	self = emalloc(sizeof(*self));
	memset(self, 0, sizeof(*self));
	TO_INT(size, self->size);
	TO_INT(chunkSize, self->chunkSize);

	self->fileObj  = file_object;

	self->chunkObj = READ_OBJ_PROP(GridFS, gridfs, "chunks");
	self->buffer   = emalloc(self->chunkSize +1);
	self->id = *id;
	self->chunkId = -1;
	self->totalChunks = ceil(self->size/self->chunkSize);

	zval_add_ref(&self->fileObj);
	zval_add_ref(&self->chunkObj);
	zval_add_ref(&self->id);


	/* create base query object */
	MAKE_STD_ZVAL(self->query);
	array_init(self->query);
	add_assoc_zval(self->query, "files_id", self->id);
	zval_add_ref(&self->id);

	stream = php_stream_alloc(&gridfs_stream_ops, self, 0, "rb");
	return stream;
}
/* }}} */

/* {{{ array fstat($fp) */
static int gridfs_stat(php_stream *stream, php_stream_statbuf *ssb TSRMLS_DC)
{
	gridfs_stream_data *self = (gridfs_stream_data *) stream->abstract;

	ssb->sb.st_size = self->size;

	return SUCCESS;
}
/* }}} */

/* {{{ int gridfs_read_chunk(gridfs_stream_data *self, int chunk_id) */
static int gridfs_read_chunk(gridfs_stream_data *self, int chunk_id TSRMLS_DC)
{
	zval *chunk = 0, **data, *bin;

	if (chunk_id == -1) {
		/* we need to figure out which chunk to load */
		chunk_id = (int)(self->offset / self->chunkSize);
	}

	if (chunk_id == self->chunkId) {
		/* nothing to load :-) */
		return SUCCESS;
	}

	DEBUG(("loading chunk %d\n", chunk_id));

	add_assoc_long(self->query, "n", chunk_id);

	MAKE_STD_ZVAL(chunk);
	MONGO_METHOD1(MongoCollection, findOne, chunk, self->chunkObj, self->query);

	if (Z_TYPE_P(chunk) == IS_NULL) {
		zval_ptr_dtor(&chunk);

		return FAILURE;
	}

	READ_ARRAY_PROP(chunk, "data", data);

	if (Z_TYPE_PP(data) == IS_STRING) {
		ASSERT_SIZE(Z_STRLEN_PP(data))
		memcpy(self->buffer, Z_STRVAL_PP(data), Z_STRLEN_PP(data));
		self->buffer_size = Z_STRLEN_PP(data);

	} else if (Z_TYPE_PP(data) == IS_OBJECT && Z_OBJCE_PP(data) == mongo_ce_BinData) {
		bin = READ_OBJ_PROP(BinData, *data, "bin");

		ASSERT_SIZE(Z_STRLEN_P(bin))
		memcpy(self->buffer, Z_STRVAL_P(bin), Z_STRLEN_P(bin));
		self->buffer_size = Z_STRLEN_P(bin);

	} else {
		zend_throw_exception(mongo_ce_GridFSException, "chunk has wrong format", 21 TSRMLS_CC);
		zval_ptr_dtor(&chunk);

		return FAILURE;
	}

	self->chunkId = chunk_id;
	self->buffer_offset = self->offset % self->chunkSize;

	zval_ptr_dtor(&chunk);
	return SUCCESS;
}
/* }}} */

/* {{{ fread($fp, $bytes) */
static size_t gridfs_read(php_stream *stream, char *buf, size_t count TSRMLS_DC)
{
	gridfs_stream_data *self = (gridfs_stream_data *) stream->abstract;
	int size, chunk_id;

	/* load the needed chunk from mongo */
	chunk_id = (int)((self->offset)/self->chunkSize);
	if (gridfs_read_chunk(self, chunk_id TSRMLS_CC) == FAILURE) {
		return -1;
	}

	size = MIN(count, self->buffer_size - self->buffer_offset % self->chunkSize);
	memcpy(buf, self->buffer + self->buffer_offset % self->chunkSize, size);

	if (size < count && chunk_id + 1 < self->totalChunks) {
		int tmp_bytes;

		/* load next chunk to return the exact requested bytes */
		if (gridfs_read_chunk(self, chunk_id + 1 TSRMLS_CC) == FAILURE) {
			return -1;
		}
		tmp_bytes = MIN(count-size, self->buffer_size);
		memcpy(buf+size, self->buffer, tmp_bytes);
		size += tmp_bytes;
	}

	self->buffer_offset += size;
	self->offset += size;

	DEBUG(("offset=%d (+%d)\n", self->offset, size));

	return size;
}
/* }}} */

/* {{{ fseek($fp, $bytes, $whence) */
static int gridfs_seek(php_stream *stream, off_t offset, int whence, off_t *newoffs TSRMLS_DC)
{
	gridfs_stream_data *self = (gridfs_stream_data *) stream->abstract;
	int newoffset = 0;

	switch (whence) {
		case SEEK_SET:
			newoffset = offset;
			break;

		case SEEK_CUR:
			newoffset = self->offset + offset;
			break;

		case SEEK_END:
			newoffset = self->size + offset;
			break;

		default:
			return FAILURE;
	}

	if (newoffset > self->size) {
		return FAILURE;
	}

	*newoffs = newoffset;
	self->offset = newoffset;

	if (self->chunkId != -1) {
		/* change the offset also in the chunk */
		self->buffer_offset = newoffset % self->chunkSize;
	}

	return SUCCESS;
}
/* }}} */

/* {{{ fclose($fp) */
static int gridfs_close(php_stream *stream, int close_handle TSRMLS_DC)
{
	gridfs_stream_data *self = (gridfs_stream_data *) stream->abstract;

	zval_ptr_dtor(&self->fileObj);
	zval_ptr_dtor(&self->chunkObj);
	zval_ptr_dtor(&self->query);
	zval_ptr_dtor(&self->id);

	efree(self->buffer);
	efree(self);

	return 0;
}
/* }}} */

/* {{{ feof */
static int gridfs_option(php_stream *stream, int option, int value, void *ptrparam TSRMLS_DC)
{

	gridfs_stream_data * self = (gridfs_stream_data *) stream->abstract;
	int ret = -1;

	switch (option) {
		case PHP_STREAM_OPTION_CHECK_LIVENESS:
			ret = self->size == self->offset ? PHP_STREAM_OPTION_RETURN_ERR : PHP_STREAM_OPTION_RETURN_OK;
			break;
	}

	return ret;
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: fdm=marker
 * vim: noet sw=4 ts=4
 */
