/*****************************************************************************
Copyright (C) 2016-2017 by Colin Edwards.
Additional Code Copyright (C) 2016-2017 by c3r1c3 <c3r1c3@nevermindonline.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*****************************************************************************/

#include "headers/VSTPlugin.h"
#include <util/platform.h>

intptr_t VSTPlugin::hostCallback_static(AEffect *effect, int32_t opcode, int32_t index, intptr_t value, void *ptr,
					float opt)
{
	UNUSED_PARAMETER(opt);

	VSTPlugin *plugin = nullptr;
	if (effect && effect->user) {
		plugin = static_cast<VSTPlugin *>(effect->user);
	}

	switch (opcode) {
	case audioMasterVersion:
		return (intptr_t)2400;

	case audioMasterGetCurrentProcessLevel:
		return 1;

	// We always replace, never accumulate
	case audioMasterWillReplaceOrAccumulate:
		return 1;

	case audioMasterGetSampleRate:
		if (plugin) {
			return (intptr_t)plugin->GetSampleRate();
		}
		return 0;

	case audioMasterGetVendorString:
		strncpy((char *)ptr, "OBS Studio", 11);
		return 1;

	case audioMasterGetTime:
		if (plugin) {
			return (intptr_t)plugin->GetTimeInfo();
		}
		return 0;

	// index: width, value: height
	case audioMasterSizeWindow:
		if (plugin && plugin->editorWidget) {
			plugin->editorWidget->handleResizeRequest(index, value);
		}
		return 1;

	default:
		return 0;
	}
}

VstTimeInfo *VSTPlugin::GetTimeInfo()
{
	mTimeInfo.nanoSeconds = os_gettime_ns() / 1000000;
	return &mTimeInfo;
}

float VSTPlugin::GetSampleRate()
{
	return mTimeInfo.sampleRate;
}

VSTPlugin::VSTPlugin(obs_source_t *sourceContext) : sourceContext{sourceContext} {}

VSTPlugin::~VSTPlugin()
{
	unloadEffect();

	cleanupChannelBuffers();
}

void VSTPlugin::createChannelBuffers(size_t count)
{
	cleanupChannelBuffers();

	int blocksize = BLOCK_SIZE;
	numChannels = std::max((size_t)0, count);

	if (numChannels > 0) {
		inputs = (float **)bmalloc(sizeof(float *) * numChannels);
		outputs = (float **)bmalloc(sizeof(float *) * numChannels);
		channelrefs = (float **)bmalloc(sizeof(float *) * numChannels);
		for (size_t channel = 0; channel < numChannels; channel++) {
			inputs[channel] = (float *)bmalloc(sizeof(float) * blocksize);
			outputs[channel] = (float *)bmalloc(sizeof(float) * blocksize);
		}
	}
}

void VSTPlugin::cleanupChannelBuffers()
{
	for (size_t channel = 0; channel < numChannels; channel++) {
		if (inputs && inputs[channel]) {
			bfree(inputs[channel]);
			inputs[channel] = NULL;
		}
		if (outputs && outputs[channel]) {
			bfree(outputs[channel]);
			outputs[channel] = NULL;
		}
	}
	if (inputs) {
		bfree(inputs);
		inputs = NULL;
	}
	if (outputs) {
		bfree(outputs);
		outputs = NULL;
	}
	if (channelrefs) {
		bfree(channelrefs);
		channelrefs = NULL;
	}
	numChannels = 0;
}

