<html>

<head>
</head>

<body>

	<div id="app" class="app"></div>
	<pre>
			TODO:
			 - if orange fields, no other selections should be possible than deselect.
		</pre>
	<template id="template_sudoku_container">
		<div style="width:640px;height:640px;font-size:1.2em;" class="container-content"></div>
	</template>
	<template id="template_sudoku_parser">
		<div style="width:100%;height:100%" class="parser-content"></div>
	</template>
	<template id="template_sudoku_cell">
		<div class="cell-content"></div>
	</template>
	<style type="text/css">
		.app {
			width: 400px;
			height: 400px;
		}

		.cell-content {

			font-family: 'Courier New', Courier, monospace;
			user-select: none;
			-webkit-user-select: none;
			-moz-user-select: none;
			-ms-user-select: none;
			border: 1px solid #CCCCCC;
			text-align: center;
			height: 10%;
			width: 10%;
			float: left;
			display: flex;
			justify-content: center;
			align-items: center;
			font-size: calc(5px + 1vw);
		}

		.sudoku-cell-selected {
			background-color: lightgreen;
		}

		.sudoku-cell-invalid {
			color: red;
			font-weight: bold;
		}
	</style>
	<script type="text/javascript">

		const game_size = 9;
		const container_template = document.getElementById('template_sudoku_container');
		const cell_template = document.getElementById("template_sudoku_cell");
		const sudokuParserTemplate = document.getElementById('template_sudoku_parser');
		const container = document.getElementById('app'); //.importNode(container_template.content, true);
		const _hashChars = "abcdefghijklmnopqrstuvwxyz\"':[]0123456789";
		function hashString(str) {
			let result = 0;
			for (let i = 0; i < str.length; i++) {
				result += (_hashChars.indexOf(str[i]) + 1) * (i * 40);
			}
			return result;
		}
		class State {
			number = 0
			cells = []
			selection = []
			selectionCount = 0
			_string = null
			_hash = null
			_json = null;
			constructor(arg) {
				if (isNaN(arg)) {
					this.number = arg.number;
					this.cells = arg.cells;
					this.selectCount = arg.selectionCount;
					this._hash = arg._hash;
					this._json = arg;
				} else {
					this.number = arg;
				}
			}
			toJson() {
				if (this._json == null) {
					const json = {
						number: this.number,
						cells: this.cells,
						selectionCount: this.selectionCount,
						_hash: this.getHash()
					};
					this._json = json;
				}
				return this._json;
			}

			getHash() {
				if (this._hash == null) {
					let started = false;
					let count = 0;
					let hash = this.cells.filter((cell) => {
						if (!started && cell.state[0] == 0 && cell.state[1] == 1)
							return false;
						started = true;;
						count++;
						return true;
					}).map(cell => {
						return cell.state;
					}).join('');
					this._hash = `${count}${hash}`;
				}
				return this._hash
			}
			equals(otherState) {
				if (otherState.selectionCount != this.selectionCount)
					return false;
				return otherState.getHash() == this.getHash();
			}
			toString() {
				if (this._string == null) {
					this._string = JSON.stringify({
						number: this.number,
						selection: this.selection.map(cell => cell.toString()),
						cells: this.cells
					});
				}
				return this._string
			}
		};
		class Cell {
			row = 0;
			col = 0;
			initial = false;
			letter = null
			name = null
			options = [];
			value = 0;
			values = []
			element = null;
			app = null;
			selected = false;
			container = null;
			async solve(){
				this.app.pushState();
				let originalValues = this.values;
				let valid = false;
				this.selected = false;
				for(let i = 1; i < 10; i++){
					this.addNumber(i);
					if(this.validate())
						if(await this.app.solve())
							return true;
					this.value = 0;
					this.values = originalValues;
					
				}
				return false;
			}
			getState() {
				return `${this.selected ? 1 : 0}${this.valid ? 1 : 0}`;
			}
			toggleSelect() {
				this.selected = !this.selected;

				if (this.selected) {
					this.select();
					//this.element.classList.add('sudoku-cell-selected');
				} else {
					this.deSelect();
					//this.element.classList.remove('sudoku-cell-selected');
				}
				this.update();
				return this.selected;


			}
			async addNumber(value) {
				this.values.pop(value);
				this.values.push(value);
				this.value = Number(value);
				const _this = this;
				window.requestAnimationFrame(() => {
					this.element.textContent = this.value == 0 ? "" : String(this.value);
					
				})
				this.validate();
				//this.update();


			}
			onClick() {
				//this.
				if (!this.initial)
					this.toggleSelect();
				//this.app.onCellClick(this)
			}
			deSelect() {
				this.selected = false;
				this.element.classList.remove('sudoku-cell-selected');
			}
			select() {
				
				this.selected = true;
				this.element.classList.add('sudoku-cell-selected');

			}
			validateBox(){
				let startRow = this.row - this.row % (9 / 3);
				let startCol = this.col - this.col % (9 / 3);
    for (let i = startRow; i < 9 / 3; i++) {
        for (let j = startCol; j < 9 / 3; j++) {
            let fieldIndex = (i * 9) + j;
			console.info(fieldIndex);
			if (this.app.cells[fieldIndex].value == this.value) {
                return false;
            }
        }
		return true
    }
		return true;
			}
			isValid() {
				const _this = this;
				this.valid = !this.value && (!this.app.cells.filter(cell => {
					return cell.value != 0 &&
						cell != _this &&
						(cell.row == _this.row || cell.col == _this.col) &&
						cell.value == _this.value;
				}).length && this.validateBox()); //&& !this.app.getBoxValues(this.name).indexOf(this.value) == -1);
				return this.valid;
			}
			update() {
				if (this.selected)
					this.select()
				else
					this.deSelect()
				this.app.cells.forEach(cell => {
					cell.validate()
				})
				this.element.textContent = this.value ? String(this.value) : "";

			}
			validate() {
				if (this.isValid() || !this.value) {
					this.element.classList.remove('sudoku-cell-invalid');
				} else {
					this.element.classList.add('sudoku-cell-invalid');
				}
				return this.valid;
			}
			destructor() {
				//this.container.delete();
			}
			constructor(app, row, col) {
				this.app = app;
				this.container = document.importNode(cell_template.content, true);
				this.row = row;
				this.col = col;
				this.selected = false;
				this.letter = "abcdefghi"[row];
				this.name = `${this.letter}${this.col}`
				this.value = 0;
				this.values = [];
				this.element = this.container.querySelector('.cell-content');
				this.valid = true;
				const _this = this;
				this.element.addEventListener('click', (e) => {
					_this.onClick();
				});

				this.element.addEventListener('mousemove', (e) => {



					if (!_this.initial && e.buttons == 1)
						_this.select();
					else if (!_this.initial && e.buttons == 2)
						_this.deSelect();
					else
						_this.app.pushState()

				});

				this.element.addEventListener('mouseexit', (e) => {
					if (!e.buttons) {
						//		_this.app.pushState();
					}
				});
				this.element.addEventListener('contextmenu', (e) => {
					e.preventDefault();

					if (!_this.initial && _this.selected) {
						_this.deSelect();
					} else {
						_this.values.pop(_this.value);
						_this.addNumber(0);
						_this.deSelect();
						_this.update();
						//_this.app.deSelectAll();
					}
				});
			}

			redraw() {

			}
			render(container) {
				container.appendChild(this.container);
			}
			toString() {
				return `${this.row}:${this.col}`
			}
		}
		class SudokuParser {
			app = null
			element = null
			container = null
			blank = true
			content = ''
			size = 9
			constructor(app, container) {
				//this.container = container;
				this.app = app;
				this.container = document.importNode(sudokuParserTemplate.content, true);
				this.element = document.createElement('div');
				this.element.style.width = '90%';
				this.element.style.height = '50%';
				this.element.border = '1px solid #CCCCCC';
				this.content = '';
				this.cells = []
				this.element.contentEditable = true;
				this.element.textContent = 'Paste puzzle here';//this.container.querySelector('.parser-content');
				this.toggle();
				this.blank = true;
				const _this = this;
				this.element.addEventListener('click', (e) => {
					if (_this.blank)
						_this.element.textContent = '';
					_this.blank = false;

				});
				this.element.addEventListener('contextmenu', (e) => {
					if (_this.blank)
						_this.element.textContent = '';
					this.blank = false;


				});
				this.element.addEventListener('input', (e) => {
					_this.element.innerHTML = this.element.textContent;
					_this.parseAndApply();
				});
				this.element.addEventListener('keyup', (e) => {
					//_this.parseAndApply();
				});
				container.appendChild(this.element);
			}
			parseAndApply() {
				this.parse();
				this.apply();
			}
			parse() {
				const content = this.element.textContent;
				const regex = /\d/g;
				const matches = [...content.matchAll(regex)];
				let row = 0;
				let col = 0;
				const cells = []
				const max = this.size * this.size;

				matches.forEach(match => {
					if (row * col == max) {
						return;
					}
					if (col == 9) {
						row++;
						col = 0;
					}
					if (row == 9) {
						row = 0;
						col = 0;
					}
					const digit = match[0];
					const cell = new Cell(this.app, row, col);
					cell.addNumber(digit);
					cell.initial = true;
					cells.push(cell);
					col++;
				});
				this.cells = cells;
			}
			apply() {
				this.app.cells.forEach(cell => {
					cell.initial = false;
					cell.number = 0;
					cell.numbers = [];
					cell.addNumber(0);
				});
				this.cells.forEach(cell => {
					const appCell = this.app.getCellByName(cell.name);
					appCell.initial = cell.value != 0;
					appCell.addNumber(cell.value);

				});
				this.toggle();
			}
			toggle() {
				if (this.element.style.display == 'none') {
					this.element.innerHTML = 'Paste here your puzzle';
				}
				this.element.style.display = this.element.style.display != 'none' ? 'none' : 'block';

			}
		}
		class Sudoku {
			cells = [];
			game_size = 0;
			cell_count = 0;
			selectedCells = []
			container = null
			element = null
			states = []
			previousSelection = []
			state_number = 1
			parser = null;
			status = null;
			reset(){
				this.cells.forEach(cell=>{
					cell.values = []
					cell.selected = false;
					cell.addNumber(0);
				})	
			}
			loadSession() {
				const session = this.getSession();
				if (!session)
					return null;
				this.state_number = session.state_number;
				this.states = session.states.map(state => {
					return new State(state);
				});
				this.refreshState();

			}
			
			async getEmptyCell(){
				return this.cells.filter(cell=>{
					if(cell.value == 0)
						return true
					return false 
				})[0]
			}
			async solve() {
				const cell = await this.getEmptyCell();
				if(!cell)
					return this.isValid();
				return await cell.solve();
			}
			deleteSession(){
				localStorage.removeItem('session');
			}
			getSession() {
				const session = localStorage.getItem('session');
				if (!session) {
					return null
				}
				return JSON.parse(session);
			}
			saveSession() {
				this.pushState();
				const states = this.states.map(state => {
					return state.toJson()
				});
				const session = {
					state_number: this.state_number,
					states: states
				}
				localStorage.setItem('session', JSON.stringify(session));
				//console.info('session saved');
			}
			getBoxValues(cell_name) {
				let values = this.cells.filter(cell => {
					return cell.name != cell_name && cell.name[0] == cell_name[0]
				}).map(cell => {
					return cell.value
				});
				return values;
			}
			toggle() {
				this.container.style.display = this.container.style.display != 'none' ? 'none' : 'block';
			}
			toggleParser() {

				//this.parser.toggle();
				this.deSelectAll();
				this.parser.toggle()

			}
			constructor(container, game_size) {
				const _this = this;
				this.container = container
				this.element = container

				this.parser = new SudokuParser(this, this.container);
				this.game_size = game_size;
				this.cell_count = game_size * game_size;
				for (let row = 0; row < this.game_size; row++) {
					for (let col = 0; col < this.game_size; col++) {
						this.cells.push(new Cell(this, row, col));
					}
				}
				console.info("Loading session");
				setTimeout(()=>{
					if(_this.status == "applying state"){
						_this.deleteSession();
						window.location.reload();
					}else{
						console.info("Finished session validation");
					}
				},10000);
				this.loadSession();
				document.addEventListener('keypress', (e) => {

					if (!isNaN(e.key) || e.key == 'd') {
						let number = e.key == 'd' ? 0 : Number(e.key);
						_this.addNumberToSelection(number);
						//console.info({set:Number(e.key)});
					}
					if (e.key == 'p') {
						_this.toggleParser();
					}
					if (e.key == 'u') {
						_this.popState();
					}
					if (e.key == 'r') {
						if (this.selection().length) {
							this.pushState();
							_this.deSelectAll();
						} else {
							let state = this.getLastState();
							if (state) {
								state.cells.filter(cell => cell.selected).forEach(cell => {

									this.getCellByName(cell.name).select();
								})
							}
						}
					}
				});
				this.element.addEventListener('mousemove', (e) => {
					//this.pushState();
				})
				document.addEventListener('dblclick', (e) => {
					_this.previousSelection = _this.selection();
					_this.cells.forEach(cell => {
						cell.deSelect();
					});
				});
				document.addEventListener('contextmenu', (e) => {

				});
				this.element.addEventListener('mouseexit', (e) => {
					// Edge case while holding mouse button while dragging out. Should save state
					_this.pushState();
				});
				this.element.addEventListener('mouseup', (e) => {
					_this.pushState();
				});
				this.pushState()

			}
			isValid() {
				return this.cells.filter(cell => !cell.isValid()).length == 0
			}
			createState() {
				const state = new State(this.state_number)
				let selectedCount = 0;
				state.cells = this.cells.map(cell => {
					if (cell.selected) {
						selectedCount++;
					}
					return { name: cell.name, values: cell.values, value: cell.value, selected: cell.selected, state: cell.getState() }

				});
				state.selectedCount = selectedCount;
				return state;
			}
			pushState() {
				const state = this.createState();
				const previousState = this.getLastState();
				if (!previousState || !previousState.equals(state)) {
					this.states.push(state);
					this.state_number++;
					this.saveSession();
					//console.info({ pushState: state.getHash(), length: state.getHash().length, number: state.number });
				}
			}
			refreshState() {
				const state = this.getLastState();
				if (!state)
					return null;
				this.applyState(state)
				return state;
			}
			getLastState() {
				return this.states.length ? this.states.at(this.states.length - 1) : null;
			}
			applyState(state) {
				this.status = "applying state";
				state.cells.forEach(stateCell => {
					const cell = this.getCellByName(stateCell.name);
					cell.selected = stateCell.selected;
					cell.values = stateCell.values;
					cell.value = stateCell.value;
					cell.update();
				})
				this.status = "applied state"

			}
			popState() {
				let state = this.states.pop();

				if (!state)
					return;
				if (state.equals(this.createState())) {
					return this.popState();
				}

				this.applyState(state);
				this.saveSession();
				//console.info({ popState: state.getHash(), length: state.getHash().length, number: state.number });
			}
			getCellByName(name) {
				return this.cells.filter(cell => {
					return cell.name == name
				})[0]
			}
			deSelectAll() {
				this.cells.forEach(cell => {
					cell.deSelect();
				});
			}
			selection() {
				return this.cells.filter(cell => {
					return cell.selected
				});
			}
			onSelectionToggle() {

			}
			addNumberToSelection(number) {
				const _this = this;
				this.pushState();
				this.selection().forEach((cell) => {



					cell.addNumber(number)
					cell.update();

				})
				_this.pushState();
				if (this.isValid()) {
					this.deSelectAll();
				}
				_this.pushState();
			}
			onCellDblClick(cell) {
				this.previousSelection = this.selection();
				this.popState()
				let originalSelected = this.selctedCells
				if (cell.selected) {
					this.selectedCells.push(cell);
				} else {
					this.selectedCells.pop(cell);
				}
				if (!this.originalSelected != this.selectedCells) {
					this.popState();
				}
				//console.info({selected:this.selectedCells});
			}
			render() {


				this.cells.forEach(cell => {
					cell.render(this.element);
				});
			}
		}
		const sudoku = new Sudoku(container, 9);
		
		sudoku.render();
		const app = sudoku;
		/*
		document.addEventListener('contextmenu',(e)=>{
			e.preventDefault();
		});*/
		/*
		for(let i = 0; i < game_size*game_size; i++){
			const cell = document.importNode(cell_template.content,true);

			app.appendChild(cell);
		}*/
		//document.body.appendChild(app);
	</script>
</body>