#include <test/tools/ossfuzz/protoToAbiV2.h>

#include <boost/preprocessor.hpp>
#include <regex>

/// Convenience macros
/// Returns a valid Solidity integer width w such that 8 <= w <= 256.
#define INTWIDTH(z, n, _ununsed) BOOST_PP_MUL(BOOST_PP_ADD(n, 1), 8)
/// Using declaration that aliases long boost multiprecision types with
/// s(u)<width> where <width> is a valid Solidity integer width and "s"
/// stands for "signed" and "u" for "unsigned".
#define USINGDECL(z, n, sign) \
	using BOOST_PP_CAT(BOOST_PP_IF(sign, s, u), INTWIDTH(z, n,)) =             \
	boost::multiprecision::number<                                             \
		boost::multiprecision::cpp_int_backend<                                \
			INTWIDTH(z, n,),                                                   \
			INTWIDTH(z, n,),                                                   \
			BOOST_PP_IF(                                                       \
				sign,                                                          \
				boost::multiprecision::signed_magnitude,                       \
				boost::multiprecision::unsigned_magnitude                      \
			),                                                                 \
			boost::multiprecision::unchecked,                                  \
			void                                                               \
		>                                                                      \
	>;
/// Instantiate the using declarations for signed and unsigned integer types.
BOOST_PP_REPEAT(32, USINGDECL, 1)
BOOST_PP_REPEAT(32, USINGDECL, 0)
/// Case implementation that returns an integer value of the specified type.
/// For signed integers, we divide by two because the range for boost multiprecision
/// types is double that of Solidity integer types. Example, 8-bit signed boost
/// number range is [-255, 255] but Solidity `int8` range is [-128, 127]
#define CASEIMPL(z, n, sign)                                                   \
	case INTWIDTH(z, n,):                                                      \
		stream << BOOST_PP_IF(                                                 \
			sign,                                                              \
			integerValue<                                                      \
				BOOST_PP_CAT(                                                  \
					BOOST_PP_IF(sign, s, u),                                   \
					INTWIDTH(z, n,)                                            \
                )>(_counter) / 2,                                              \
			integerValue<                                                      \
				BOOST_PP_CAT(                                                  \
					BOOST_PP_IF(sign, s, u),                                   \
					INTWIDTH(z, n,)                                            \
                )>(_counter)                                                   \
        );                                                                     \
		break;
/// Switch implementation that instantiates case statements for (un)signed
/// Solidity integer types.
#define SWITCHIMPL(sign)                                                       \
	ostringstream stream;                                                      \
	switch (_intWidth)                                                         \
	{                                                                          \
	BOOST_PP_REPEAT(32, CASEIMPL, sign)	                                       \
	}	                                                                       \
	return stream.str();

using namespace std;
using namespace solidity::util;
using namespace solidity::test::abiv2fuzzer;

namespace
{
template <typename V>
static V integerValue(unsigned _counter)
{
	V value = V(
		u256(solidity::util::keccak256(solidity::util::h256(_counter))) % u256(boost::math::tools::max_value<V>())
	);
	if (boost::multiprecision::is_signed_number<V>::value && value % 2 == 0)
		return value * (-1);
	else
		return value;
}

static string signedIntegerValue(unsigned _counter, unsigned _intWidth)
{
	SWITCHIMPL(1)
}

static string unsignedIntegerValue(unsigned _counter, unsigned _intWidth)
{
	SWITCHIMPL(0)
}

static string integerValue(unsigned _counter, unsigned _intWidth, bool _signed)
{
	if (_signed)
		return signedIntegerValue(_counter, _intWidth);
	else
		return unsignedIntegerValue(_counter, _intWidth);
}
}

string ProtoConverter::getVarDecl(
	string const& _type,
	string const& _varName,
	string const& _qualifier
)
{
	// One level of indentation for state variable declarations
	// Two levels of indentation for local variable declarations
	return Whiskers(R"(
	<?isLocalVar>	</isLocalVar><type><?qual> <qualifier></qual> <varName>;)"
		)
		("isLocalVar", !m_isStateVar)
		("type", _type)
		("qual", !_qualifier.empty())
		("qualifier", _qualifier)
		("varName", _varName)
		.render() +
		"\n";
}

pair<string, string> ProtoConverter::visit(Type const& _type)
{
	switch (_type.type_oneof_case())
	{
	case Type::kVtype:
		return visit(_type.vtype());
	case Type::kNvtype:
		return visit(_type.nvtype());
	case Type::TYPE_ONEOF_NOT_SET:
		return make_pair("", "");
	}
}

pair<string, string> ProtoConverter::visit(ValueType const& _type)
{
	switch (_type.value_type_oneof_case())
	{
	case ValueType::kBoolty:
		return visit(_type.boolty());
	case ValueType::kInty:
		return visit(_type.inty());
	case ValueType::kByty:
		return visit(_type.byty());
	case ValueType::kAdty:
		return visit(_type.adty());
	case ValueType::VALUE_TYPE_ONEOF_NOT_SET:
		return make_pair("", "");
	}
}