void VSTPlugin::loadEffectFromPath(const std::string &path)
{
	if (this->pluginPath.compare(path) != 0) {
		unloadEffect();
		blog(LOG_INFO, "User selected new VST plugin: '%s'", path.c_str());
	}

	if (!effect) {
		// TODO: alert user of error if VST is not available.

		pluginPath = path;

		AEffect *effectTemp = loadEffect();
		if (!effectTemp) {
			blog(LOG_WARNING, "VST Plug-in: Can't load effect!");
			return;
		}

		{
			std::lock_guard<std::recursive_mutex> lock(lockEffect);
			effect = effectTemp;
		}

		// Check plug-in's magic number
		// If incorrect, then the file either was not loaded properly,
		// is not a real VST plug-in, or is otherwise corrupt.
		if (effect->magic != kEffectMagic) {
			blog(LOG_WARNING, "VST Plug-in's magic number is bad");
			return;
		}

		int maxchans = std::max(effect->numInputs, effect->numOutputs);
		// sanity check
		if (maxchans < 0 || maxchans > 256) {
			blog(LOG_WARNING, "VST Plug-in has invalid number of channels");
			return;
		}

		createChannelBuffers(maxchans);

		// It is better to invoke this code after checking magic number
		effect->dispatcher(effect, effGetEffectName, 0, 0, effectName, 0);
		effect->dispatcher(effect, effGetVendorString, 0, 0, vendorString, 0);

		// This check logic is refer to open source project : Audacity
		if ((effect->flags & effFlagsIsSynth) || !(effect->flags & effFlagsCanReplacing)) {
			blog(LOG_WARNING, "VST Plug-in can't support replacing. '%s'", path.c_str());
			return;
		}

		// Ask the plugin to identify itself...might be needed for older plugins
		effect->dispatcher(effect, effIdentify, 0, 0, nullptr, 0.0f);

		effect->dispatcher(effect, effOpen, 0, 0, nullptr, 0.0f);

		// Set some default properties
		size_t sampleRate = audio_output_get_sample_rate(obs_get_audio());

		// Initialize time info
		memset(&mTimeInfo, 0, sizeof(mTimeInfo));
		mTimeInfo.sampleRate = sampleRate;
		mTimeInfo.nanoSeconds = os_gettime_ns() / 1000000;
		mTimeInfo.tempo = 120.0;
		mTimeInfo.timeSigNumerator = 4;
		mTimeInfo.timeSigDenominator = 4;
		mTimeInfo.flags = kVstTempoValid | kVstNanosValid | kVstTransportPlaying;

		effect->dispatcher(effect, effSetSampleRate, 0, 0, nullptr, sampleRate);
		int blocksize = BLOCK_SIZE;
		effect->dispatcher(effect, effSetBlockSize, 0, blocksize, nullptr, 0.0f);

		effect->dispatcher(effect, effMainsChanged, 0, 1, nullptr, 0);

		effectReady = true;

		if (openInterfaceWhenActive) {
			openEditor();
		}
	}
}

static void silenceChannel(float **channelData, size_t numChannels, long numFrames)
{
	for (size_t channel = 0; channel < numChannels; ++channel) {
		for (long frame = 0; frame < numFrames; ++frame) {
			channelData[channel][frame] = 0.0f;
		}
	}
}

obs_audio_data *VSTPlugin::process(struct obs_audio_data *audio)
{
	// Here we check the status firstly,
	// which help avoid waiting for lock while unloadEffect() is running.
	bool effectValid = (effect && effectReady && numChannels > 0);
	if (!effectValid)
		return audio;

	std::lock_guard<std::recursive_mutex> lock(lockEffect);

	if (effect && effectReady && numChannels > 0) {
		uint passes = (audio->frames + BLOCK_SIZE - 1) / BLOCK_SIZE;
		uint extra = audio->frames % BLOCK_SIZE;
		for (uint pass = 0; pass < passes; pass++) {
			uint frames = pass == passes - 1 && extra ? extra : BLOCK_SIZE;
			silenceChannel(outputs, numChannels, BLOCK_SIZE);

			for (size_t d = 0; d < numChannels; d++) {
				if (d < MAX_AV_PLANES && audio->data[d] != nullptr) {
					channelrefs[d] = ((float *)audio->data[d]) + (pass * BLOCK_SIZE);
				} else {
					channelrefs[d] = inputs[d];
				}
			};

			effect->processReplacing(effect, channelrefs, outputs, frames);

			// only copy back the channels the plugin may have generated
			for (size_t c = 0; c < (size_t)effect->numOutputs && c < MAX_AV_PLANES; c++) {
				if (audio->data[c]) {
					for (size_t i = 0; i < frames; i++) {
						channelrefs[c][i] = outputs[c][i];
					}
				}
			}
		}
	}

	return audio;
}

void VSTPlugin::unloadEffect()
{
	closeEditor();

	{
		std::lock_guard<std::recursive_mutex> lock(lockEffect);

		// Reset the status firstly to avoid VSTPlugin::process is blocked
		effectReady = false;

		if (effect) {
			effect->dispatcher(effect, effMainsChanged, 0, 0, nullptr, 0);
			effect->dispatcher(effect, effClose, 0, 0, nullptr, 0.0f);
		}

		effect = nullptr;
	}

	unloadLibrary();

	pluginPath.clear();
}

