// The Construct
//
// Copyright (C) The Construct Developers, Authors & Contributors
// Copyright (C) 2016-2020 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.

#include <RB_INC_SIGNAL_H
#include <RB_INC_MAGICK_API_H

namespace ircd::magick
{
	struct display;
	struct transform;

	constexpr bool debug_progress {false};

	[[noreturn]] static void handle_exception(const ExceptionType, const char *, const char *);
	static void handle_fatal(const ExceptionType, const char *, const char *) __attribute__((noreturn));
	static void handle_error(const ExceptionType, const char *, const char *) noexcept;
	static void handle_warning(const ExceptionType, const char *, const char *) noexcept;
	static void handle_log(const ExceptionType, const char *) noexcept;
	static void *handle_realloc(void *, size_t) noexcept;
	static void *handle_malloc(size_t) noexcept;
	static void handle_free(void *) noexcept;
	static uint handle_progress(const char *, const int64_t, const uint64_t, ExceptionInfo *) noexcept;

	template<class R, class F, class... A> static R call(F&&, A&&...);
	template<class R, class F, class... A> static R callex(F&&, A&&...);
	template<class F, class... A> static void callpf(F&&, A&&...);

	extern bool call_ready;
	extern ctx::dock call_dock;
	extern ctx::mutex call_mutex;
	extern conf::item<uint64_t> limit_ticks;
	extern conf::item<uint64_t> limit_cycles;
	extern conf::item<uint64_t> yield_threshold;
	extern conf::item<uint64_t> yield_interval;
	extern log::log log;
}

struct ircd::magick::display
{
	display(const ImageInfo &, Image &);
	display(const const_buffer &);
};

struct ircd::magick::transform
{
	using input = std::tuple<const ImageInfo &, const Image *>;
	using output = std::function<void (const const_buffer &)>;
	using transformer = std::function<Image *(const input &)>;

	transform(const const_buffer &, const output &, const transformer &);
};

decltype(ircd::magick::log)
ircd::magick::log
{
	"magick"
};

decltype(ircd::magick::limit_ticks)
ircd::magick::limit_ticks
{
	{ "name",    "ircd.magick.limit.ticks" },
	{ "default", 10000L                    },
};

decltype(ircd::magick::limit_cycles)
ircd::magick::limit_cycles
{
	{ "name",    "ircd.magick.limit.cycles" },
	{ "default", 0L                         },
};

decltype(ircd::magick::yield_threshold)
ircd::magick::yield_threshold
{
	{ "name",    "ircd.magick.yield.threshold" },
	{ "default", 1000L                         },
};

decltype(ircd::magick::yield_interval)
ircd::magick::yield_interval
{
	{ "name",    "ircd.magick.yield.interval" },
	{ "default", 768L                         },
};

// It is likely that we can't have two contexts enter libmagick
// simultaneously. This race is possible if the progress callback yields
// and another context starts an operation. It is highly unlikely the lib
// can handle reentrancy on the same thread. Hitting thread mutexes within
// magick will also be catastrophic to ircd::ctx.
decltype(ircd::magick::call_mutex)
ircd::magick::call_mutex;

decltype(ircd::magick::call_dock)
ircd::magick::call_dock;

decltype(ircd::magick::call_ready)
ircd::magick::call_ready;

decltype(ircd::magick::version_api)
ircd::magick::version_api
{
	"magick", info::versions::API, MagickLibVersion, {0}, MagickLibVersionText
};

decltype(ircd::magick::version_abi)
ircd::magick::version_abi
{
	"magick", info::versions::ABI, 0, {0}, []
	(auto &version, const auto &buf)
	{
		ulong monotonic(0);
		strlcpy(buf, GetMagickVersion(&monotonic));
		version.monotonic = monotonic;
	}
};