pair<string, string> ProtoConverter::visit(NonValueType const& _type)
{
	switch (_type.nonvalue_type_oneof_case())
	{
	case NonValueType::kDynbytearray:
		return visit(_type.dynbytearray());
	case NonValueType::kArrtype:
		if (ValidityVisitor().visit(_type.arrtype()))
			return visit(_type.arrtype());
		else
			return make_pair("", "");
	case NonValueType::kStype:
		if (ValidityVisitor().visit(_type.stype()))
			return visit(_type.stype());
		else
			return make_pair("", "");
	case NonValueType::NONVALUE_TYPE_ONEOF_NOT_SET:
		return make_pair("", "");
	}
}

pair<string, string> ProtoConverter::visit(BoolType const& _type)
{
	return processType(_type, true);
}

pair<string, string> ProtoConverter::visit(IntegerType const& _type)
{
	return processType(_type, true);
}

pair<string, string> ProtoConverter::visit(FixedByteType const& _type)
{
	return processType(_type, true);
}

pair<string, string> ProtoConverter::visit(AddressType const& _type)
{
	return processType(_type, true);
}

pair<string, string> ProtoConverter::visit(DynamicByteArrayType const& _type)
{
	return processType(_type, false);
}

pair<string, string> ProtoConverter::visit(ArrayType const& _type)
{
	return processType(_type, false);
}

pair<string, string> ProtoConverter::visit(StructType const& _type)
{
	return processType(_type, false);
}

template <typename T>
pair<string, string> ProtoConverter::processType(T const& _type, bool _isValueType)
{
	ostringstream local, global;
	auto [varName, paramName] = newVarNames(getNextVarCounter(), m_isStateVar);

	// Add variable name to the argument list of coder function call
	if (m_argsCoder.str().empty())
		m_argsCoder << varName;
	else
		m_argsCoder << ", " << varName;

	string location{};
	if (!m_isStateVar && !_isValueType)
		location = "memory";

	auto varDeclBuffers = varDecl(
		varName,
		paramName,
		_type,
		_isValueType,
		location
	);
	global << varDeclBuffers.first;
	local << varDeclBuffers.second;
	auto assignCheckBuffers = assignChecker(varName, paramName, _type);
	global << assignCheckBuffers.first;
	local << assignCheckBuffers.second;

	m_structCounter += m_numStructsAdded;
	return make_pair(global.str(), local.str());
}

template <typename T>
pair<string, string> ProtoConverter::varDecl(
	string const& _varName,
	string const& _paramName,
	T _type,
	bool _isValueType,
	string const& _location
)
{
	ostringstream local, global;

	TypeVisitor tVisitor(m_structCounter);
	string typeStr = tVisitor.visit(_type);
	if (typeStr.empty())
		return make_pair("", "");

	// Append struct defs
	global << tVisitor.structDef();
	m_numStructsAdded = tVisitor.numStructs();

	// variable declaration
	if (m_isStateVar)
		global << getVarDecl(typeStr, _varName, _location);
	else
		local << getVarDecl(typeStr, _varName, _location);

	// Add typed params for calling public and external functions with said type
	appendTypedParams(
		CalleeType::PUBLIC,
		_isValueType,
		typeStr,
		_paramName,
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);
	appendTypedParams(
		CalleeType::EXTERNAL,
		_isValueType,
		typeStr,
		_paramName,
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);
	appendTypes(
		_isValueType,
		typeStr,
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);
	appendTypedReturn(
		_isValueType,
		typeStr,
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);
	appendToIsabelleTypeString(
		tVisitor.isabelleTypeString(),
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);
	// Update dyn param only if necessary
	if (tVisitor.isLastDynParamRightPadded())
		m_isLastDynParamRightPadded = true;

	return make_pair(global.str(), local.str());
}

template <typename T>
pair<string, string> ProtoConverter::assignChecker(
	string const& _varName,
	string const& _paramName,
	T _type
)
{
	ostringstream local;
	AssignCheckVisitor acVisitor(
		_varName,
		_paramName,
		m_returnValue,
		m_isStateVar,
		m_counter,
		m_structCounter
	);
	pair<string, string> assignCheckStrPair = acVisitor.visit(_type);
	m_returnValue += acVisitor.errorStmts();
	m_counter += acVisitor.counted();

	m_checks << assignCheckStrPair.second;
	appendToIsabelleValueString(
		acVisitor.isabelleValueString(),
		((m_varCounter == 1) ? Delimiter::SKIP : Delimiter::ADD)
	);

	// State variables cannot be assigned in contract-scope
	// Therefore, we buffer their assignments and
	// render them in function scope later.
	local << assignCheckStrPair.first;
	return make_pair("", local.str());
}

pair<string, string> ProtoConverter::visit(VarDecl const& _x)
{
	return visit(_x.type());
}

std::string ProtoConverter::equalityChecksAsString()
{
	return m_checks.str();
}

std::string ProtoConverter::delimiterToString(Delimiter _delimiter, bool _space)
{
	switch (_delimiter)
	{
	case Delimiter::ADD:
		return _space ? ", " : ",";
	case Delimiter::SKIP:
		return "";
	}
}

/* When a new variable is declared, we can invoke this function
 * to prepare the typed param list to be passed to callee functions.
 * We independently prepare this list for "public" and "external"
 * callee functions.
 */
void ProtoConverter::appendTypedParams(
	CalleeType _calleeType,
	bool _isValueType,
	std::string const& _typeString,
	std::string const& _varName,
	Delimiter _delimiter
)
{
	switch (_calleeType)
	{
	case CalleeType::PUBLIC:
		appendTypedParamsPublic(_isValueType, _typeString, _varName, _delimiter);
		break;
	case CalleeType::EXTERNAL:
		appendTypedParamsExternal(_isValueType, _typeString, _varName, _delimiter);
		break;
	}
}

