(
~genSecret = {arg tiling, offset, bitDepth = 5, codePad = 1;
	var sideLen, secret, subTiling, xOffset = 10, yOffset = 10, w = 100, h = 100, colorMap;

	xOffset = offset[0];
	yOffset = offset[1];
	sideLen = bitDepth + (codePad * 2);

	if(tiling.isString, {
		var img;
		tiling.postln;
		img = Image.new(w * sideLen + 1, h * sideLen + 1);
		img.draw({ arg image;
			Pen.stringRightJustIn(tiling, Rect(0, img.bounds.height - 100, img.bounds.width, 100), Font("Courier", 35, true), color: Color.black)
		});
		secret = img.height.collect({arg y;
			img.width.collect({arg x;
				if(img.getColor(x, y) == Color(), {0}, {1});
			})
		});
		secret
	}, {
		var colorDepth;
		subTiling = tiling.slice((yOffset..(yOffset + h - 1)), (xOffset..(xOffset + w - 1)));

		colorMap = (0..31).sort({ arg a, b; (a.asDigits(2, bitDepth)).sum < (b.asDigits(2, bitDepth).sum) }); //.scramble;
		colorDepth = subTiling.flat.maxItem.postln;

		secret = Array.fill2D(h * sideLen + 1, w * sideLen + 1, {0});
		subTiling.do({arg row, r;
			row.do({arg tile, c;
				tile.do({arg color, d;
					var shape;
					shape = codePad.collect({0}) ++ colorMap[ (31 / colorDepth) * color].asDigits(2, bitDepth) ++ (codePad + 1).collect({0});
					//shape = codePad.collect({0}) ++ (color % 8).asDigits(2, bitDepth) ++ (codePad + 1).collect({0});
					shape.do({arg bit, b;
						(bit * 2 + 1).do({arg e;
						//(bit * (color / 8).trunc + 1).do({arg e;
							if(e == 0 && bit == 0, {0}, {
								switch(d,
									0, {secret[r * sideLen + b][c * sideLen + e] = 1},
									1, {secret[r * sideLen + e][c * sideLen + b] = 1},
									2, {secret[r * sideLen + b][(c + 1) * sideLen - e] = 1},
									3, {secret[(r + 1) * sideLen - e][c * sideLen + b] = 1}
								);
							})
						});
					});
				});
			});
		});
		(secret + 1) % 2
		//secret
	});
};

~genSecrets = {arg tilings, offsets;
	var secrets;
	secrets = tilings.collect({arg row, r;
		row.collect({arg tiling, c; ~genSecret.value(tiling, offsets[r][c])})});
	secrets
};

~genOneTimePad = {arg rows, cols;
	var otp;
	otp = Array.fill2D(rows, cols, {2.rand});
	otp
};

~genShadowArray0 = {arg secrets, otp,  shiftMult = 5;
	// I think best ratio is 5 / 3 with 250 * 150 tiles
	var shadowstmp, shadow0tmp, shadow1tmp, shadow0, shadow0NoisePad, shadow0ZeroPad, xDiff, yDiff, shift, offsets, refPoints;

	shadowstmp = secrets.collect({arg row, r;
		row.collect({arg secret, c; (secret + otp) % 2})});

	// induce shift on each shadow
	shadow0tmp = shadowstmp.collect({arg row, r;
		row.collect({arg shadow, c;
			var padTop, padBottom, padLeft, padRight, res;
			padTop = (r * shiftMult).collect({shadow[0].size.collect({2.rand})});
			padBottom = ((2 - r) * shiftMult).collect({shadow[0].size.collect({2.rand})});
			res = padTop ++ shadow ++ padBottom;
			padLeft = (c * shiftMult).collect({res.size.collect({2.rand})});
			padRight = ((2 - c) * shiftMult).collect({res.size.collect({2.rand})});
			(padLeft ++ res.flop ++ padRight).flop;
		});
	});

	shadow0tmp = shadow0tmp.collect({arg row, r;
		row.collect({arg shadow; shadow.flop}).lace.flop
	}).lace;

	// pad the entire array with some random data
	shadow0NoisePad = 0;
	shadow0tmp = shadow0NoisePad.collect({shadow0tmp[0].size.collect({2.rand})}) ++ shadow0tmp ++ shadow0NoisePad.collect({shadow0tmp[0].size.collect({2.rand})});
	shadow0tmp = (shadow0NoisePad.collect({shadow0tmp.size.collect({2.rand})}) ++ shadow0tmp.flop ++ shadow0NoisePad.collect({shadow0tmp.size.collect({2.rand})})).flop;

	shadow0 = Array.fill2D((shadow0tmp.size * 2), (shadow0tmp[0].size * 2), {2.rand});
	(shadow0tmp.size).do({arg r;
		(shadow0tmp[0].size).do({arg c;
			var val = shadow0tmp[r][c];
			shadow0[r * 2][c * 2] = val;
			shadow0[r * 2][c * 2 + 1] = (val - 1).abs;
			shadow0[r * 2 + 1][c * 2] = (val - 1).abs;
			shadow0[r * 2 + 1][c * 2 + 1] = val;
	})});

	// pad the entire array with some 0s

	/*
	shadow0ZeroPad = ((shiftMult * 3 * 2 + 2) * 2) + 350;
	shadow0 = shadow0ZeroPad.collect({shadow0[0].size.collect({0})}) ++ shadow0 ++ shadow0ZeroPad.collect({shadow0[0].size.collect({0})});
	shadow0 = (shadow0ZeroPad.collect({shadow0.size.collect({0})}) ++ shadow0.flop ++ shadow0ZeroPad.collect({shadow0.size.collect({0})})).flop;
	*/

	//add alignment marks
	shift = shiftMult * 3 * 2 + 2;
	refPoints = [
		[150 + shift, shadow0[0].size * (1 / 4)],
		[150 + shift, shadow0[0].size * (3 / 4)],
		[shadow0.size - 200 - shift, shadow0[0].size * (1 / 4)],
		[shadow0.size - 200 - shift, shadow0[0].size * (3 / 4)]
	];
	offsets = [[0, 0], [0, shift.neg], [shift.neg, 0], [0, shift], [shift, 0],
		[shift, shift], [shift.neg, shift.neg], [shift, shift.neg], [shift.neg, shift]];
	refPoints.do({arg refPoint;
		offsets.do({arg offset;
			1.do({arg r; var p = refPoint + offset + 1; 1.do({arg c;
				[r + p[0], c + p[1]].postln;
				shadow0[r + p[0]][c + p[1]] =
				if(((c % 4) == 0) || ((r % 4) == 0), {0}, {1})})});
		});
	});

	//expand
	//shadow0 = [shadow0, shadow0, shadow0, shadow0].lace;
	//shadow0 = [shadow0.flop, shadow0.flop, shadow0.flop, shadow0.flop].lace.flop;

	shadow0;
};

~genShadowArray1 = {arg otp, rows, cols, interleaveExpand = 3, shiftMult = 5;
	var shadow1tmp, shadow1, yDiff, xDiff, shift, refPoints;
	shadow1tmp = otp;
	shadow1 = Array.fill2D((interleaveExpand * shadow1tmp.size * 2), (interleaveExpand * shadow1tmp[0].size * 2), {1});
	(shadow1tmp.size).do({arg r;
		(shadow1tmp[0].size).do({arg c;
			var val = shadow1tmp[r][c];
			shadow1[2 + (r * 2 * interleaveExpand)][2 + (c * 2 * interleaveExpand)] = val;
			shadow1[2 + (r * 2 * interleaveExpand)][2 + (c * 2 * interleaveExpand + 1)] = (val - 1).abs;
			shadow1[2 + (r * 2 * interleaveExpand + 1)][2 + (c * 2 * interleaveExpand)] = (val - 1).abs;
			shadow1[2 + (r * 2 * interleaveExpand + 1)][2 + (c * 2 * interleaveExpand + 1)] = val;
	})});

	yDiff = ((rows - shadow1.size) / 2).asInteger;
	shadow1 = yDiff.collect({shadow1[0].size.collect({0})}) ++ shadow1 ++ yDiff.asInteger.collect({shadow1[0].size.collect({0})});
	xDiff = ((cols - shadow1[0].size) / 2).asInteger;
	shadow1 = (xDiff.asInteger.collect({shadow1.size.collect({0})}) ++ shadow1.flop ++ xDiff.asInteger.collect({shadow1.size.collect({0})})).flop;

	//add alignment marks
	shift = shiftMult * 3 * 2 + 2;
	refPoints = [
		[150 + shift, shadow1[0].size * (1 / 4)],
		[150 + shift, shadow1[0].size * (3 / 4)],
		[shadow1.size - 200 - shift, shadow1[0].size * (1 / 4)],
		[shadow1.size - 200 - shift, shadow1[0].size * (3 / 4)]
	];
	refPoints.do({arg refPoint;
		1.do({arg r; var p = refPoint; 1.do({arg c;
			[r + p[0] + 1, c + p[1] + 1].postln;
			shadow1[r + p[0] + 1][c + p[1] + 1] =
			//if((c == 0) || (c == 5) || ((r % 2) == 0), {1}, {0})})});
			if(((c % 4) == 0) || ((r % 4) == 0), {1}, {0})})});
	});

	/* expand
	shadow1 = shadow1.collect({arg row;
		row.collect({arg val;
			[
				[1, 1, 1, 1],
				[1, val, val, 1],
				[1, val, val, 1],
				[1, 1, 1, 1]
			]
	})}).collect({arg a; a.lace.clump(a.size).collect({arg b; b.flatten})}).flop.lace;
	//shadow1 = [shadow1, shadow1, shadow1, shadow1].lace;
	*/

	shadow1;
};

~genShadowImage = {arg shadowArray;
	var shadowImg;
	shadowImg = Image.new(shadowArray[0].size, shadowArray.size);
	shadowArray.do({arg row, r;
		row.do({arg val, c; shadowImg.setColor(if(val == 0, {Color.white}, {Color.black}), c, r)})});
	shadowImg
};


~visualize = {arg shadowImgs, shift = [0, 0];
	var diff, padArray, shadowImg0, shadowImg1, secretImg,
	zoom = 1, shadowImg0Win, shadowImg1Win, secretImgWin, win, view, mode = [1, 1], sliderWin, slider;

	shadowImg0 = shadowImgs[0];
	shadowImg1 = shadowImgs[1];

	//shadowImg0.plot;
	//shadowImg1.plot;

	win = Window.new("overlay", Rect(128, 64, 1000, 800), scroll: true);
	view = UserView(win, Rect(0, 0, shadowImg0.width, shadowImg0.height));
	win.front;
    view.background_(Color.white);
	//secretImgWin = win = secretImg.plot;
	view.drawFunc_({
		Pen.scale(zoom, zoom);
		Pen.drawImage( Point(shift[0], shift[1]), shadowImg0, operation: 'plusDarker', opacity: 1);
		Pen.drawImage( Point(0, 0), shadowImg1, operation: 'plusDarker', opacity: 1);
	});


	sliderWin = Window("Slider2D", Rect(100, 100, 250, 250));
	slider = Slider2D(sliderWin, Rect(20, 20, 200, 200)).shift_scale_(0.5).action_({arg sl;
		shift = [sl.x * 200 - 100, (1 - sl.y).abs * 200 - 100].trunc; win.refresh});
	sliderWin.front;


	win.view.keyDownAction = { |doc, char, mod, unicode, keycode, key|
		[doc, char, mod, unicode, keycode, key].postln;
		case
		// <ctrl + f> = enter full screen
		{mod == 262144 && key == 70} {win.fullScreen}
		// <esc> = exit full screen
		{mod == 0 && key == 16777216} {win.endFullScreen}
		// <ctrl + +> = zoom in
		{mod == 262144 && key == 61} {zoom = zoom * 5/4; view.refresh}
		// <ctrl + -> = zoom put
		{mod == 262144 && key == 45} {zoom = zoom * 4/5; view.refresh}
		// <ctrl + 1> = shadow1 on/off
		{mod == 262144 && key == 49} {mode[0] = ((mode[0] + 1) % 2); view.refresh}
		// <ctrl + 2> = shadow2 on/off
		{mod == 262144 && key == 50} {mode[1] = ((mode[1] + 1) % 2); view.refresh}
		// <ctrl + a> = animate
		{mod == 262144 && key == 65} {
			shift = [0, 8];
			{while { shift[0] < 28 } {win.refresh; 0.25.wait; shift[0] = shift[0] + 1}}.fork(AppClock)
		}
		// <ctrl + d> = save
		{mod == 262144 && key == 68} {
			Dialog.savePanel({arg path;
				var img;
				shadowImg1.write(path , quality: 100);
				"saved".postln;
			},{
				"cancelled".postln;
			});
		}
	};
};

)
