/**
* I provide a Lucee CFML wrapper around the Password4j Java library.
* 
* GitHub: https://github.com/Password4j/password4j
* Maven: https://mvnrepository.com/artifact/com.password4j/password4j
*/
component
	output = false
	hint = "I provide password hashing and verification functions (using Password4j)."
	{

	variables.defaults = {
		// These properties are described here:
		// - https://github.com/Password4j/password4j/wiki/Argon2
		argon2: {
			memory: 12,
			iterations: 20,
			parallelisation: 2,
			ouputLength: 32,
			type: "Argon2id",
			version: 19
		},
		// These properties are described here:
		// - https://github.com/Password4j/password4j/wiki/BCrypt
		bcrypt: {
			version: "B",
			costFactor: 10
		},
		// These properties are described here:
		// - https://github.com/Password4j/password4j/wiki/Scrypt
		scrypt: {
			workFactor: 32768,
			resources: 8,
			parallelisation: 1,
			outputLength: 64,
			// Added for Adobe ColdFusion 2021 interoperability.
			saltLength: 8
		}
	};

	/**
	* I initialize the password component with the given Password4j JAR paths.
	*/
	public void function init( required array jarPaths ) {

		variables.jarPaths = arguments.jarPaths;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I generate an Argon2 hash of the given input using the given characteristics. All
	* characteristics are optional; and, any not provided will use the defined defaults.
	*/
	public string function argon2HashGet(
		required string input,
		numeric memory = defaults.argon2.memory,
		numeric iterations = defaults.argon2.iterations,
		numeric parallelisation = defaults.argon2.parallelisation,
		numeric ouputLength = defaults.argon2.ouputLength,
		string type = defaults.argon2.type,
		numeric version = defaults.argon2.version
		) {

		var enum = javaNew( "com.password4j.types.Argon2" );

		switch ( type ) {
			case "Argon2d":
			case "Argon2i":
			case "Argon2id":
				var enumToken = type.listRest( "2" ).ucase();
				var enumType = enum[ enumToken ];
			break;
			default:
				throw(
					type = "InvalidArgon2Type",
					message = "Valid argon2 types are: Argon2d, Argon2i, Argon2id (recommended)",
					detail = "Provided type: #type#"
				);
			break;
		}

		var hashingFunction = javaNew( "com.password4j.Argon2Function" )
			.getInstance( memory, iterations, parallelisation, ouputLength, enumType, version )
		;
		var hashedInput = javaNew( "com.password4j.Password" )
			.hash( input )
			.with( hashingFunction )
			.getResult()
		;

		return( hashedInput );

	}


	/**
	* I verify the Argon2 hash of the given input against the expected hash.
	* 
	* NOTE: All hash-algorithm characteristics will be pulled directly out of the
	* expected hash. As such, they do not need to be provided as arguments.
	*/
	public boolean function argon2HashVerify(
		required string input,
		required string hashedInput
		) {

		var hashingFunction = javaNew( "com.password4j.Argon2Function" )
			.getInstanceFromHash( hashedInput )
		;
		var isVerified = javaNew( "com.password4j.Password" )
			.check( input, hashedInput )
			.with( hashingFunction )
		;

		return( isVerified );

	}


	/**
	* I generate a BCrypt hash of the given input using the given characteristics. All
	* characteristics are optional; and, any not provided will use the defined defaults.
	*/
	public string function bcryptHashGet(
		required string input,
		string version = variables.defaults.bcrypt.version,
		numeric costFactor = variables.defaults.bcrypt.costFactor
		) {

		var enum = javaNew( "com.password4j.types.BCrypt" );

		switch ( version ) {
			case "a":
			case "b":
			case "x":
			case "y":
				var enumToken = version.ucase();
				var enumVersion = enum[ enumToken ];
			break;
			default:
				throw(
					type = "InvalidBcryptVersion",
					message = "Valid bcrypt versions are: a, b (recommended), x, y",
					detail = "Provided version: #version#"
				);
			break;
		}

		var hashingFunction = javaNew( "com.password4j.BCryptFunction" )
			.getInstance( enumVersion, costFactor )
		;
		var hashedInput = javaNew( "com.password4j.Password" )
			.hash( input )
			.with( hashingFunction )
			.getResult()
		;

		return( hashedInput );

	}


	/**
	* I verify the BCrypt hash of the given input against the expected hash.
	* 
	* NOTE: All hash-algorithm characteristics will be pulled directly out of the
	* expected hash. As such, they do not need to be provided as arguments.
	*/
	public boolean function bcryptHashVerify(
		required string input,
		required string hashedInput
		) {

		var hashingFunction = javaNew( "com.password4j.BCryptFunction" )
			.getInstanceFromHash( hashedInput )
		;
		var isVerified = javaNew( "com.password4j.Password" )
			.check( input, hashedInput )
			.with( hashingFunction )
		;

		return( isVerified );

	}


	/**
	* I generate a SCrypt hash of the given input using the given characteristics. All
	* characteristics are optional; and, any not provided will use the defined defaults.
	*/
	public string function scryptHashGet(
		required string input,
		numeric workFactor = defaults.scrypt.workFactor,
		numeric resources = defaults.scrypt.resources,
		numeric parallelisation = defaults.scrypt.parallelisation,
		numeric outputLength = defaults.scrypt.outputLength,
		numeric saltLength = defaults.scrypt.saltLength
		) {

		var hashingFunction = javaNew( "com.password4j.SCryptFunction" )
			.getInstance( workFactor, resources, parallelisation, outputLength )
		;
		var hashedInput = javaNew( "com.password4j.Password" )
			.hash( input )
			.addRandomSalt( saltLength )
			.with( hashingFunction )
			.getResult()
		;

		return( hashedInput );

	}


	/**
	* I verify the SCrypt hash of the given input against the expected hash.
	* 
	* NOTE: All hash-algorithm characteristics will be pulled directly out of the
	* expected hash. As such, they do not need to be provided as arguments.
	*/
	public boolean function scryptHashVerify(
		required string input,
		required string hashedInput
		) {

		var hashingFunction = javaNew( "com.password4j.SCryptFunction" )
			.getInstanceFromHash( hashedInput )
		;

		var isVerified = javaNew( "com.password4j.Password" )
			.check( input, hashedInput )
			.with( hashingFunction )
		;

		return( isVerified );

	}


	/**
	* I update the default arguments for the Argon2 hashing method.
	*/
	public any function withArgon2Defaults(
		numeric memory = defaults.argon2.memory,
		numeric iterations = defaults.argon2.iterations,
		numeric parallelisation = defaults.argon2.parallelisation,
		numeric ouputLength = defaults.argon2.ouputLength,
		string type = defaults.argon2.type,
		numeric version = defaults.argon2.version
		) {

		defaults.argon2.memory = memory;
		defaults.argon2.iterations = iterations;
		defaults.argon2.parallelisation = parallelisation;
		defaults.argon2.ouputLength = ouputLength;
		defaults.argon2.type = type;
		defaults.argon2.version = version;

		return( this );

	}


	/**
	* I update the default arguments for the BCrypt hashing method.
	*/
	public any function withBCryptDefaults(
		string version = defaults.bcrypt.version,
		string costFactor = defaults.bcrypt.costFactor
		) {

		defaults.bcrypt.version = version;
		defaults.bcrypt.costFactor = costFactor;

		return( this );

	}


	/**
	* I update the default arguments for the SCrypt hashing method.
	*/
	public any function withSCryptDefaults(
		numeric workFactor = defaults.scrypt.workFactor,
		numeric resources = defaults.scrypt.resources,
		numeric parallelisation = defaults.scrypt.parallelisation,
		numeric outputLength = defaults.scrypt.outputLength,
		numeric saltLength = defaults.scrypt.saltLength
		) {

		defaults.scrypt.workFactor = workFactor;
		defaults.scrypt.resources = resources;
		defaults.scrypt.parallelisation = parallelisation;
		defaults.scrypt.outputLength = outputLength;
		defaults.scrypt.saltLength = saltLength;

		return( this );

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I create the given Java class using the Password4j JAR paths.
	*/
	private any function javaNew( required string className ) {

		return( createObject( "java", className, jarPaths ) );

	}

}