bool VSTPlugin::isEditorOpen()
{
	return editorWidget ? true : false;
}

void VSTPlugin::onEditorClosed()
{
	if (!editorWidget)
		return;

	editorWidget->deleteLater();
	editorWidget = nullptr;

	if (effect && editorOpened) {
		editorOpened = false;
		effect->dispatcher(effect, effEditClose, 0, 0, nullptr, 0);
	}
}

void VSTPlugin::openEditor()
{
	if (effect && !editorWidget) {
		// This check logic is refer to open source project : Audacity
		if (!(effect->flags & effFlagsHasEditor)) {
			blog(LOG_WARNING, "VST Plug-in: Can't support edit feature. '%s'", pluginPath.c_str());
			return;
		}

		editorOpened = true;
		editorWidget = new EditorWidget(nullptr, this);
		editorWidget->buildEffectContainer(effect);

		if (sourceName.empty()) {
			sourceName = "VST 2.x";
		}

		if (filterName.empty()) {
			editorWidget->setWindowTitle(QString("%1 - %2").arg(sourceName.c_str(), effectName));
		} else {
			editorWidget->setWindowTitle(
				QString("%1: %2 - %3").arg(sourceName.c_str(), filterName.c_str(), effectName));
		}
		editorWidget->show();
	}
}

void VSTPlugin::closeEditor()
{
	if (editorWidget)
		editorWidget->close();
}

std::string VSTPlugin::getEffectPath()
{
	return pluginPath;
}

std::string VSTPlugin::getChunk()
{
	if (!effect) {
		return "";
	}

	if (effect->flags & effFlagsProgramChunks) {
		void *buf = nullptr;

		intptr_t chunkSize = effect->dispatcher(effect, effGetChunk, 1, 0, &buf, 0.0);

		QByteArray data = QByteArray((char *)buf, chunkSize);
		return QString(data.toBase64()).toStdString();
	} else {
		std::vector<float> params;
		for (int i = 0; i < effect->numParams; i++) {
			float parameter = effect->getParameter(effect, i);
			params.push_back(parameter);
		}

		const char *bytes = reinterpret_cast<const char *>(&params[0]);
		QByteArray data = QByteArray(bytes, (int)(sizeof(float) * params.size()));
		std::string encoded = QString(data.toBase64()).toStdString();
		return encoded;
	}
}

void VSTPlugin::setChunk(const std::string &data)
{
	if (!effect) {
		return;
	}

	if (effect->flags & effFlagsProgramChunks) {
		QByteArray base64Data = QByteArray(data.c_str(), (int)data.length());
		QByteArray chunkData = QByteArray::fromBase64(base64Data);
		void *buf = nullptr;
		buf = chunkData.data();
		effect->dispatcher(effect, effSetChunk, 1, chunkData.length(), buf, 0);
	} else {
		QByteArray base64Data = QByteArray(data.c_str(), (int)data.length());
		QByteArray paramData = QByteArray::fromBase64(base64Data);

		const char *p_chars = paramData.data();
		const float *p_floats = reinterpret_cast<const float *>(p_chars);

		const size_t size = paramData.length() / sizeof(float);

		std::vector<float> params(p_floats, p_floats + size);

		if (params.size() != (size_t)effect->numParams) {
			return;
		}

		for (int i = 0; i < effect->numParams; i++) {
			effect->setParameter(effect, i, params[i]);
		}
	}
}

void VSTPlugin::setProgram(const int programNumber)
{
	if (programNumber < effect->numPrograms) {
		effect->dispatcher(effect, effSetProgram, 0, programNumber, NULL, 0.0f);
	} else {
		blog(LOG_ERROR, "Failed to load program, number was outside possible program range.");
	}
}

int VSTPlugin::getProgram()
{
	return effect->dispatcher(effect, effGetProgram, 0, 0, NULL, 0.0f);
}

void VSTPlugin::getSourceNames()
{
	/* Only call inside the vst_filter_audio function! */
	sourceName = obs_source_get_name(obs_filter_get_parent(sourceContext));
	filterName = obs_source_get_name(sourceContext);
}