void ProtoConverter::appendTypes(
	bool _isValueType,
	string const& _typeString,
	Delimiter _delimiter
)
{
	string qualifiedTypeString = (
		_isValueType ?
		_typeString :
		_typeString + " memory"
	);
	m_types << Whiskers(R"(<delimiter><type>)")
		("delimiter", delimiterToString(_delimiter))
		("type", qualifiedTypeString)
		.render();
}

void ProtoConverter::appendTypedReturn(
	bool _isValueType,
	string const& _typeString,
	Delimiter _delimiter
)
{
	string qualifiedTypeString = (
		_isValueType ?
		_typeString :
		_typeString + " memory"
	);
	m_typedReturn << Whiskers(R"(<delimiter><type> <varName>)")
		("delimiter", delimiterToString(_delimiter))
		("type", qualifiedTypeString)
		("varName", "lv_" + to_string(m_varCounter - 1))
		.render();
}

// Adds the qualifier "calldata" to non-value parameter of an external function.
void ProtoConverter::appendTypedParamsExternal(
	bool _isValueType,
    std::string const& _typeString,
    std::string const& _varName,
    Delimiter _delimiter
)
{
	std::string qualifiedTypeString = (
		_isValueType ?
		_typeString :
		_typeString + " calldata"
	);
	m_typedParamsExternal << Whiskers(R"(<delimiter><type> <varName>)")
		("delimiter", delimiterToString(_delimiter))
		("type", qualifiedTypeString)
		("varName", _varName)
		.render();
}

// Adds the qualifier "memory" to non-value parameter of an external function.
void ProtoConverter::appendTypedParamsPublic(
	bool _isValueType,
	std::string const& _typeString,
	std::string const& _varName,
	Delimiter _delimiter
)
{
	std::string qualifiedTypeString = (
		_isValueType ?
		_typeString :
		_typeString + " memory"
		);
	m_typedParamsPublic << Whiskers(R"(<delimiter><type> <varName>)")
		("delimiter", delimiterToString(_delimiter))
		("type", qualifiedTypeString)
		("varName", _varName)
		.render();
}

void ProtoConverter::appendToIsabelleTypeString(
	std::string const& _typeString,
	Delimiter _delimiter
)
{
	m_isabelleTypeString << delimiterToString(_delimiter, false) << _typeString;
}

void ProtoConverter::appendToIsabelleValueString(
	std::string const& _valueString,
	Delimiter _delimiter
)
{
	m_isabelleValueString << delimiterToString(_delimiter, false) << _valueString;
}

std::string ProtoConverter::typedParametersAsString(CalleeType _calleeType)
{
	switch (_calleeType)
	{
	case CalleeType::PUBLIC:
		return m_typedParamsPublic.str();
	case CalleeType::EXTERNAL:
		return m_typedParamsExternal.str();
	}
}

string ProtoConverter::visit(TestFunction const& _x, string const& _storageVarDefs)
{
	// TODO: Support more than one but less than N local variables
	auto localVarBuffers = visit(_x.local_vars());

	string structTypeDecl = localVarBuffers.first;
	string localVarDefs = localVarBuffers.second;

	ostringstream testBuffer;

	string testFunction = Whiskers(R"(
	function test() public returns (uint) {
		<?calldata>return test_calldata_coding();</calldata>
		<?returndata>return test_returndata_coding();</returndata>
	})")
	("calldata", m_test == Contract_Test::Contract_Test_CALLDATA_CODER)
	("returndata", m_test == Contract_Test::Contract_Test_RETURNDATA_CODER)
	.render();

	string functionDeclCalldata = "function test_calldata_coding() internal returns (uint)";
	string functionDeclReturndata = "function test_returndata_coding() internal returns (uint)";

	testBuffer << Whiskers(R"(<structTypeDecl>
<testFunction>
<?calldata>
	<functionDeclCalldata> {
<storageVarDefs>
<localVarDefs>
<calldataTestCode>
	}
<calldataHelperFuncs>
</calldata>
<?returndata>
	<functionDeclReturndata> {
<returndataTestCode>
	}

<?varsPresent>
	function coder_returndata_external() external returns (<return_types>) {
<storageVarDefs>
<localVarDefs>
		return (<return_values>);
	}
</varsPresent>
</returndata>)")
		("testFunction", testFunction)
		("calldata", m_test == Contract_Test::Contract_Test_CALLDATA_CODER)
		("returndata", m_test == Contract_Test::Contract_Test_RETURNDATA_CODER)
		("calldataHelperFuncs", calldataHelperFunctions())
		("varsPresent", !m_types.str().empty())
		("structTypeDecl", structTypeDecl)
		("functionDeclCalldata", functionDeclCalldata)
		("functionDeclReturndata", functionDeclReturndata)
		("storageVarDefs", _storageVarDefs)
		("localVarDefs", localVarDefs)
		("calldataTestCode", testCallDataFunction(static_cast<unsigned>(_x.invalid_encoding_length())))
		("returndataTestCode", testReturnDataFunction())
		("return_types", m_types.str())
		("return_values", m_argsCoder.str())
		.render();
	return testBuffer.str();
}

