//-----------------------------------------------------------------------------
// Flags       : clang-format SMTGSequencer
// Project     : VST SDK
//
// Category    : Validator
// Filename    : public.sdk/source/vst/testsuite/general/scanparameters.cpp
// Created by  : Steinberg, 04/2005
// Description : VST Test Suite
//
//-----------------------------------------------------------------------------
// This file is part of a Steinberg SDK. It is subject to the license terms
// in the LICENSE file found in the top-level directory of this distribution
// and at www.steinberg.net/sdklicenses.
// No part of the SDK, including this file, may be copied, modified, propagated,
// or distributed except according to the terms contained in the LICENSE file.
//-----------------------------------------------------------------------------

#include "public.sdk/source/vst/testsuite/general/scanparameters.h"
#include "public.sdk/source/vst/utility/stringconvert.h"
#include "pluginterfaces/base/funknownimpl.h"
#include "pluginterfaces/vst/ivstunits.h"
#include <algorithm>
#include <map>
#include <unordered_map>
#include <vector>

//------------------------------------------------------------------------
namespace Steinberg {
namespace Vst {

//------------------------------------------------------------------------
// ScanParametersTest
//------------------------------------------------------------------------
ScanParametersTest::ScanParametersTest (ITestPlugProvider* plugProvider) : TestBase (plugProvider)
{
}

//------------------------------------------------------------------------
bool PLUGIN_API ScanParametersTest::run (ITestResult* testResult)
{
	if (!testResult || !vstPlug)
		return false;

	printTestHeader (testResult);

	if (!controller)
	{
		addMessage (testResult, STR ("No Edit Controller supplied!"));
		return true;
	}

	int32 numParameters = controller->getParameterCount ();
	if (numParameters <= 0)
	{
		addMessage (testResult, STR ("This component does not export any parameters!"));
		return true;
	}

	addMessage (testResult, printf ("This component exports %d parameter(s)", numParameters));

	auto iUnitInfo2 = U::cast<IUnitInfo> (controller);
	if (!iUnitInfo2 && numParameters > 20)
	{
		addMessage (
		    testResult,
		    STR ("Note: it could be better to use UnitInfo in order to sort Parameters (>20)."));
	}
	struct SUnitCount
	{
		int32 idx {-1};
		int32 numParams {0};
		std::string name;
	};
	std::unordered_map<int32, SUnitCount> unitIds;
	// root unit
	unitIds[kRootUnitId].name = "Root";

	if (iUnitInfo2)
	{
		for (int32 ui = 0, uc = iUnitInfo2->getUnitCount (); ui < uc; ++ui)
		{
			UnitInfo uinfo = {};
			if (iUnitInfo2->getUnitInfo (ui, uinfo) == kResultTrue)
			{
				// check if ID is already used by another unit
				if (uinfo.id != kRootUnitId)
				{
					auto search = unitIds.find (uinfo.id);
					if (search != unitIds.end ())
					{
						addErrorMessage (
						    testResult,
						    printf ("=>Unit %03d (id=%d): ID already used by idx=%03d!!!", ui,
						            uinfo.id, search->second.idx));
						return false;
					}
				}
				std::string unitTitle;
				unitTitle = StringConvert::convert (uinfo.name);
				unitIds[uinfo.id] = {ui, 0, unitTitle}; // init counter
			}
			else
			{
				addErrorMessage (testResult, printf ("IUnitInfo::getUnitInfo (%d..) failed.", ui));
				return false;
			}
		}
	}

	// used for ID check
	std::unordered_map<int32, int32> paramIds;
	std::map<int32, std::vector<std::string>> listTitleUnitMap;

	bool foundBypass = false;
	for (int32 i = 0; i < numParameters; ++i)
	{
		ParameterInfo paramInfo = {};

		tresult result = controller->getParameterInfo (i, paramInfo);
		if (result != kResultOk)
		{
			addErrorMessage (testResult, printf ("=>Parameter %03d: is missing!!!", i));
			return false;
		}

		int32 paramId = paramInfo.id;
		if (paramId < 0)
		{
			addErrorMessage (testResult,
			                 printf ("=>Parameter %03d (id=%d): Invalid Id!!!", i, paramId));
			return false;
		}

		// check if ID is already used by another parameter
		auto search = paramIds.find (paramId);
		if (search != paramIds.end ())
		{
			addErrorMessage (testResult,
			                 printf ("=>Parameter %03d (id=%d): ID already used by idx=%03d!!!", i,
			                         paramId, search->second));
			return false;
		}
		paramIds[paramId] = i;

		const char8* paramType = kEmptyString8;
		if (paramInfo.stepCount < 0)
		{
			addErrorMessage (
			    testResult,
			    printf ("=>Parameter %03d (id=%d): invalid stepcount (<0)!!!", i, paramId));
			return false;
		}
		if (paramInfo.stepCount == kStepCountContinuous)
			paramType = "Float";
		else if (paramInfo.stepCount == kStepCountToggle)
			paramType = "Toggle";
		else
			paramType = "Discrete";

		auto paramTitle = StringConvert::convert (paramInfo.title);
		auto paramUnits = StringConvert::convert (paramInfo.units);

		addMessage (
		    testResult,
		    printf (
		        R"(   Parameter %03d (id=%d): [title="%s"] [unit="%s"] [type = %s, default = %lf, unit = %d])",
		        i, paramId, paramTitle.data (), paramUnits.data (), paramType,
		        paramInfo.defaultNormalizedValue, paramInfo.unitId));

		if (paramTitle.empty ())
		{
			addErrorMessage (testResult,
			                 printf ("=>Parameter %03d (id=%d): has no title!!!", i, paramId));
			return false;
		}

		// check if the same title is present in the same unit
		auto it = listTitleUnitMap.find (paramInfo.unitId);
		if (it != listTitleUnitMap.end ())
		{
			const std::vector<std::string>& list = it->second;
			auto found = std::find (list.begin (), list.end (), paramTitle);
			if (found != list.end ())
			{
				addMessage (
				    testResult,
				    printf (
				        R"(=>Parameter %03d (id=%d): [title="%s"] has the same title as another parameter in this unit = %d!)",
				        i, paramId, paramTitle.c_str (), paramInfo.unitId));
			}
		}
		listTitleUnitMap[paramInfo.unitId].push_back (paramTitle);

		if (paramInfo.defaultNormalizedValue != -1.f &&
		    (paramInfo.defaultNormalizedValue < 0. || paramInfo.defaultNormalizedValue > 1.))
		{
			addErrorMessage (
			    testResult,
			    printf (
			        "=>Parameter %03d (id=%d): paramInfo.defaultNormalizedValue is not normalized!!!",
			        i, paramId));
			return false;
		}
		int32 unitId = paramInfo.unitId;
		if (unitId < -1)
		{
			addErrorMessage (
			    testResult,
			    printf ("=>Parameter %03d (id=%d): No appropriate unit ID!!!", i, paramId));
			return false;
		}
		if (unitId >= -1)
		{
			auto iUnitInfo = U::cast<IUnitInfo> (controller);
			if (!iUnitInfo && unitId != kRootUnitId)
			{
				addErrorMessage (
				    testResult,
				    printf (
				        "IUnitInfo interface is missing, but ParameterInfo::unitID is not %03d (kRootUnitId).",
				        kRootUnitId));
				return false;
			}
			auto searchUnit = unitIds.find (unitId);
			if (searchUnit != unitIds.end ())
			{
				unitIds[unitId].numParams++;
			}
			else if (unitId != kRootUnitId)
			{
				addErrorMessage (
				    testResult,
				    printf (
				        "=>Parameter %03d (id=%d) has a UnitID (%d), which isn't defined in IUnitInfo.",
				        i, paramId, unitId));
				return false;
			}
			else // if (unitId == kRootUnitId)
				unitIds[kRootUnitId].numParams++;
		}

		//---check for incompatible flags---------------------
		// kCanAutomate and kIsReadOnly
		if (((paramInfo.flags & ParameterInfo::kCanAutomate) != 0) &&
		    ((paramInfo.flags & ParameterInfo::kIsReadOnly) != 0))
		{
			addErrorMessage (
			    testResult,
			    printf (
			        "=>Parameter %03d (id=%d) must not be kCanAutomate and kReadOnly at the same time.",
			        i, paramId));
			return false;
		}
		// kIsProgramChange and kIsReadOnly
		if (((paramInfo.flags & ParameterInfo::kIsProgramChange) != 0) &&
		    ((paramInfo.flags & ParameterInfo::kIsReadOnly) != 0))
		{
			addErrorMessage (
			    testResult,
			    printf (
			        "=>Parameter %03d (id=%d) must not be kIsProgramChange and kReadOnly at the same time.",
			        i, paramId));
			return false;
		}
		// kIsBypass only or kIsBypass and kCanAutomate only
		if (((paramInfo.flags & ParameterInfo::kIsBypass) != 0) &&
		    !((paramInfo.flags == ParameterInfo::kIsBypass) ||
		      ((paramInfo.flags & ParameterInfo::kCanAutomate) != 0)))
		{
			addErrorMessage (
			    testResult,
			    printf (
			        "=>Parameter %03d (id=%d) is kIsBypass and could have only kCanAutomate as other flag at the same time.",
			        i, paramId));
			return false;
		}

		//---maybe wrong combination of flags-------------------
		// kIsBypass but not kCanAutomate
		if (paramInfo.flags == ParameterInfo::kIsBypass)
		{
			addMessage (testResult,
			            printf ("=>Parameter %03d (id=%d) is kIsBypass, but not kCanAutomate!", i,
			                    paramId));
		}
		// kIsHidden and (kCanAutomate or not kIsReadOnly)
		if (paramInfo.flags == ParameterInfo::kIsHidden)
		{
			if ((paramInfo.flags & ParameterInfo::kCanAutomate) != 0)
			{
				addMessage (
				    testResult,
				    printf ("=>Parameter %03d (id=%d) is kIsHidden and kCanAutomate!", i, paramId));
			}
			if ((paramInfo.flags & ParameterInfo::kIsReadOnly) == 0)
			{
				addMessage (testResult,
				            printf ("=>Parameter %03d (id=%d) is kIsHidden and NOT kIsReadOnly!", i,
				                    paramId));
			}
		}

		// kIsProgramChange and not kIsList
		if (((paramInfo.flags & ParameterInfo::kIsProgramChange) != 0) &&
		    ((paramInfo.flags & ParameterInfo::kIsList) == 0))
		{
			addMessage (testResult,
			            printf ("=>Parameter %03d (id=%d) is kIsProgramChange, but not a kIsList!",
			                    i, paramId));
		}
		// kIsReadOnly and kIsWrapAround
		if (((paramInfo.flags & ParameterInfo::kIsReadOnly) != 0) &&
		    ((paramInfo.flags & ParameterInfo::kIsWrapAround) != 0))
		{
			addMessage (
			    testResult,
			    printf ("=>Parameter %03d (id=%d) is kIsReadOnly, no need to be kIsWrapAround too!",
			            i, paramId));
		}

		//---check bypass--------------------------------------
		if ((paramInfo.flags & ParameterInfo::kIsBypass) != 0)
		{
			if (!foundBypass)
				foundBypass = true;
			else
			{
				addErrorMessage (
				    testResult,
				    printf ("=>Parameter %03d (id=%d): There can only be one bypass (kIsBypass).",
				            i, paramId));
				return false;
			}
		}

		// --- Check if getParamValueByString returns the original normalized value for the string
		// returned by getParamStringByValue (no error for now just warning)
		auto currentNormVal = controller->getParamNormalized (paramId);
		String128 currentString;
		if (controller->getParamStringByValue (paramId, currentNormVal, currentString) == kResultOk)
		{
			ParamValue valueFromString = 0.f;
			auto res = controller->getParamValueByString (paramId, currentString, valueFromString);
			if (res == kResultOk)
			{
				if (std::abs (valueFromString - currentNormVal) > 0.0001f)
				{
					std::string str = StringConvert::convert (currentString);
					addMessage (
					    testResult,
					    printf (
					        R"(=>Parameter %03d (id=%d): getParamValueByString ("%s") doesn't return the original normalized value! Got %f instead of %f.)",
					        i, paramId, str.c_str (), valueFromString, currentNormVal));
				}
			}
			else
			{
				std::string str = StringConvert::convert (currentString);
				if (res == kNotImplemented)
				{
					addMessage (
					    testResult,
					    printf (
					        R"(=>Parameter %03d (id=%d): getParamValueByString ("%s") not implemented.)",
					        i, paramId, str.c_str ()));
				}
				else
				{
					addMessage (
					    testResult,
					    printf (
					        R"(=>Parameter %03d (id=%d): getParamValueByString ("%s") failed for the string returned by getParamStringByValue.)",
					        i, paramId, str.c_str ()));
				}
			}
		}
	} // end for each parameter

	for (const auto& unit : unitIds)
	{
		if (unit.second.numParams > 128) // 128 due to MIDI CC mapped parameter
		{
			addMessage (
			    testResult,
			    printf (
			        R"(Note: This Unit (idx=%d, id=%d, name="%s") has %d parameters: it could be better to split it in sub-units!.)",
			        unit.second.idx, unit.first, unit.second.name.data (), unit.second.numParams));
		}
	}

	if (foundBypass == false)
	{
		StringResult subCat;
		plugProvider->getSubCategories (subCat);
		if (subCat.get ().find ("Instrument") != std::string::npos)
			addMessage (testResult, STR ("No bypass parameter found. This is an instrument."));
		else
			addMessage (testResult, STR ("Warning: No bypass parameter found. Is this intended ?"));
	}

	return true;
}

//------------------------------------------------------------------------
} // Vst
} // Steinberg