//
// Magick library signal handler workarounds.
//
// By default the graphicsmagick library installs signal handlers on
// supporting platforms starting in InitializeMagick() for the duration
// of the library. These handlers provide no essential functionality,
// polluting the address space for other libraries and users of our libircd,
// causing unexpected behavior.
//
// Even though the library makes a good faith attempt to not step on already-
// installed signal handlers: it falls short by not maintaining the full
// sigaction structure. It loses information for SA_SIGINFO handlers, etc.
//
// Our principal workaround involves interposing this function (thankfully
// exported by the library). Unfortunately this doesn't work in all
// environments so we retain a full list of signal numbers the libmagick
// interferes with.

namespace ircd::magick
{
	extern const int sig_overrides[];
	extern const size_t sig_overrides_num;
	static void sig_pre(), sig_post();
}

/// List of signals from libmagick
decltype(ircd::magick::sig_overrides)
ircd::magick::sig_overrides
{
	#ifdef HAVE_SIGNAL_H
	SIGABRT,   SIGBUS,    SIGCHLD,
	SIGFPE,    SIGHUP,    SIGINT,
	SIGQUIT,   SIGTERM,   SIGSEGV,
	SIGXCPU,   SIGXFSZ,
	#endif HAVE_SIGNAL_H
};

decltype(ircd::magick::sig_overrides_num)
ircd::magick::sig_overrides_num
{
	sizeof(sig_overrides) / sizeof(int)
};

#ifdef HAVE_SIGNAL_H
static struct sigaction
ircd_magick_sig_vector[ircd::magick::sig_overrides_num];
#endif

void
ircd::magick::sig_pre()
{
	#ifdef HAVE_SIGNAL_H
	for(size_t i(0); i < sig_overrides_num; ++i)
		syscall(::sigaction, sig_overrides[i], nullptr, ircd_magick_sig_vector + i);
	#endif HAVE_SIGNAL_H
}

void
ircd::magick::sig_post()
{
	#ifdef HAVE_SIGNAL_H
	for(size_t i(0); i < sig_overrides_num; ++i)
		syscall(::sigaction, sig_overrides[i], ircd_magick_sig_vector + i, nullptr);
	#endif HAVE_SIGNAL_H
}

extern "C" void
InitializeMagickSignalHandlers(void)
{
	ircd::log::debug
	{
		ircd::magick::log, "Bypassed InitializeMagickSignalHandlers()",
	};
}

//
// init
//

ircd::magick::init::init()
{
	log::info
	{
		log, "Initializing Magick Library version API:%lu [%s] ABI:%lu [%s]",
		long(version_api),
		string_view{version_api},
		long(version_abi),
		string_view{version_abi},
	};

	if(long(version_api) != long(version_abi))
		log::warning
		{
			log, "Magick Library version mismatch headers:%lu library:%lu",
			long(version_api),
			long(version_abi),
		};

	sig_pre();
	InitializeMagick(nullptr);
	MagickAllocFunctions(handle_free, handle_malloc, handle_realloc);
	SetFatalErrorHandler(handle_fatal);
	SetErrorHandler(handle_error);
	SetWarningHandler(handle_warning);
	SetLogMethod(handle_log);
	//SetLogEventMask("all"); // Pollutes stderr :/ can't fix
	SetMonitorHandler(handle_progress);
	SetMagickResourceLimit(ThreadsResource, 1UL);
	sig_post();

	call_ready = true;
	call_dock.notify_all();

	log::debug
	{
		log, "resource settings: pixel max:%lu:%lu height:%lu:%lu width:%lu:%lu; threads:%lu:%lu",
		GetMagickResource(PixelsResource),
		GetMagickResourceLimit(PixelsResource),
		GetMagickResource(HeightResource),
		GetMagickResourceLimit(HeightResource),
		GetMagickResource(WidthResource),
		GetMagickResourceLimit(WidthResource),
		GetMagickResource(ThreadsResource),
		GetMagickResourceLimit(ThreadsResource),
	};
}

[[gnu::cold]]
ircd::magick::init::~init()
noexcept
{
	log::debug
	{
		log, "Shutting down Magick Library..."
	};

	call_ready = false;
	call_dock.wait([]() noexcept
	{
		return !call_mutex.locked();
	});

	DestroyMagick();
}