string ProtoConverter::testCallDataFunction(unsigned _invalidLength)
{
	return Whiskers(R"(
		uint returnVal = this.coder_calldata_public(<argumentNames>);
		if (returnVal != 0)
			return returnVal;

		returnVal = this.coder_calldata_external(<argumentNames>);
		if (returnVal != 0)
			return uint(200000) + returnVal;

		<?atLeastOneVar>
		bytes memory argumentEncoding = abi.encode(<argumentNames>);

		returnVal = checkEncodedCall(
			this.coder_calldata_public.selector,
			argumentEncoding,
			<invalidLengthFuzz>,
			<isRightPadded>
		);
		if (returnVal != 0)
			return returnVal;

		returnVal = checkEncodedCall(
			this.coder_calldata_external.selector,
			argumentEncoding,
			<invalidLengthFuzz>,
			<isRightPadded>
		);
		if (returnVal != 0)
			return uint(200000) + returnVal;
		</atLeastOneVar>
		return 0;
		)")
		("argumentNames", m_argsCoder.str())
		("invalidLengthFuzz", std::to_string(_invalidLength))
		("isRightPadded", isLastDynParamRightPadded() ? "true" : "false")
		("atLeastOneVar", m_varCounter > 0)
		.render();
}

string ProtoConverter::testReturnDataFunction()
{
	return Whiskers(R"(
<?varsPresent>
		(<varDecl>) = this.coder_returndata_external();
<equality_checks>
</varsPresent>
		return 0;
		)")
		("varsPresent", !m_typedReturn.str().empty())
		("varDecl", m_typedReturn.str())
		("equality_checks", m_checks.str())
		.render();
}

string ProtoConverter::calldataHelperFunctions()
{
	stringstream calldataHelperFuncs;
	calldataHelperFuncs << R"(
	/// Accepts function selector, correct argument encoding, and length of
	/// invalid encoding and returns the correct and incorrect abi encoding
	/// for calling the function specified by the function selector.
	function createEncoding(
		bytes4 funcSelector,
		bytes memory argumentEncoding,
		uint invalidLengthFuzz,
		bool isRightPadded
	) internal pure returns (bytes memory, bytes memory)
	{
		bytes memory validEncoding = new bytes(4 + argumentEncoding.length);
		// Ensure that invalidEncoding crops at least 32 bytes (padding length
		// is at most 31 bytes) if `isRightPadded` is true.
		// This is because shorter bytes/string values (whose encoding is right
		// padded) can lead to successful decoding when fewer than 32 bytes have
		// been cropped in the worst case. In other words, if `isRightPadded` is
		// true, then
		//  0 <= invalidLength <= argumentEncoding.length - 32
		// otherwise
		//  0 <= invalidLength <= argumentEncoding.length - 1
		uint invalidLength;
		if (isRightPadded)
			invalidLength = invalidLengthFuzz % (argumentEncoding.length - 31);
		else
			invalidLength = invalidLengthFuzz % argumentEncoding.length;
		bytes memory invalidEncoding = new bytes(4 + invalidLength);
		for (uint i = 0; i < 4; i++)
			validEncoding[i] = invalidEncoding[i] = funcSelector[i];
		for (uint i = 0; i < argumentEncoding.length; i++)
			validEncoding[i+4] = argumentEncoding[i];
		for (uint i = 0; i < invalidLength; i++)
			invalidEncoding[i+4] = argumentEncoding[i];
		return (validEncoding, invalidEncoding);
	}

	/// Accepts function selector, correct argument encoding, and an invalid
	/// encoding length as input. Returns a non-zero value if either call with
	/// correct encoding fails or call with incorrect encoding succeeds.
	/// Returns zero if both calls meet expectation.
	function checkEncodedCall(
		bytes4 funcSelector,
		bytes memory argumentEncoding,
		uint invalidLengthFuzz,
		bool isRightPadded
	) public returns (uint)
	{
		(bytes memory validEncoding, bytes memory invalidEncoding) = createEncoding(
			funcSelector,
			argumentEncoding,
			invalidLengthFuzz,
			isRightPadded
		);
		(bool success, bytes memory returnVal) = address(this).call(validEncoding);
		uint returnCode = abi.decode(returnVal, (uint));
		// Return non-zero value if call fails for correct encoding
		if (success == false || returnCode != 0)
			return 400000;
		(success, ) = address(this).call(invalidEncoding);
		// Return non-zero value if call succeeds for incorrect encoding
		if (success == true)
			return 400001;
		return 0;
	})";

	// These are callee functions that encode from storage, decode to
	// memory/calldata and check if decoded value matches storage value
	// return true on successful match, false otherwise
	calldataHelperFuncs << Whiskers(R"(
	function coder_calldata_public(<parameters_memory>) public pure returns (uint) {
<equality_checks>
		return 0;
	}

	function coder_calldata_external(<parameters_calldata>) external pure returns (uint) {
<equality_checks>
		return 0;
	}
	)")
	("parameters_memory", typedParametersAsString(CalleeType::PUBLIC))
	("equality_checks", equalityChecksAsString())
	("parameters_calldata", typedParametersAsString(CalleeType::EXTERNAL))
	.render();

	return calldataHelperFuncs.str();
}

