/// 
// Fixture taken from https://github.com/RyanCavanaugh/koany/blob/master/koany.tsx
interface Garden {
	colors: Gardens.RockColor[];
	shapes: Gardens.RockShape[];
}
namespace Gardens {
	export enum RockShape {
		Empty,
		Circle,
		Triangle,
		Square,
		Max
	}
	export const RockShapes = [RockShape.Circle, RockShape.Triangle, RockShape.Square];
	export const RockShapesAndEmpty = RockShapes.concat(RockShape.Empty);
	export enum RockColor {
		Empty,
		White,
		Red,
		Black,
		Max
	}
	export const RockColors = [RockColor.White, RockColor.Red, RockColor.Black];
	export const RockColorsAndEmpty = RockColors.concat(RockColor.Empty);
	export const Size = 9;
	// 012
	// 345
	// 678
	export const adjacencies = [
		[1, 3], [0, 4, 2], [1, 5],
		[0, 4, 6], [3, 1, 7, 5], [2, 4, 8],
		[3, 7], [6, 4, 8], [7, 5]
	];
}
module Koan {
	export enum DescribeContext {
		// every "white stone" is ...
		Singular,
		// all "white stones" are
		Plural,
		// every stone in the top row is "white"
		Adjectival
	}
	export enum PartType {
		Selector,
		Aspect
	}
	export enum StateTestResult {
		Fail = 0,
		WeakPass = 1,
		Pass = 2
	}
	/// A general format for producing a Statement
	export interface StatementTemplate {
		holes: PartType[];
		describe(args: T): string;
		test(g: Garden, args: T): StateTestResult;
	}
	/// A completed rule that can be used to test a Garden
	export interface ProducedStatement {
		test(g: Garden): StateTestResult;
		description: string;
		children: T;
		hasPassedAndFailed(): boolean;
	}
	function rnd(max: number) {
		return Math.floor(Math.random() * max);
	}
	function randomColor(): Gardens.RockColor {
		return Math.floor(Math.random() * (Gardens.RockColor.Max - 1)) + 1
	}
	function randomShape(): Gardens.RockShape {
		return Math.floor(Math.random() * (Gardens.RockShape.Max - 1)) + 1
	}
	/* New Impl Here */
	interface SelectorSpec {
		childTypes?: PartType[];
		precedence: number;
		weight: number;
		test(args: T, g: Garden, index: number): string|number|boolean;
		describe(args: T, context: DescribeContext): string;
		isAllValues(values: Array|Array): boolean;
	}
	interface ProducedSelector {
		test(g: Garden, index: number): string|number|boolean;
		getDescription(plural: DescribeContext): string;
		seenAllValues(): boolean;
	}
	export function buildSelector(spec: SelectorSpec, args: T): ProducedSelector {
		let seenResults: { [s: string]: boolean;} = {};
		return {
			test: (g: Garden, index: number) => {
				var result = spec.test(args, g, index);
				seenResults[result + ''] = true;
				return result;
			},
			getDescription: (context) => {
				return spec.describe(args, context);
			},
			seenAllValues: () => {
				return spec.isAllValues(Object.keys(seenResults));
			}
		}
	}
	export var SelectorTemplates: Array> = [];
	module LetsMakeSomeSelectors {
		// Is rock
		SelectorTemplates.push({
			test: (args, g, i) => g.colors[i] !== Gardens.RockColor.Empty,
			describe: (args, context) => {
				switch(context) {
					case DescribeContext.Plural:
						return 'Stones';
					case DescribeContext.Adjectival:
						return 'not empty';
					case DescribeContext.Singular:
						return 'Stone';
				}
			},
			isAllValues: items => items.length === 2,
			precedence: 0,
			weight: 1
		});
		// Is of a certain color and/or shape
		Gardens.RockColorsAndEmpty.forEach(color => {
			let colorName = Gardens.RockColor[color];
			let colorWeight = color === Gardens.RockColor.Empty ? 1 : 0.33;
			Gardens.RockShapesAndEmpty.forEach(shape => {
				let shapeName = Gardens.RockShape[shape];
				let shapeWeight = shape === Gardens.RockShape.Empty ? 1 : 0.33;
				SelectorTemplates.push({
					test: (args, g, i) => {
						if(color === Gardens.RockColor.Empty) {
							if (shape === Gardens.RockShape.Empty) {
								return g.colors[i] === Gardens.RockColor.Empty;
							} else {
								return g.shapes[i] === shape;
							}
						} else {
							if (shape === Gardens.RockShape.Empty) {
								return g.colors[i] === color;
							} else {
								return g.shapes[i] === shape && g.colors[i] === color;
							}
						}
					},
					describe: (args, context) => {
						if(color === Gardens.RockColor.Empty) {
							if (shape === Gardens.RockShape.Empty) {
								switch(context) {
									case DescribeContext.Plural:
										return 'Empty Cells';
									case DescribeContext.Adjectival:
										return 'Empty';
									case DescribeContext.Singular:
										return 'Empty Cell';
								}
							} else {
								switch(context) {
									case DescribeContext.Plural:
										return shapeName + 's';
									case DescribeContext.Adjectival:
										return 'a ' + shapeName;
									case DescribeContext.Singular:
										return shapeName;
								}
							}
						} else {
							if (shape === Gardens.RockShape.Empty) {
								switch(context) {
									case DescribeContext.Plural:
										return colorName + ' Stones';
									case DescribeContext.Adjectival:
										return colorName;
									case DescribeContext.Singular:
										return colorName + ' Stone';
								}
							} else {
								switch(context) {
									case DescribeContext.Plural:
										return colorName + ' ' + shapeName + 's';
									case DescribeContext.Adjectival:
										return 'a ' + colorName + ' ' + shapeName;
									case DescribeContext.Singular:
										return colorName + ' ' + shapeName;
								}
							}
						}
					},
					isAllValues: items => items.length === 2,
					precedence: 3,
					weight: (shapeWeight + colorWeight === 2) ? 0.3 : shapeWeight * colorWeight
				});
			});
		});
		// [?] in the [top|middle|bottom] [row|column]
		[0, 1, 2].forEach(rowCol => {
			[true, false].forEach(isRow => {
				var name = (isRow ? ['top', 'middle', 'bottom'] : ['left', 'middle', 'right'])[rowCol] + ' ' + (isRow ? 'row' : 'column');
				var spec: SelectorSpec<[ProducedSelector]> = {
					childTypes: [PartType.Selector],
					test: (args, g, i) => {
						var c = isRow ? Math.floor(i / 3) : i % 3;
						if (c === rowCol) {
							return args[0].test(g, i);
						} else {
							return false;
						}
					},
					describe: (args, plural) => args[0].getDescription(plural) + ' in the ' + name,
					isAllValues: items => items.length === 2,
					precedence: 4,
					weight: 1 / 6
				};
				SelectorTemplates.push(spec);
			});
		});
		// [?] next to a [?]
		SelectorTemplates.push({
			childTypes: [PartType.Selector, PartType.Selector],
			test: (args, g, i) => {
				if (args[0].test(g, i)) {
					return Gardens.adjacencies[i].some(x => !!args[1].test(g, x));
				} else {
					return false;
				}
			},
			describe: (args, plural) => {
				return args[0].getDescription(plural) + ' next to a ' + args[1].getDescription(DescribeContext.Singular);
			},
			isAllValues: items => items.length === 2,
			precedence: 4,
			weight: 1
		} as SelectorSpec<[ProducedSelector, ProducedSelector]>);
	}
	export function buildStatement(s: StatementTemplate, args: T): ProducedStatement {
		let hasPassed = false;
		let hasFailed = false;
		let result: ProducedStatement = {
			children: args,
			description: s.describe(args),
			test: (g) => {
				let r = s.test(g, args);
				if (r === StateTestResult.Pass) {
					hasPassed = true;
				} else if(r === StateTestResult.Fail) {
					hasFailed = true;
				}
				return r;
			},
			hasPassedAndFailed: () => {
				return hasPassed && hasFailed && (args as any as ProducedSelector[]).every(c => c.seenAllValues());
			}
		};
		return result;
	}
	export let StatementList: StatementTemplate[] = [];
	module LetsMakeSomeStatements {
		// Every [?] is a [?]
		StatementList.push({
			holes: [PartType.Selector, PartType.Selector],
			test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
				let didAnyTests = false;
				for (var i = 0; i < Gardens.Size; i++) {
					if (args[0].test(g, i)) {
						if(!args[1].test(g, i)) return StateTestResult.Fail;
						didAnyTests = true;
					}
				}
				return didAnyTests ? StateTestResult.Pass : StateTestResult.WeakPass;
			},
			describe: args => {
				return 'Every ' + args[0].getDescription(DescribeContext.Singular) + ' is ' + args[1].getDescription(DescribeContext.Adjectival);
			}
		});
		// There is exactly 1 [?]
		StatementList.push({
			holes: [PartType.Selector],
			test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
				var count = 0;
				for (var i = 0; i < Gardens.Size; i++) {
					if (args[0].test(g, i)) count++;
				}
				return count === 1 ? StateTestResult.Pass : StateTestResult.Fail;
			},
			describe: args => {
				return 'There is exactly one ' + args[0].description;
			}
		});
		// There are more [?] than [?]
		StatementList.push({
			holes: [PartType.Selector, PartType.Selector],
			test: (g: Garden, args: [ProducedSelector, ProducedSelector]) => {
				var p1c = 0, p2c = 0;
				for (var i = 0; i < Gardens.Size; i++) {
					if (args[0].test(g, i)) p1c++;
					if (args[1].test(g, i)) p2c++;
				}
				if(p1c > p2c && p2c > 0) {
					return StateTestResult.Pass;
				} else if(p1c > p2c) {
					return StateTestResult.WeakPass;
				} else {
					return StateTestResult.Fail;
				}
			},
			describe: args => {
				return 'There are more ' + args[0].descriptionPlural + ' than ' + args[1].descriptionPlural;
			}
		});
	}
	function randomElementOf(arr: T[]): T {
		if (arr.length === 0) {
			return undefined;
		} else {
			return arr[Math.floor(Math.random() * arr.length)];
		}
	}
	function randomWeightedElementOf(arr: T[]): T {
		var totalWeight = arr.reduce((acc, v) => acc + v.weight, 0);
		var rnd = Math.random() * totalWeight;
		for (var i = 0; i < arr.length; i++) {
			rnd -= arr[i].weight;
			if (rnd <= 0) return arr[i];
		}
		// Got destroyed by floating error, just try again
		return randomWeightedElementOf(arr);
	}
	export function buildRandomNewSelector(maxPrecedence = 1000000): ProducedSelector {
		var choices = SelectorTemplates;
		let initial = randomWeightedElementOf(choices.filter(p => p.precedence <= maxPrecedence));
		// Fill in the holes
		if (initial.childTypes) {
			var fills = initial.childTypes.map(h => {
				if (h === PartType.Selector) {
					return buildRandomNewSelector(initial.precedence - 1);
				} else {
					throw new Error('Only know how to fill Selector holes')
				}
			});
			return buildSelector(initial, fills);
		} else {
			return buildSelector(initial, []);
		}
	}
	export function makeEmptyGarden(): Garden {
		var g = {} as Garden;
		g.colors = [];
		g.shapes = [];
		for (var i = 0; i < Gardens.Size; i++) {
			g.colors.push(Gardens.RockColor.Empty);
			g.shapes.push(Gardens.RockShape.Empty);
		}
		return g;
	}
	export function gardenToString(g: Garden): string {
		return g.colors.join('') + g.shapes.join('');
	}
	export function makeRandomGarden(): Garden {
		var g = makeEmptyGarden();
		blitRandomGardenPair(g, g);
		return g;
	}
	export function cloneGarden(g: Garden): Garden {
		var result: Garden = {
			colors: g.colors.slice(0),
			shapes: g.shapes.slice(0)
		};
		return result;
	}
	export function clearGarden(g: Garden) {
		for (var i = 0; i < Gardens.Size; i++) {
			g.colors[i] = Gardens.RockColor.Empty;
			g.shapes[i] = Gardens.RockShape.Empty;
		}
	}
	export function blitRandomGardenPair(g1: Garden, g2: Garden): void {
		let placeCount = 0;
		for (var i = 0; i < Gardens.Size; i++) {
			if (rnd(7) === 0) {
				g1.colors[i] = g2.colors[i] = randomColor();
				g1.shapes[i] = g2.shapes[i] = randomShape();
			} else {
				placeCount++;
				g1.colors[i] = g2.colors[i] = Gardens.RockColor.Empty;
				g1.shapes[i] = g2.shapes[i] = Gardens.RockShape.Empty;
			}
		}
		if (placeCount === 0) blitRandomGardenPair(g1, g2);
	}
	export function blitNumberedGarden(g: Garden, stoneCount: number, n: number): void {
		clearGarden(g);
		let cellNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8];
		for (let i = 0; i < stoneCount; i++) {
			let cellNum = getValue(cellNumbers.length);
			let cell = cellNumbers.splice(cellNum, 1)[0];
			g.colors[cell] = getValue(3) + 1;
			g.shapes[cell] = getValue(3) + 1;
		}
		function getValue(max: number) {
			let result = n % max;
			n = (n - result) / max;
			return result;
		}
	}
	export function mutateGarden(g: Garden): void {
		while (true) {
			var op = rnd(5);
			let x = rnd(Gardens.Size);
			let y = rnd(Gardens.Size);
			switch (op) {
				case 0: // Swap two non-identical cells
					if (g.colors[x] !== g.colors[y] || g.shapes[x] !== g.shapes[y]) {
						var tmp: any = g.colors[x];
						g.colors[x] = g.colors[y];
						g.colors[y] = tmp;
						tmp = g.shapes[x];
						g.shapes[x] = g.shapes[y];
						g.shapes[y] = tmp;
						return;
					}
					break;
				case 1: // Add a stone
					if (g.colors[x] === Gardens.RockColor.Empty) {
						g.colors[x] = randomColor();
						g.shapes[x] = randomShape();
						return;
					}
					break;
				case 2: // Remove a stone
					if (g.colors.filter(x => x !== Gardens.RockColor.Empty).length === 1) continue;
					if (g.colors[x] !== Gardens.RockColor.Empty) {
						g.colors[x] = Gardens.RockColor.Empty;
						g.shapes[x] = Gardens.RockShape.Empty;
						return;
					}
					break;
				case 3: // Change a color
					let c = randomColor();
					if (g.colors[x] !== Gardens.RockColor.Empty && g.colors[x] !== c) {
						g.colors[x] = c;
						return;
					}
					break;
				case 4: // Change a shape
					let s = randomShape();
					if (g.shapes[x] !== Gardens.RockShape.Empty && g.shapes[x] !== s) {
						g.shapes[x] = s;
						return;
					}
					break;
			}
		}
	}
}
class Indexion {
	sizes: number[];
	constructor(...sizes: number[]) {
		this.sizes = sizes;
	}
	public getValues(index: number): number[] {
		let result = new Array(this.sizes.length);
		this.fillValues(index, result);
		return result;
	}
	public fillValues(index: number, result: number[]): void {
		for (var i = 0; i < this.sizes.length; i++) {
			result[i] = index % this.sizes[i];
			index -= result[i];
			index /= this.sizes[i];
		}
	}
	public valuesToIndex(values: number[]): number {
		var result = 0;
		var factor = 1;
		for (var i = 0; i < this.sizes.length; i++) {
			result += values[i] * this.sizes[i] * factor;
			factor *= this.sizes[i];
		}
		return result;
	}
	public getAdjacentIndices(index: number): number[][] {
		var baseline = this.getValues(index);
		var results: number[][] = [];
		for (var i = 0; i < this.sizes.length; i++) {
			if(baseline[i] > 0) {
				baseline[i]--;
				results.push(baseline.slice());
				baseline[i]++;
			}
			if(baseline[i] < this.sizes[i] - 1) {
				baseline[i]++;
				results.push(baseline.slice());
				baseline[i]--;
			}
		}
		return results;
	}
	public distance(index1: number, index2: number): number {
		let delta = 0;
		for (var i = 0; i < this.sizes.length; i++) {
			var a = index1 % this.sizes[i];
			var b = index2 % this.sizes[i];
			delta += Math.abs(b - a);
			index1 -= a;
			index2 -= b;
			index1 /= this.sizes[i];
			index2 /= this.sizes[i];
		}
		return delta;
	}
}
function makeNewExample() {
	while (true) {
		var p1 = Koan.buildSelector(Koan.SelectorTemplates[12], []);
		var p2 = Koan.buildSelector(Koan.SelectorTemplates[14], []);
		var test = Koan.buildStatement(Koan.StatementList[0], [p1, p2]);
		var examples: Garden[] = [];
		console.log('Attempt to generate examples for "' + test.description + '"');
		var maxGarden = /*(9 * 9) + (9 * 9 * 9 * 8) + */(9 * 9 * 9 * 8 * 9 * 7);
		let g = Koan.makeEmptyGarden();
		let passCount = 0, failCount = 0;
		let resultLookup: boolean[] = [];
		let lastResult: boolean = undefined;
		for (var i = 0; i < maxGarden; i++) {
			Koan.blitNumberedGarden(g, 3, i);
			let result = test.test(g);
			if(result === Koan.StateTestResult.Pass) {
				resultLookup[i] = true;
				passCount++;
				if (lastResult !== true && examples.length < 10) examples.push(Koan.cloneGarden(g));
				lastResult = true;
			} else if (result === Koan.StateTestResult.Fail) {
				resultLookup[i] = false;
				failCount++;
				if (lastResult !== false && examples.length < 10) examples.push(Koan.cloneGarden(g));
				lastResult = false;
			}
			if (examples.length === 10) break;
		}
		console.log('Rule passes ' + passCount + ' and fails ' + failCount);
		/*
		if (!test.hasPassedAndFailed()) {
			console.log('Rule has unreachable, contradictory, or tautological clauses');
			continue;
		}
		if (passCount === 0 || failCount === 0) {
			console.log('Rule is always true or always false');
			continue;
		}
		*/
		var h = document.createElement('h2');
		h.innerText = test.description;
		document.body.appendChild(h);
		return { test: test, examples: examples };
	}
}
let list: Garden[] = [];
let test: Koan.ProducedStatement;
window.onload = function() {
	let rule = makeNewExample();
	let garden = Koan.makeRandomGarden();
	list = rule.examples;
	test = rule.test;
	function renderList() {
		function makeGarden(g: Garden, i: number) {
			return  {
					console.log(list.indexOf(g));
					list.splice(list.indexOf(g), 1);
					renderList();
				}}
				onRightButtonClicked={() => {
					garden = g;
					renderEditor();
				}}
			/>;
		}
		let gardenList = {list.map(makeGarden)}
;
		React.render(gardenList, document.getElementById('results'));
	}
	let i = 0;
	function renderEditor() {
		i++;
		let editor =  {
			list.push(garden);
			renderList();
		}} />;
		React.render(editor, document.getElementById('editor'));
	}
	renderList();
	renderEditor();
}
function classNames(nameMap: any): string {
	return Object.keys(nameMap).filter(k => nameMap[k]).join(' ');
}
interface GardenCellProps extends React.Props<{}> {
	color: Gardens.RockColor;
	shape: Gardens.RockShape;
	index: number;
	movable?: boolean;
	onEdit?(newColor: Gardens.RockColor, newShape: Gardens.RockShape): void;
}
interface GardenCellState {
	isDragging?: boolean;
}
class GardenCell extends React.Component {
	state: GardenCellState = {};
	ignoreNextEdit = false;
	render() {
		var classes = ['cell', 'index_' + this.props.index];
		if (this.state.isDragging) {
			// Render as blank
		} else {
			classes.push(Gardens.RockColor[this.props.color], Gardens.RockShape[this.props.shape]);
		}
		if (this.props.movable) classes.push('movable');
		let events: React.HTMLAttributes = {
			onDragStart: (e) => {
				this.ignoreNextEdit = false;
				e.dataTransfer.dropEffect = 'copyMove';
				e.dataTransfer.effectAllowed = 'move';
				e.dataTransfer.setData('shape', this.props.shape.toString());
				e.dataTransfer.setData('color', this.props.color.toString());
				let drag = document.getElementById(getGardenName(this.props.color, this.props.shape));
				let xfer: any = (e.nativeEvent as DragEvent).dataTransfer;
				xfer.setDragImage(drag, drag.clientWidth * 0.5, drag.clientHeight * 0.5);
				this.setState({ isDragging: true });
			},
			onDragEnter: (e) => {
				e.dataTransfer.dropEffect = 'move';
				e.preventDefault();
			},
			onDragOver: (e) => {
				e.dataTransfer.dropEffect = 'move';
				e.preventDefault();
			},
			onDragEnd: (e) => {
				this.setState({ isDragging: false });
				if (!this.ignoreNextEdit) {
					this.props.onEdit && this.props.onEdit(undefined, undefined);
				}
			},
			draggable: true
		}
		let handleDrop = (event: React.DragEvent) => {
			if(this.props.onEdit) {
				if (this.state.isDragging) {
					// Dragged to self, don't do anything
					this.ignoreNextEdit = true;
				} else {
					let shape: Gardens.RockShape = +event.dataTransfer.getData('shape');
					let color: Gardens.RockColor = +event.dataTransfer.getData('color');
					this.props.onEdit(color, shape);
				}
			}
		}
		return ;
	}
}
interface GardenDisplayProps extends React.Props {
	garden?: Garden;
	test?: Koan.ProducedStatement;
	leftButton?: string;
	rightButton?: string;
	onLeftButtonClicked?(): void;
	onRightButtonClicked?(): void;
	editable?: boolean;
	onChanged?(newGarden: Garden): void;
}
interface GardenDisplayState {
	garden?: Garden;
}
class GardenDisplay extends React.Component {
	state = {
		garden: Koan.cloneGarden(this.props.garden)
	};
	leftClicked = () => {
		this.props.onLeftButtonClicked && this.props.onLeftButtonClicked();
	};
	rightClicked = () => {
		this.props.onRightButtonClicked && this.props.onRightButtonClicked();
	};
	render() {
		let g = this.state.garden;
		let pass = (this.props.test && this.props.test.test(this.state.garden));
		let classes = {
			garden: true,
			unknown: pass === undefined,
			pass: pass === Koan.StateTestResult.Pass || pass === Koan.StateTestResult.WeakPass,
			fail: pass === Koan.StateTestResult.Fail,
			editable: this.props.editable
		};
		var children = g.colors.map((_, i) => (
			 {
					if(this.props.editable) {
						let newGarden = Koan.cloneGarden(this.state.garden);
						newGarden.colors[i] = newColor;
						newGarden.shapes[i] = newShape;
						this.setState({ garden: newGarden });
						this.props.onChanged && this.props.onChanged(newGarden);
					}
				}}
			/>));
		return 
			{children}
			