//
// thumbcrop
//

ircd::magick::thumbcrop::thumbcrop(const const_buffer &in,
                                   const dimensions &req,
                                   const result_closure &out)
{
	crop::offset offset;
	const auto scaler{[&req, &offset]
	(const auto &image)
	{
		const auto &img_p
		{
			std::get<const Image *>(image)
		};

		const auto &img_x(img_p->columns);
		const auto &img_y(img_p->rows);
		const auto &[req_x_, req_y_] {req};
		const auto &req_x{std::min(req_x_, img_x)};
		const auto &req_y{std::min(req_y_, img_y)};
		const bool aspect
		{
			req_x * img_y < req_y * img_x
		};

		const dimensions scaled
		{
			aspect? req_y * img_x / img_y : req_x,
			aspect? req_y : req_x * img_y / img_x,
		};

		offset =
		{
			aspect? (scaled.first - req_x) / 2.0 : 0,
			aspect? 0 : (scaled.second - req_y) / 2.0,
		};

		return callex<Image *>(ThumbnailImage, img_p, scaled.first, scaled.second);
	}};

	const auto cropper{[&req, &out, &offset]
	(const const_buffer &in)
	{
		crop
		{
			in, req, offset, out
		};
	}};

	transform
	{
		in, cropper, scaler
	};
}

//
// thumbnail
//

ircd::magick::thumbnail::thumbnail(const const_buffer &in,
                                   const dimensions &req,
                                   const result_closure &out)
{
	transform
	{
		in, out, [&req](const auto &image)
		{
			const auto &img_p
			{
				std::get<const Image *>(image)
			};

			const auto &img_x(img_p->columns);
			const auto &img_y(img_p->rows);
			const auto &[req_x_, req_y_] {req};
			const auto &req_x{std::min(req_x_, img_x)};
			const auto &req_y{std::min(req_y_, img_y)};
			const bool aspect
			{
				req_x * img_y < req_y * img_x
			};

			const dimensions scaled
			{
				aspect? req_y * img_x / img_y : req_x,
				aspect? req_y : req_x * img_y / img_x,
			};

			return callex<Image *>(ThumbnailImage, img_p, scaled.first, scaled.second);
		}
	};
}

//
// scale
//

ircd::magick::scale::scale(const const_buffer &in,
                           const dimensions &dim,
                           const result_closure &out)
{
	transform
	{
		in, out, [&dim](const auto &image)
		{
			return callex<Image *>(ScaleImage, std::get<const Image *>(image), dim.first, dim.second);
		}
	};
}

//
// shave
//

ircd::magick::shave::shave(const const_buffer &in,
                           const dimensions &dim,
                           const offset &off,
                           const result_closure &out)
{
	const RectangleInfo geometry
	{
		dim.first,   // width
		dim.second,  // height
		off.first,   // x
		off.second,  // y
	};

	transform
	{
		in, out, [&geometry](const auto &image)
		{
			return callex<Image *>(ShaveImage, std::get<const Image *>(image), &geometry);
		}
	};
}

//
// crop
//

ircd::magick::crop::crop(const const_buffer &in,
                         const dimensions &dim,
                         const offset &off,
                         const result_closure &out)
{
	const RectangleInfo geometry
	{
		dim.first,   // width
		dim.second,  // height
		off.first,   // x
		off.second,  // y
	};

	transform
	{
		in, out, [&geometry](const auto &image)
		{
			return callex<Image *>(CropImage, std::get<const Image *>(image), &geometry);
		}
	};
}

//
// transform (internal)
//