string ProtoConverter::commonHelperFunctions()
{
	stringstream helperFuncs;
	helperFuncs << R"(
	/// Compares bytes, returning true if they are equal and false otherwise.
	function bytesCompare(bytes memory a, bytes memory b) internal pure returns (bool) {
		if(a.length != b.length)
			return false;
		for (uint i = 0; i < a.length; i++)
			if (a[i] != b[i])
				return false;
		return true;
	}
	)";

	return helperFuncs.str();
}

void ProtoConverter::visit(Contract const& _x)
{
	string pragmas = R"(pragma solidity >=0.0;
pragma experimental ABIEncoderV2;)";

	// Record test spec
	m_test = _x.test();

	// TODO: Support more than one but less than N state variables
	auto storageBuffers = visit(_x.state_vars());
	string storageVarDecls = storageBuffers.first;
	string storageVarDefs = storageBuffers.second;
	m_isStateVar = false;
	string testFunction = visit(_x.testfunction(), storageVarDefs);
	/* Structure of contract body
	 * - Storage variable declarations
	 * - Struct definitions
	 * - Test function
	 *     - Storage variable assignments
	 *     - Local variable definitions and assignments
	 *     - Test code proper (calls public and external functions)
	 * - Helper functions
	 */
	ostringstream contractBody;
	contractBody << storageVarDecls
	             << testFunction
	             << commonHelperFunctions();
	m_output << Whiskers(R"(<pragmas>
<contractStart>
<contractBody>
<contractEnd>)")
		("pragmas", pragmas)
		("contractStart", "contract C {")
		("contractBody", contractBody.str())
		("contractEnd", "}")
		.render();
}

string ProtoConverter::isabelleTypeString() const
{
	string typeString = m_isabelleTypeString.str();
	if (!typeString.empty())
		return "(" + typeString + ")";
	else
		return typeString;
}

string ProtoConverter::isabelleValueString() const
{
	string valueString = m_isabelleValueString.str();
	if (!valueString.empty())
		return "(" + valueString + ")";
	else
		return valueString;
}

string ProtoConverter::contractToString(Contract const& _input)
{
	visit(_input);
	return m_output.str();
}

/// Type visitor
void TypeVisitor::StructTupleString::addTypeStringToTuple(string& _typeString)
{
	index++;
	if (index > 1)
		stream << ",";
	stream << _typeString;
}

void TypeVisitor::StructTupleString::addArrayBracketToType(string& _arrayBracket)
{
	stream << _arrayBracket;
}

string TypeVisitor::visit(BoolType const&)
{
	m_baseType = "bool";
	m_structTupleString.addTypeStringToTuple(m_baseType);
	return m_baseType;
}

string TypeVisitor::visit(IntegerType const& _type)
{
	m_baseType = getIntTypeAsString(_type);
	m_structTupleString.addTypeStringToTuple(m_baseType);
	return m_baseType;
}

string TypeVisitor::visit(FixedByteType const& _type)
{
	m_baseType = getFixedByteTypeAsString(_type);
	m_structTupleString.addTypeStringToTuple(m_baseType);
	return m_baseType;
}

string TypeVisitor::visit(AddressType const&)
{
	m_baseType = "address";
	m_structTupleString.addTypeStringToTuple(m_baseType);
	return m_baseType;
}

string TypeVisitor::visit(ArrayType const& _type)
{
	if (!ValidityVisitor().visit(_type))
		return "";

	string baseType = visit(_type.t());
	solAssert(!baseType.empty(), "");
	string arrayBracket = _type.is_static() ?
	                     string("[") +
	                     to_string(getStaticArrayLengthFromFuzz(_type.length())) +
	                     string("]") :
	                     string("[]");
	m_baseType += arrayBracket;
	m_structTupleString.addArrayBracketToType(arrayBracket);

	// If we don't know yet if the array will be dynamically encoded,
	// check again. If we already know that it will be, there's no
	// need to do anything.
	if (!m_isLastDynParamRightPadded)
		m_isLastDynParamRightPadded = DynParamVisitor().visit(_type);

	return baseType + arrayBracket;
}

string TypeVisitor::visit(DynamicByteArrayType const&)
{
	m_isLastDynParamRightPadded = true;
	m_baseType = "bytes";
	m_structTupleString.addTypeStringToTuple(m_baseType);
	return m_baseType;
}

void TypeVisitor::structDefinition(StructType const& _type)
{
	// Return an empty string if struct is empty
	solAssert(ValidityVisitor().visit(_type), "");

	// Reset field counter and indentation
	unsigned wasFieldCounter = m_structFieldCounter;
	unsigned wasIndentation = m_indentation;

	m_indentation = 1;
	m_structFieldCounter = 0;

	// Commence struct declaration
	string structDef = lineString(
		"struct " +
		string(s_structNamePrefix) +
		to_string(m_structCounter) +
		" {"
	);
	// Start tuple of types with parenthesis
	m_structTupleString.start();
	// Increase indentation for struct fields
	m_indentation++;
	for (auto const& t: _type.t())
	{
		string type{};

		if (!ValidityVisitor().visit(t))
			continue;

		TypeVisitor tVisitor(m_structCounter + 1);
		type = tVisitor.visit(t);
		m_structCounter += tVisitor.numStructs();
		m_structDef << tVisitor.structDef();

		solAssert(!type.empty(), "");

		structDef += lineString(
			Whiskers(R"(<type> <member>;)")
				("type", type)
				("member", "m" + to_string(m_structFieldCounter++))
				.render()
		);
		string isabelleTypeStr = tVisitor.isabelleTypeString();
		m_structTupleString.addTypeStringToTuple(isabelleTypeStr);
	}
	m_indentation--;
	structDef += lineString("}");
	// End tuple of types with parenthesis
	m_structTupleString.end();
	m_structCounter++;
	m_structDef << structDef;
	m_indentation = wasIndentation;
	m_structFieldCounter = wasFieldCounter;
}

