/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of mediastreamer2
 * (see https://gitlab.linphone.org/BC/public/mediastreamer2).
 *
 * This program 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.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <bctoolbox/defs.h>

#ifdef HAVE_CONFIG_H
#include "mediastreamer-config.h"
#endif

#include "../voip/nowebcam.h"
#include "mediastreamer2/mscommon.h"
#include "mediastreamer2/msfilter.h"
#include "mediastreamer2/msticker.h"
#include "mediastreamer2/msvideo.h"
#include "mediastreamer2/mswebcam.h"

#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif

#define FF_INPUT_BUFFER_PADDING_SIZE 32

#if LIBAVCODEC_VERSION_MAJOR >= 57

#ifdef _MSC_VER
#pragma warning(disable : 4996)
#else
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

#endif

#if TARGET_OS_IPHONE
#include <CoreGraphics/CGBitmapContext.h>
#include <CoreGraphics/CGContext.h>
#include <CoreGraphics/CGDataProvider.h>
#include <CoreGraphics/CGImage.h>
#endif

#include <sys/stat.h>

static mblk_t *_ms_load_jpeg_as_yuv(const char *jpgpath, MSVideoSize *reqsize) {
#if defined(_WIN32)
	mblk_t *m = NULL;
	DWORD st_sizel;
	DWORD st_sizeh;
	uint8_t *jpgbuf;
	DWORD err;
	HANDLE fd;
#ifdef UNICODE
	WCHAR wUnicode[1024];
	MultiByteToWideChar(CP_UTF8, 0, jpgpath, -1, wUnicode, 1024);
#else
	const char *wUnicode = jpgpath;
#endif
#if !defined(MS2_WINDOWS_DESKTOP) || defined(MS2_WINDOWS_UWP)
	fd = CreateFile2(wUnicode, GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING, NULL);
#else
	fd = CreateFile(wUnicode, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
#endif
	if (fd == INVALID_HANDLE_VALUE) {
		ms_error("Failed to open %s", jpgpath);
		return NULL;
	}
	st_sizel = 0;
	st_sizeh = 0;
#if !defined(MS2_WINDOWS_DESKTOP) || defined(MS2_WINDOWS_UWP)
	{
		WIN32_FILE_ATTRIBUTE_DATA attr_data;
		GetFileAttributesEx(wUnicode, GetFileExInfoStandard, &attr_data);
		st_sizel = attr_data.nFileSizeLow;
		st_sizeh = attr_data.nFileSizeHigh;
	}
#else
	st_sizel = GetFileSize(fd, &st_sizeh);
#endif
	if (st_sizeh > 0 || st_sizel <= 0) {
		CloseHandle(fd);
		ms_error("Can't load file %s", jpgpath);
		return NULL;
	}
	jpgbuf = (uint8_t *)ms_malloc0(st_sizel);
	if (jpgbuf == NULL) {
		CloseHandle(fd);
		ms_error("Cannot allocate buffer for %s", jpgpath);
		return NULL;
	}
	err = 0;
	ReadFile(fd, jpgbuf, st_sizel, &err, NULL);

	if (err != st_sizel) {
		ms_error("Could not read as much as wanted !");
	}
	m = jpeg2yuv(jpgbuf, st_sizel, reqsize);
	ms_free(jpgbuf);
	if (m == NULL) {
		CloseHandle(fd);
		ms_error("Cannot load image from buffer for %s", jpgpath);
		return NULL;
	}
	CloseHandle(fd);
	return m;
#else
	mblk_t *m = NULL;
	struct stat statbuf;
	uint8_t *jpgbuf;
	int err;
	int fd = open(jpgpath, O_RDONLY);

	if (fd != -1) {
		fstat(fd, &statbuf);
		if (statbuf.st_size <= 0) {
			close(fd);
			ms_error("Cannot load %s", jpgpath);
			return NULL;
		}
		jpgbuf = (uint8_t *)ms_malloc0(statbuf.st_size + FF_INPUT_BUFFER_PADDING_SIZE);
		if (jpgbuf == NULL) {
			close(fd);
			ms_error("Cannot allocate buffer for %s", jpgpath);
			return NULL;
		}
		err = read(fd, jpgbuf, statbuf.st_size);
		if (err != statbuf.st_size) {
			ms_error("Could not read as much as wanted: %i<>%li !", err, (long)statbuf.st_size);
		}
		m = jpeg2yuv(jpgbuf, statbuf.st_size, reqsize);
		ms_free(jpgbuf);
		if (m == NULL) {
			close(fd);
			ms_error("Cannot load image from buffer for %s", jpgpath);
			return NULL;
		}
	} else {
		ms_error("Cannot load %s", jpgpath);
		return NULL;
	}
	close(fd);
	return m;
#endif
}

static mblk_t *generate_black_yuv_frame(MSVideoSize *reqsize) {
	MSPicture dest;
	mblk_t *m = ms_yuv_buf_alloc(&dest, reqsize->width, reqsize->height);
	int ysize = dest.w * dest.h;
	int usize = ysize / 4;
	memset(dest.planes[0], 16, ysize);
	memset(dest.planes[1], 128, usize);
	memset(dest.planes[2], 128, usize);
	return m;
}

mblk_t *ms_load_jpeg_as_yuv(const char *jpgpath, MSVideoSize *reqsize) {
	mblk_t *m = NULL;
	if (jpgpath != NULL) {
		m = _ms_load_jpeg_as_yuv(jpgpath, reqsize);
	}
	if (m == NULL) m = generate_black_yuv_frame(reqsize);
	return m;
}

#ifndef NOWEBCAM_JPG
#define NOWEBCAM_JPG "nowebcamCIF"
#endif

#define DEF_IMAGE_MAX_SIZE 511
static char ms_nowebcam_def_image[DEF_IMAGE_MAX_SIZE + 1] = {0}; //+1 to ensure having a final null character
#ifndef MIN
#define MIN(a, b) a <= b ? a : b
#endif

mblk_t *ms_load_nowebcam(MSFactory *factory, MSVideoSize *reqsize, int idx) {
	mblk_t *m;
	char *tmp;
	const char *image_resources_dir = ms_factory_get_image_resources_dir(factory);
	if (idx < 0) tmp = bctbx_strdup_printf("%s/%s.jpg", image_resources_dir, NOWEBCAM_JPG);
	else tmp = bctbx_strdup_printf("%s/%s%i.jpg", image_resources_dir, NOWEBCAM_JPG, idx);
	m = ms_load_jpeg_as_yuv(tmp, reqsize);
	bctbx_free(tmp);
	return m;
}

typedef struct _SIData {
	MSVideoSize vsize;
	char *nowebcamimage;
	uint64_t lasttime;
	float fps;
	mblk_t *pic;
} SIData;

void static_image_init(MSFilter *f) {
	SIData *d = (SIData *)ms_new0(SIData, 1);
	d->vsize.width = MS_VIDEO_SIZE_CIF_W;
	d->vsize.height = MS_VIDEO_SIZE_CIF_H;

	if (ms_nowebcam_def_image[0] != 0) d->nowebcamimage = ms_strdup(ms_nowebcam_def_image);
	d->lasttime = 0;
	d->pic = NULL;
	d->fps = 1;
	f->data = d;
}

void static_image_uninit(MSFilter *f) {
	SIData *d = (SIData *)f->data;
	if (d->nowebcamimage) ms_free(d->nowebcamimage);
	ms_free(d);
}

static void static_image_load_jpeg(MSFilter *f) {
	SIData *d = (SIData *)f->data;
	if (d->pic == NULL) {
		d->pic = ms_load_jpeg_as_yuv(d->nowebcamimage, &d->vsize);
	}
}

void static_image_preprocess(MSFilter *f) {
	static_image_load_jpeg(f);
}

void static_image_process(MSFilter *f) {
	SIData *d = (SIData *)f->data;
	uint64_t frame_interval = (uint64_t)(1000 / d->fps);
	/*output a frame whenever needed, i.e. respect the FPS parameter */
	if ((f->ticker->time - d->lasttime > frame_interval) || d->lasttime == 0) {
		ms_mutex_lock(&f->lock);
		if (d->pic) {
			mblk_t *o = dupmsg(d->pic);
			/*prevent mirroring at the output*/
			mblk_set_precious_flag(o, 1);
			mblk_set_timestamp_info(o, (uint32_t)(f->ticker->time * 90));
			ms_queue_put(f->outputs[0], o);
		}
		ms_filter_unlock(f);
		d->lasttime = f->ticker->time;
	}
}

void static_image_postprocess(MSFilter *f) {
	SIData *d = (SIData *)f->data;
	if (d->pic) {
		freemsg(d->pic);
		d->pic = NULL;
	}
}

static int static_image_set_fps(MSFilter *f, void *arg) {
	SIData *d = (SIData *)f->data;
	d->fps = *((float *)arg);
	d->lasttime = 0;
	return 0;
}

static int static_image_get_fps(MSFilter *f, void *arg) {
	SIData *d = (SIData *)f->data;
	*((float *)arg) = d->fps;
	return 0;
}

int static_image_set_vsize(MSFilter *f, void *data) {
	SIData *d = (SIData *)f->data;
	d->vsize = *(MSVideoSize *)data;
	return 0;
}

int static_image_get_vsize(MSFilter *f, void *data) {
	SIData *d = (SIData *)f->data;
	static_image_load_jpeg(f);
	*(MSVideoSize *)data = d->vsize;
	return 0;
}

int static_image_get_pix_fmt(BCTBX_UNUSED(MSFilter *f), void *data) {
	*(MSPixFmt *)data = MS_YUV420P;
	return 0;
}

static int static_image_set_image(MSFilter *f, void *arg) {
	SIData *d = (SIData *)f->data;
	const char *image = (const char *)arg;
	ms_filter_lock(f);

	if (d->nowebcamimage) {
		ms_free(d->nowebcamimage);
		d->nowebcamimage = NULL;
	}

	if (image != NULL && image[0] != '\0') d->nowebcamimage = ms_strdup(image);

	if (d->pic != NULL) {
		/* Get rid of the old image and force a new preprocess so that the
		     new image is properly read. */
		freemsg(d->pic);
		d->pic = NULL;
	}
	d->lasttime = 0;

	// We have to make sure ticker is running so that the image is not loaded until the filter is configured
	if (f->ticker) static_image_load_jpeg(f);

	ms_filter_unlock(f);
	return 0;
}

MSFilterMethod static_image_methods[] = {{MS_FILTER_SET_FPS, static_image_set_fps},
                                         {MS_FILTER_GET_FPS, static_image_get_fps},
                                         {MS_FILTER_SET_VIDEO_SIZE, static_image_set_vsize},
                                         {MS_FILTER_GET_VIDEO_SIZE, static_image_get_vsize},
                                         {MS_FILTER_GET_PIX_FMT, static_image_get_pix_fmt},
                                         {MS_STATIC_IMAGE_SET_IMAGE, static_image_set_image},
                                         {0, 0}};

MSFilterDesc ms_static_image_desc = {MS_STATIC_IMAGE_ID,
                                     "MSStaticImage",
                                     "A filter that outputs a static image.",
                                     MS_FILTER_OTHER,
                                     NULL,
                                     0,
                                     1,
                                     static_image_init,
                                     static_image_preprocess,
                                     static_image_process,
                                     static_image_postprocess,
                                     static_image_uninit,
                                     static_image_methods,
                                     0};

MS_FILTER_DESC_EXPORT(ms_static_image_desc)

static void static_image_detect(MSWebCamManager *obj);

static void static_image_cam_init(MSWebCam *cam) {
	cam->name = ms_strdup("Static picture");
	if (ms_nowebcam_def_image[0] == 0) {
		MSFactory *factory = ms_web_cam_get_factory(cam);
		const char *image_resources_dir = ms_factory_get_image_resources_dir(factory);
		char *temp = ms_strdup_printf("%s/%s.jpg", image_resources_dir, NOWEBCAM_JPG);
		strncpy(ms_nowebcam_def_image, temp, MIN(DEF_IMAGE_MAX_SIZE, strlen(temp)));
		ms_free(temp);
	}
}

static MSFilter *static_image_create_reader(MSWebCam *obj) {
	return ms_factory_create_filter_from_desc(ms_web_cam_get_factory(obj), &ms_static_image_desc);
}

MSWebCamDesc static_image_desc = {
    "StaticImage", &static_image_detect, &static_image_cam_init, &static_image_create_reader, NULL, NULL};

static void static_image_detect(MSWebCamManager *obj) {
	MSWebCam *cam = ms_web_cam_manager_create_cam(obj, &static_image_desc);
	ms_web_cam_manager_add_cam(obj, cam);
}

void ms_static_image_set_default_image(const char *path) {
	if (ms_nowebcam_def_image[0] != 0) ms_nowebcam_def_image[0] = 0;
	if (path) {
		int min = MIN(DEF_IMAGE_MAX_SIZE, (int)strlen(path));
		strncpy(ms_nowebcam_def_image, path, min);
		ms_nowebcam_def_image[min] = '\0';
	}
}

const char *ms_static_image_get_default_image() {
	return ms_nowebcam_def_image;
}

#if __clang__
#pragma clang diagnostic pop
#endif
