(
var substitute, resamp, read, readPos = [0, 0];

// this function is the tiler
// it is essentially the generator of the underlying structure of the piece and all the variations
substitute = {arg parent, transform; var s, t, u, v, sub;
	s = 4.collect({arg i; parent[0] + ((parent[i] - parent[0]) / 2)});
	t = [parent[2] + ((parent[3] - parent[2]) / 2), parent[2], s[3], s[2]];
	u = [t[0], s[1], t[0] + ((parent[1] - s[1]) / 2), s[1] + ((parent[1] - s[1]) / 2)];
	v = [parent[1], parent[3], u[3], u[2]];
	sub = [s, t, u, v].collect({arg child, i; if(transform[i] == nil, {nil},
		{4.collect({arg j; child[((j.asDigits(2, 2) + transform[i].asDigits(2, 2)) % 2).convertDigits(2)]})})});
	sub.removeEvery([nil])};

// these functions either tile a specific depth (mapDepth) or up to a specific depth (mapAll)
~mapDepth = {arg depth, transform; var tiles = [[[0, 0], [2, 0], [0, 1], [2, 1]]];
	depth.do({arg i; tiles = tiles.collect({arg tile; substitute.value(tile, transform)}).flatten});
	[tiles]};

~mapAll = {arg depth, transform; var tiles = Array.fill(depth + 1, {[]});
	tiles[0].add([[0, 0], [2, 0], [0, 1], [2, 1]]);
	depth.do({arg i; tiles[i].do({arg tile; tiles[i + 1] = tiles[i + 1].addAll(substitute.value(tile, transform))})});
	tiles};

// utility to resample the matrix output by ~matricize
resamp = {arg matrixOrig, resampLevel; var mult, matrixNew = [];
	mult = pow(2, resampLevel);
	matrixOrig.do({arg rowOrig; var rowNew = [];
		rowOrig.do({arg elem; mult.do({rowNew = rowNew.add(elem)})});
		mult.do({matrixNew = matrixNew.add(rowNew)})});
	matrixNew};

// function to turn the tile map into a matrix of 1s and 0s by reading blocks of 2 vertical or horizontal tiles
~matricize = {
	arg tileMap, depth = 6, resampLevel = 0, transform;
	var rotationState = transform[4], mirrorState = transform[5], invertState = transform[6], matrix;
	matrix = Array.fill2D(pow(2, depth - 1), pow(2, depth), {0});
	tileMap[tileMap.size-1].do({arg tile; var transpose, xMin, xMax, yMin, yMax;
		transpose = (tile * pow(2, depth)).flop;
		xMin = minItem(transpose[0]); xMax = maxItem(transpose[0]);
		yMin = minItem(transpose[1]); yMax = maxItem(transpose[1]);
		if(xMin + yMin % 2 == 1, {if(xMax - xMin == 1,
			{matrix[yMin / 2][xMin / 2] = ((invertState + 1) % 2)}, {matrix[yMin / 2][xMin / 2] = invertState})})});
	if(resampLevel > 0, {matrix = resamp.value(matrix, resampLevel)});
	matrix = switch(rotationState, 0, {matrix}, 1, {matrix.flop},
		2, {matrix.reverse.flop.reverse.flop}, 3, {matrix.reverse.flop});
	if(mirrorState == 1, {matrix = matrix.flop.reverse.flop});
	matrix};

// tape reading model that reads matrix from ~genData function
// wordShift and letterShift are only used if readPos is not reset elsewhere.
read = {
	arg matrix, wordLen = 1, wordShift = [0, 1], letterShift = [0, 1], wrap = true;
	var width = matrix.shape[1], height = matrix.shape[0], startReadPos = readPos, word = [];
	wordLen.do({arg i;
		word = word.add(matrix[readPos[0] % height][readPos[1] % width]);
		readPos = readPos + letterShift});
	readPos = startReadPos + wordShift; word.convertDigits(2)};

// read the matrix - this data is ultimately used to generate the piece
~genData = {arg matrix, transform, partSize, shift = 0, pad = 16, minScale = 0, maxScale = 1;
	var width = matrix.shape[1], height = matrix.shape[0], res;
	readPos = [0, 0];
	res = partSize.collect({arg p; height.collect({arg i;
		readPos[0] = i + p + shift;
		width.collect({arg j; var val = read.value(matrix);
			minScale + ((maxScale - minScale) * val)});
}).flatten.addAll(Array.fill(pad, {0}))})};
)