string TypeVisitor::visit(StructType const& _type)
{
	if (ValidityVisitor().visit(_type))
	{
		// Add struct definition
		structDefinition(_type);
		// Set last dyn param if struct contains a dyn param e.g., bytes, array etc.
		m_isLastDynParamRightPadded = DynParamVisitor().visit(_type);
		// If top-level struct is a non-emtpy struct, assign the name S<suffix>
		m_baseType = s_structTypeName + to_string(m_structStartCounter);
	}
	else
		m_baseType = {};

	return m_baseType;
}

/// AssignCheckVisitor implementation
void AssignCheckVisitor::ValueStream::appendValue(string& _value)
{
	solAssert(!_value.empty(), "Abiv2 fuzzer: Empty value");
	index++;
	if (index > 1)
		stream << ",";
	stream << _value;
}

pair<string, string> AssignCheckVisitor::visit(BoolType const& _type)
{
	string value = ValueGetterVisitor(counter()).visit(_type);
	if (!m_forcedVisit)
		m_valueStream.appendValue(value);
	return assignAndCheckStringPair(m_varName, m_paramName, value, value, DataType::VALUE);
}

pair<string, string> AssignCheckVisitor::visit(IntegerType const& _type)
{
	string value = ValueGetterVisitor(counter()).visit(_type);
	if (!m_forcedVisit)
		m_valueStream.appendValue(value);
	return assignAndCheckStringPair(m_varName, m_paramName, value, value, DataType::VALUE);
}

pair<string, string> AssignCheckVisitor::visit(FixedByteType const& _type)
{
	string value = ValueGetterVisitor(counter()).visit(_type);
	if (!m_forcedVisit)
	{
		string isabelleValue = ValueGetterVisitor{}.isabelleBytesValueAsString(value);
		m_valueStream.appendValue(isabelleValue);
	}
	return assignAndCheckStringPair(m_varName, m_paramName, value, value, DataType::VALUE);
}

pair<string, string> AssignCheckVisitor::visit(AddressType const& _type)
{
	string value = ValueGetterVisitor(counter()).visit(_type);
	if (!m_forcedVisit)
	{
		string isabelleValue = ValueGetterVisitor{}.isabelleAddressValueAsString(value);
		m_valueStream.appendValue(isabelleValue);
	}
	return assignAndCheckStringPair(m_varName, m_paramName, value, value, DataType::VALUE);
}

pair<string, string> AssignCheckVisitor::visit(DynamicByteArrayType const& _type)
{
	string value = ValueGetterVisitor(counter()).visit(_type);
	if (!m_forcedVisit)
	{
		string isabelleValue = ValueGetterVisitor{}.isabelleBytesValueAsString(value);
		m_valueStream.appendValue(isabelleValue);
	}
	DataType dataType = DataType::BYTES;
	return assignAndCheckStringPair(m_varName, m_paramName, value, value, dataType);
}

pair<string, string> AssignCheckVisitor::visit(ArrayType const& _type)
{
	if (!ValidityVisitor().visit(_type))
		return make_pair("", "");

	// Obtain type of array to be resized and initialized
	string typeStr{};

	unsigned wasStructCounter = m_structCounter;
	TypeVisitor tVisitor(m_structCounter);
	typeStr = tVisitor.visit(_type);

	pair<string, string> resizeBuffer;
	string lengthStr;
	unsigned length;

	// Resize dynamic arrays
	if (!_type.is_static())
	{
		length = getDynArrayLengthFromFuzz(_type.length(), counter());
		lengthStr = to_string(length);
		if (m_stateVar)
		{
			// Dynamic storage arrays are resized via the empty push() operation
			resizeBuffer.first = Whiskers(R"(<indentation>for (uint i = 0; i < <length>; i++) <arrayRef>.push();)")
				("indentation", indentation())
				("length", lengthStr)
				("arrayRef", m_varName)
				.render() + "\n";
			// Add a dynamic check on the resized length
			resizeBuffer.second = checkString(m_paramName + ".length", lengthStr, DataType::VALUE);
		}
		else
		{
			// Resizing memory arrays via the new operator
			string resizeOp = Whiskers(R"(new <fullTypeStr>(<length>))")
				("fullTypeStr", typeStr)
				("length", lengthStr)
				.render();
			resizeBuffer = assignAndCheckStringPair(
				m_varName,
				m_paramName + ".length",
				resizeOp,
				lengthStr,
				DataType::VALUE
				);
		}
	}
	else
	{
		length = getStaticArrayLengthFromFuzz(_type.length());
		lengthStr = to_string(length);
		// Add check on length
		resizeBuffer.second = checkString(m_paramName + ".length", lengthStr, DataType::VALUE);
	}

	// Add assignCheckBuffer and check statements
	pair<string, string> assignCheckBuffer;
	string wasVarName = m_varName;
	string wasParamName = m_paramName;
	if (!m_forcedVisit)
		m_valueStream.startArray();
	for (unsigned i = 0; i < length; i++)
	{
		m_varName = wasVarName + "[" + to_string(i) + "]";
		m_paramName = wasParamName + "[" + to_string(i) + "]";
		pair<string, string> assign = visit(_type.t());
		assignCheckBuffer.first += assign.first;
		assignCheckBuffer.second += assign.second;
		if (i < length - 1)
			m_structCounter = wasStructCounter;
	}
	// Since struct visitor won't be called for zero-length
	// arrays, struct counter will not get incremented. Therefore,
	// we need to manually force a recursive struct visit.
	if (length == 0 && TypeVisitor().arrayOfStruct(_type))
	{
		bool previousState = m_forcedVisit;
		m_forcedVisit = true;
		visit(_type.t());
		m_forcedVisit = previousState;
	}
	if (!m_forcedVisit)
		m_valueStream.endArray();

	m_varName = wasVarName;
	m_paramName = wasParamName;

	// Compose resize and initialization assignment and check
	return make_pair(
		resizeBuffer.first + assignCheckBuffer.first,
		resizeBuffer.second + assignCheckBuffer.second
	);
}