				{this.props.leftButton && {this.props.leftButton}
}
				{pass ? '✓' : '🚫'}
				{this.props.rightButton && {this.props.rightButton}
}
			
		 ;
	}
}
interface GardenEditorProps extends React.Props {
	onSaveClicked?(garden: Garden): void;
	test?: Koan.ProducedStatement;
	garden?: Garden;
}
interface GardenEditorState {
	garden?: Garden;
	pass?: boolean;
}
class GardenEditor extends React.Component {
	state = { garden: this.props.garden };
	save = () => {
		this.props.onSaveClicked && this.props.onSaveClicked(this.state.garden);
	};
	render() {
		return 
			 this.setState({ garden: g }) } />
			
			{'💾'}
		 ;
	}
}
class StonePalette extends React.Component<{}, {}> {
	render() {
		let items: JSX.Element[] = [];
		Gardens.RockColors.forEach(color => {
			Gardens.RockShapes.forEach(shape => {
				let name = getGardenName(color, shape);
				let extraProps = { id: name, key: name };
				let index = items.length;
				items.push()
			});
		});
		return {items}
;
	}
}
function getGardenName(color: Gardens.RockColor, shape: Gardens.RockShape) {
	return 'draggable.' + Gardens.RockShape[shape] + '.' + Gardens.RockColor[color];
}