ircd::magick::transform::transform(const const_buffer &input,
                                   const output &output,
                                   const transformer &transformer)
{
	const custom_ptr<ImageInfo> input_info
	{
		CloneImageInfo(nullptr),
		DestroyImageInfo
	};

	const custom_ptr<ImageInfo> output_info
	{
		CloneImageInfo(nullptr),
		DestroyImageInfo
	};

	const custom_ptr<Image> input_image
	{
		callex<Image *>(BlobToImage, input_info.get(), data(input), size(input)),
		DestroyImage // pollock
	};

	const custom_ptr<Image> output_image
	{
		transformer({*input_info, input_image.get()}),
		DestroyImage
	};

	size_t output_size(0);
	const auto output_data
	{
		callex<void *>(ImageToBlob, output_info.get(), output_image.get(), &output_size)
	};

	const const_buffer result
	{
		reinterpret_cast<char *>(output_data), output_size
	};

	output(result);
}

//
// display (internal)
//

ircd::magick::display::display(const const_buffer &input)
{
	const custom_ptr<ImageInfo> input_info
	{
		CloneImageInfo(nullptr),
		DestroyImageInfo
	};

	const custom_ptr<Image> input_image
	{
		callex<Image *>(BlobToImage, input_info.get(), data(input), size(input)),
		DestroyImage // pollock
	};

	display
	{
		*input_info, *input_image
	};
}

ircd::magick::display::display(const ImageInfo &info,
                               Image &image)
{
	callpf(DisplayImages, &info, &image);
}

//
// util (internal)
//

template<class return_t,
         class function,
         class... args>
return_t
ircd::magick::callex(function&& f,
                     args&&... a)
{
	if(unlikely(!call_ready))
		throw error
		{
			"Graphics library not ready."
		};

	const std::lock_guard lock
	{
		call_mutex
	};

	ExceptionInfo ei;
	GetExceptionInfo(&ei); // initializer
	const unwind destroy{[&ei]
	{
		DestroyExceptionInfo(&ei);
	}};

	assert(call_ready);
	const auto ret
	{
		f(std::forward<args>(a)..., &ei)
	};

	const auto their_handler
	{
		SetErrorHandler(handle_exception)
	};

	const unwind reset{[&their_handler]
	{
		SetErrorHandler(their_handler);
	}};

	// exception comes out of here; if this is not safe we'll have to
	// convey with a global or inspect ExceptionInfo manually.
	CatchException(&ei);
	return ret;
}

template<class function,
         class... args>
void
ircd::magick::callpf(function&& f,
                     args&&... a)
{
	if(unlikely(!call<MagickPassFail>(f, std::forward<args>(a)...)))
		throw error{};
}

template<class return_t,
         class function,
         class... args>
return_t
ircd::magick::call(function&& f,
                   args&&... a)
{
	if(unlikely(!call_ready))
		throw error
		{
			"Graphics library not ready."
		};

	const std::lock_guard lock
	{
		call_mutex
	};

	assert(call_ready);
	return f(std::forward<args>(a)...);
}

//
// ircd::magick::job
//

namespace ircd::magick
{
	static string_view loghead(const job &);
	static void job_init(const string_view &, const int64_t &, const uint64_t &, const uint64_t &);
	static void finished(job &);
	static bool check_yield(job &);
	static void check_cycles(job &);
}

struct ircd::magick::job::state
{
	uint64_t cycles {0};
	uint64_t yield {0};
	char description[1024];
}
thread_local ircd::magick::job::state;

decltype(ircd::magick::job::cur) thread_local
ircd::magick::job::cur;

decltype(ircd::magick::job::tot) thread_local
ircd::magick::job::tot;

uint
ircd::magick::handle_progress(const char *const text,
                              const int64_t tick,
                              const uint64_t ticks,
                              ExceptionInfo *ei)