pair<string, string> AssignCheckVisitor::visit(StructType const& _type)
{
	if (!ValidityVisitor().visit(_type))
		return make_pair("", "");

	pair<string, string> assignCheckBuffer;
	unsigned i = 0;

	// Increment struct counter
	m_structCounter++;

	string wasVarName = m_varName;
	string wasParamName = m_paramName;

	if (!m_forcedVisit)
		m_valueStream.startStruct();
	for (auto const& t: _type.t())
	{
		m_varName = wasVarName + ".m" + to_string(i);
		m_paramName = wasParamName + ".m" + to_string(i);
		pair<string, string> assign = visit(t);
		// If type is not well formed continue without
		// updating state.
		if (assign.first.empty() && assign.second.empty())
			continue;
		assignCheckBuffer.first += assign.first;
		assignCheckBuffer.second += assign.second;
		i++;
	}
	if (!m_forcedVisit)
		m_valueStream.endStruct();
	m_varName = wasVarName;
	m_paramName = wasParamName;
	return assignCheckBuffer;
}

pair<string, string> AssignCheckVisitor::assignAndCheckStringPair(
	string const& _varRef,
	string const& _checkRef,
	string const& _assignValue,
	string const& _checkValue,
	DataType _type
)
{
	return make_pair(assignString(_varRef, _assignValue), checkString(_checkRef, _checkValue, _type));
}

string AssignCheckVisitor::assignString(string const& _ref, string const& _value)
{
	string assignStmt = Whiskers(R"(<ref> = <value>;)")
		("ref", _ref)
		("value", _value)
		.render();
	return indentation() + assignStmt + "\n";
}

string AssignCheckVisitor::checkString(string const& _ref, string const& _value, DataType _type)
{
	string checkPred;
	switch (_type)
	{
	case DataType::BYTES:
		checkPred = Whiskers(R"(!bytesCompare(<varName>, <value>))")
			("varName", _ref)
			("value", _value)
			.render();
		break;
	case DataType::VALUE:
		checkPred = Whiskers(R"(<varName> != <value>)")
			("varName", _ref)
			("value", _value)
			.render();
		break;
	case DataType::ARRAY:
		solUnimplemented("Proto ABIv2 fuzzer: Invalid data type.");
	}
	string checkStmt = Whiskers(R"(if (<checkPred>) return <errCode>;)")
		("checkPred", checkPred)
		("errCode", to_string(m_errorCode++))
		.render();
	return indentation() + checkStmt + "\n";
}

/// ValueGetterVisitor
string ValueGetterVisitor::visit(BoolType const&)
{
	return counter() % 2 ? "true" : "false";
}

string ValueGetterVisitor::visit(IntegerType const& _type)
{
	return integerValue(counter(), getIntWidth(_type), _type.is_signed());
}

string ValueGetterVisitor::visit(FixedByteType const& _type)
{
	return fixedByteValueAsString(
		getFixedByteWidth(_type),
		counter()
	);
}

string ValueGetterVisitor::visit(AddressType const&)
{
	return addressValueAsString(counter());
}

string ValueGetterVisitor::visit(DynamicByteArrayType const&)
{
	return bytesArrayValueAsString(
		counter(),
		true
	);
}

std::string ValueGetterVisitor::croppedString(
	unsigned _numBytes,
	unsigned _counter,
	bool _isHexLiteral
)
{
	solAssert(
		_numBytes > 0 && _numBytes <= 32,
		"Proto ABIv2 fuzzer: Too short or too long a cropped string"
	);

	// Number of masked nibbles is twice the number of bytes for a
	// hex literal of _numBytes bytes. For a string literal, each nibble
	// is treated as a character.
	unsigned numMaskNibbles = _isHexLiteral ? _numBytes * 2 : _numBytes;

	// Start position of substring equals totalHexStringLength - numMaskNibbles
	// totalHexStringLength = 64 + 2 = 66
	// e.g., 0x12345678901234567890123456789012 is a total of 66 characters
	//      |---------------------^-----------|
	//      <--- start position---><--numMask->
	//      <-----------total length --------->
	// Note: This assumes that maskUnsignedIntToHex() invokes toHex(..., HexPrefix::Add)
	unsigned startPos = 66 - numMaskNibbles;
	// Extracts the least significant numMaskNibbles from the result
	// of maskUnsignedIntToHex().
	return maskUnsignedIntToHex(
		_counter,
		numMaskNibbles
	).substr(startPos, numMaskNibbles);
}

