import io


def s_map(p_string, domain, mapping):
	"""
	Replaces all characters of the `domain` in `p_string` with their
	respective mapping in `mapping`. The length of the domain string
	must be the same as the mapping string unless the mapping string
	is empty or a single character, in which case all domain 
	characters will	be either replaced or substituted with that mapping 
	character in the `p_string`.

	:param p_string
		The string whose characters in domain to substitute with their
		respective mappings in `mapping`.
	
	:param domain
		The string of characters to replace in the `p_string`. If some
		characters reappear, this will not affect the substitution process,
		the extra characters are simply ignored.

	:param mapping
		The corresponding mapping of the `domain`. This must match the
		length of the `domain` string, empty, or be a single character 
		to which all domain characters will be mapped to.
	
	If any of `p_string`, `domain`, or `mapping` are None, this function
	does nothing and simply returns `p_string`.

	If len(mapping) != len(domain) and len(mapping) > 1, this function
	raises a ValueError.
	"""
	if p_string is None or domain is None or mapping is None:
		return p_string

	res = io.StringIO()

	# void mapping
	if len(mapping) == 0:
		for c in p_string:
			if c not in domain:
				res.write(c)
	# surjective mapping
	elif len(mapping) == 1:
		for c in p_string:
			if c in domain:
				res.write(mapping)
			else:
				res.write(c)
	# injective mapping
	elif len(mapping) == len(domain):
		for c in p_string:
			pos = domain.find(c)
			if pos != -1:
				res.write(mapping[pos])
			else:
				res.write(c)
	else:
		raise ValueError("len(mapping) > 1 and len(mapping) != len(domain)")

	return res.getvalue()