noexcept try
{
	// Sample the current reference cycle count first and once. This is an
	// accumulated cycle count for only this ircd::ctx and the current slice,
	// (all other cycles are not accumulated here) which is non-zero by now
	// and monotonically increases across jobs as well.
	const auto cycles_sample
	{
		ctx::this_ctx::cycles()
	};

	// Detect if this is a new job. Tick is usually zero for a new job, but for
	// large jobs it may start after 0. Tick always appears monotonic for a job.
	// The ticks appears constant for a job, though could be the same for different
	// jobs. We don't know of any succinct way to test for a new job, so we use all
	// of the above information.
	const bool new_job
	{
		tick == 0
		|| tick < job::cur.tick
		|| ticks != job::cur.ticks
	};

	// Assert general assumptions about invocations of this callback.
	assert(new_job || tick >= job::cur.tick);
	assert(new_job || ticks == job::cur.ticks);

	// Branch after detecting this callback is unrelated to the last job.
	if(new_job)
	{
		finished(job::cur);
		job_init(text, tick, ticks, cycles_sample);
	}

	// Unconditional bookkeeping updates for this invocation. These statements
	// behave properly regardless of whether this is the same or a new job.
	assert(cycles_sample >= job::state.cycles);
	job::cur.cycles += cycles_sample - job::state.cycles;
	job::state.cycles = cycles_sample;
	job::cur.tick = tick;

	// This debug message is very noisy, even for debug mode. Developer can
	// enable it at their discretion.
	if constexpr(debug_progress)
		log::debug
		{
			log, "job:%lu progress %2.2lf%% (%ld/%ld) cycles:%lu intrs:%lu errors:%lu",
			job::cur.id,
			(job::cur.tick / double(job::cur.ticks) * 100.0),
			job::cur.tick,
			job::cur.ticks,
			job::cur.cycles,
			job::cur.intrs,
			job::cur.errors,
		};

	check_cycles(job::cur);
	check_yield(job::cur);

	return true;
}
catch(const ctx::interrupted &e)
{
	++job::cur.intrs;
	job::cur.eptr = std::current_exception();
	ThrowException(ei, MonitorError, "interrupted", e.what());
	ei->signature = MagickSignature; // ???
	return false;
}
catch(const ctx::terminated &)
{
	++job::cur.intrs;
	job::cur.eptr = std::current_exception();
	ThrowException(ei, MonitorError, "terminated", nullptr);
	ei->signature = MagickSignature; // ???
	return false;
}
catch(const std::exception &e)
{
	++job::cur.errors;
	job::cur.eptr = std::current_exception();
	ThrowLoggedException(ei, MonitorError, "error", e.what(), __FILE__, __FUNCTION__, __LINE__);
	ei->signature = MagickSignature; // ???
	return false;
}
catch(...)
{
	++job::cur.errors;
	job::cur.eptr = std::current_exception();
	ThrowLoggedException(ei, MonitorFatalError, "unknown", nullptr, __FILE__, __FUNCTION__, __LINE__);
	ei->signature = MagickSignature; // ???
	return false;
}

void
ircd::magick::check_cycles(job &job)
{
	const uint64_t &limit_cycles
	{
		magick::limit_cycles
	};

	// Check if job exceeded its reference cycle limit if enabled.
	if(unlikely(limit_cycles && job.cycles > limit_cycles))
		throw error
		{
			"job:%lu CPU cycles:%lu exceeded server limit:%lu (progress %2.2lf%% (%ld/%ld))",
			job.id,
			job.cycles,
			limit_cycles,
			(job.tick / double(job.ticks) * 100.0),
			job.tick,
			job.ticks,
		};
}

bool
ircd::magick::check_yield(job &job)
{
	const uint64_t &yield_threshold
	{
		magick::yield_threshold
	};

	// This job is too small to conduct any yields.
	if(likely(job.ticks < yield_threshold))
		return false;

	const uint64_t &yield_interval
	{
		magick::yield_interval
	};

	// Haven't reached the yield interval yet.
	if(likely(job.tick - job::state.yield <= yield_interval))
		return false;

	job::state.yield = job.tick;
	ctx::yield();
	return true;
}

void
ircd::magick::finished(job &job)
{
	// Update total state from last job
	assert(job.id == job::tot.id + 1 || (job.id == job::tot.id && !job.id));
	job::tot.id = job.id;
	job::tot.tick += job.tick;
	job::tot.ticks += job.ticks;
	job::tot.cycles += job.cycles;
	job::tot.yields += job.yields;
	job::tot.intrs += job.intrs;
	job::tot.errors += job.errors;
}