std::string ValueGetterVisitor::hexValueAsString(
	unsigned _numBytes,
	unsigned _counter,
	bool _isHexLiteral,
	bool _decorate
)
{
	solAssert(_numBytes > 0 && _numBytes <= 32,
	          "Proto ABIv2 fuzzer: Invalid hex length"
	);

	// If _decorate is set, then we return a hex"" or a "" string.
	if (_numBytes == 0)
		return Whiskers(R"(<?decorate><?isHex>hex</isHex>""</decorate>)")
			("decorate", _decorate)
			("isHex", _isHexLiteral)
			.render();

	// This is needed because solidity interprets a 20-byte 0x prefixed hex literal as an address
	// payable type.
	return Whiskers(R"(<?decorate><?isHex>hex</isHex>"</decorate><value><?decorate>"</decorate>)")
		("decorate", _decorate)
		("isHex", _isHexLiteral)
		("value", croppedString(_numBytes, _counter, _isHexLiteral))
		.render();
}

std::string ValueGetterVisitor::fixedByteValueAsString(unsigned _width, unsigned _counter)
{
	solAssert(
		(_width >= 1 && _width <= 32),
		"Proto ABIv2 Fuzzer: Fixed byte width is not between 1--32"
	);
	return hexValueAsString(_width, _counter, /*isHexLiteral=*/true);
}

std::string ValueGetterVisitor::addressValueAsString(unsigned _counter)
{
	return "address(" + maskUnsignedIntToHex(_counter, 40) + ")";
}

std::string ValueGetterVisitor::isabelleAddressValueAsString(std::string& _solAddressString)
{
	// Isabelle encoder expects address literal to be exactly
	// 20 bytes and a hex string.
	// Example: 0x0102030405060708090a0102030405060708090a
	std::regex const addressPattern("address\\((.*)\\)");
	std::smatch match;
	solAssert(std::regex_match(_solAddressString, match, addressPattern), "Abiv2 fuzzer: Invalid address string");
	std::string addressHex = match[1].str();
	addressHex.erase(2, 24);
	return addressHex;
}

std::string ValueGetterVisitor::isabelleBytesValueAsString(std::string& _solBytesString)
{
	std::regex const bytesPattern("hex\"(.*)\"");
	std::smatch match;
	solAssert(std::regex_match(_solBytesString, match, bytesPattern), "Abiv2 fuzzer: Invalid bytes string");
	std::string bytesHex = match[1].str();
	return "0x" + bytesHex;
}

std::string ValueGetterVisitor::variableLengthValueAsString(
	unsigned _numBytes,
	unsigned _counter,
	bool _isHexLiteral
)
{
	if (_numBytes == 0)
		return Whiskers(R"(<?isHex>hex</isHex>"")")
			("isHex", _isHexLiteral)
			.render();

	unsigned numBytesRemaining = _numBytes;
	// Stores the literal
	string output{};
	// If requested value is shorter than or exactly 32 bytes,
	// the literal is the return value of hexValueAsString.
	if (numBytesRemaining <= 32)
		output = hexValueAsString(
			numBytesRemaining,
			_counter,
			_isHexLiteral,
			/*decorate=*/false
		);
		// If requested value is longer than 32 bytes, the literal
		// is obtained by duplicating the return value of hexValueAsString
		// until we reach a value of the requested size.
	else
	{
		// Create a 32-byte value to be duplicated and
		// update number of bytes to be appended.
		// Stores the cached literal that saves us
		// (expensive) calls to keccak256.
		string cachedString = hexValueAsString(
			/*numBytes=*/32,
			             _counter,
			             _isHexLiteral,
			/*decorate=*/false
		);
		output = cachedString;
		numBytesRemaining -= 32;

		// Append bytes from cachedString until
		// we create a value of desired length.
		unsigned numAppendedBytes;
		while (numBytesRemaining > 0)
		{
			// We append at most 32 bytes at a time
			numAppendedBytes = numBytesRemaining >= 32 ? 32 : numBytesRemaining;
			output += cachedString.substr(
				0,
				// Double the substring length for hex literals since each
				// character is actually half a byte (or a nibble).
				_isHexLiteral ? numAppendedBytes * 2 : numAppendedBytes
			);
			numBytesRemaining -= numAppendedBytes;
		}
		solAssert(
			numBytesRemaining == 0,
			"Proto ABIv2 fuzzer: Logic flaw in variable literal creation"
		);
	}

	if (_isHexLiteral)
		solAssert(
			output.size() == 2 * _numBytes,
			"Proto ABIv2 fuzzer: Generated hex literal is of incorrect length"
		);
	else
		solAssert(
			output.size() == _numBytes,
			"Proto ABIv2 fuzzer: Generated string literal is of incorrect length"
		);

	// Decorate output
	return Whiskers(R"(<?isHexLiteral>hex</isHexLiteral>"<value>")")
		("isHexLiteral", _isHexLiteral)
		("value", output)
		.render();
}

string ValueGetterVisitor::bytesArrayValueAsString(unsigned _counter, bool _isHexLiteral)
{
	return variableLengthValueAsString(
		getVarLength(_counter),
		_counter,
		_isHexLiteral
	);
}