void
ircd::magick::job_init(const string_view &text,
                       const int64_t &tick,
                       const uint64_t &ticks,
                       const uint64_t &cycles_sample)
{
	// Reset the current job structure
	job::cur =
	{
		job::tot.id + 1,   // id
		tick,              // tick
		ticks,             // ticks
	};

	// Update internal state
	job::state.cycles = cycles_sample;

	// The description text may have this annoying empty "[]" on this
	// message so we'll strip that here.
	job::cur.description = strlcpy
	{
		job::state.description, lstrip(text, "[] ")
	};

	log::debug
	{
		log, "job:%lu started; ticks:%lu :%s",
		job::cur.id,
		job::cur.ticks,
		job::cur.description,
	};

	// This job is too large based on the ticks measurement. This is an ad hoc
	// measurement of the job size created internally by ImageMagick.
	if(job::cur.ticks > uint64_t(limit_ticks))
		throw error
		{
			"job:%lu computation ticks:%lu exceeds server limit:%lu :%s",
			job::cur.id,
			job::cur.ticks,
			uint64_t(limit_ticks),
			job::cur.description,
		};
}

ircd::string_view
ircd::magick::loghead(const job &job)
{
	thread_local char buf[256];
	return fmt::sprintf
	{
		buf, "job:%lu %ld/%lu [%s]",
		job.id,
		job.tick,
		job.ticks,
		job.description,
	};
}

//
// (Internal) patch panels
//

void
ircd::magick::handle_free(void *const ptr)
noexcept
{
	std::free(ptr);
}

void *
ircd::magick::handle_malloc(size_t size)
noexcept
{
	return std::malloc(size);
}

void *
ircd::magick::handle_realloc(void *const ptr,
                             size_t size)
noexcept
{
	return std::realloc(ptr, size);
}

void
ircd::magick::handle_log(const ExceptionType type,
                         const char *const message)
noexcept
{
	log::debug
	{
		log, "%s (%d) %s :%s",
		loghead(job::cur),
		int(type),
		GetLocaleExceptionMessage(type, ""),
		message,
	};
}

void
ircd::magick::handle_warning(const ExceptionType type,
                             const char *const reason,
                             const char *const description)
noexcept
{
	log::warning
	{
		log, "%s (#%d) %s :%s :%s",
		loghead(job::cur),
		int(type),
		GetLocaleExceptionMessage(type, ""),
		reason,
		description,
	};
}

void
ircd::magick::handle_error(const ExceptionType type,
                           const char *const reason,
                           const char *const description)
noexcept
{
	log::error
	{
		log, "%s (#%d) %s :%s :%s",
		loghead(job::cur),
		int(type),
		GetLocaleExceptionMessage(type, ""),
		reason,
		description,
	};
}

void
ircd::magick::handle_fatal(const ExceptionType type,
                           const char *const reason,
                           const char *const description)
{
	log::critical
	{
		log, "%s (#%d) %s :%s :%s",
		loghead(job::cur),
		int(type),
		GetLocaleExceptionMessage(type, ""),
		reason,
		description,
	};

	ircd::terminate();
	__builtin_unreachable();
}

void
ircd::magick::handle_exception(const ExceptionType type,
                               const char *const reason,
                               const char *const description)
{
	const auto &message
	{
		GetLocaleExceptionMessage(type, "")?: "???"
	};

	thread_local char buf[exception::BUFSIZE];
	const string_view what{fmt::sprintf
	{
		buf, "(#%d) %s :%s :%s",
		int(type),
		message,
		reason,
		description,
	}};

	log::derror
	{
		log, "%s %s",
		loghead(job::cur),
		what,
	};

	if(reason == "terminated"_sv)
		throw ctx::terminated{};

	if(reason == "interrupted"_sv)
		throw ctx::interrupted
		{
			"%s", what
		};

	throw error
	{
		"%s", what
	};
}
