function randInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function cssRuleExists(match) {
    for (let i = 0; i < document.styleSheets.length; i++) {
        let styleSheet = document.styleSheets[i]
        let rules = styleSheet.cssRules
        for (let j = 0; j < rules.length; j++) {
            let rule = rules[j]
            if (rule.selectorText && rule.selectorText == match) {
                return true;
            }
        }
    }
    return false
}

class EventHandler {
    constructor() {
        this.events = {};
        this.eventCount = 0;
        this.suppresEvents = false;
        this.debugEvents = false;
    }

    on(event, listener) {
        if (!this.events[event]) {
            this.events[event] = {name: name, listeners: [], callCount: 0};
        }
        this.events[event].listeners.push(listener);
    }

    off(event, listenerToRemove) {
        if (!this.events[event]) return;
        this.events[event].listeners = this.events[event].listeners.filter(listener => listener !== listenerToRemove);
    }

    emit(event, data) {
        if (!this.events[event]) return [];
        if (this.suppresEvents) return [];
        this.eventCount++;
        const returnValue = this.events[event].listeners.map(listener => {
            var returnValue = listener(data)
            if (returnValue == undefined)
                return null
            return returnValue
        });
        this.events[event].callCount++;
        if (this.debugEvents) {
            console.debug('debugEvent', {
                event: event,
                arg: data,
                callCount: this.events[event].callCount,
                number: this.eventCount,
                returnValues: returnValue
            })
        }
        return returnValue
    }

    suppres(fn) {
        const originallySuppressed = this.suppresEvents
        this.suppresEvents = true
        fn(this)
        this.suppresEvents = originallySuppressed
        return originallySuppressed
    }
}


class Col extends EventHandler {
    id = 0
    values = []
    initial = false
    _marked = false
    row = null
    index = 0
    valid = true
    _selected = false
    _value = 0

    _isValidXy() {
        if (!this._value)
            return true;
        return this.row.puzzle.fields.filter(field => {
            return (
                field.index == this.index && field.value == this._value
                ||
                field.row.index == this.row.index && field.value == this._value)

        }).filter(field => field != this).length == 0
    }

    mark() {
        this.marked = true
    }

    unmark() {
        this.marked = false
    }

    select() {
        this.selected = true
    }

    unselect() {
        this.selected = false
    }

    _isValidBox() {
        if (!this._value)
            return true;
        let startRow = this.row.index - this.row.index % (this.row.puzzle.size / 3);
        let startCol = this.index - this.index % (this.row.puzzle.size / 3);
        for (let i = 0; i < this.row.puzzle.size / 3; i++) {
            for (let j = 0; j < this.row.puzzle.size / 3; j++) {
                const field = this.row.puzzle.get(i + startRow, j + startCol);
                if (field != this && field.value == this._value) {
                    return false;
                }
            }
        }
        return true
    }

    validate() {
        if (!this.row.puzzle.initalized) {
            return this.valid;
        }
        if (this.initial) {
            this.valid = true
            return this.valid
        }
        if (!this.value && !this.valid) {
            this.valid = true
            this.emit('update', this)
            return this.valid
        }
        let oldValue = this.valid
        this.valid = this._isValidXy() && this._isValidBox();
        if (oldValue != this.valid) {
            this.emit('update', this)
        }
        return this.valid
    }

    set value(val) {
        if (this.initial)
            return;
        const digit = Number(val)
        const validDigit = digit >= 0 && digit <= 9;
        let update = validDigit && digit != this.value
        if (update) {
            this._value = Number(digit)
            this.validate()
            this.emit('update', this);
        }
    }

    get value() {
        return this._value
    }

    get selected() {
        return this._selected
    }

    set selected(val) {
        if (val != this._selected) {
            this._selected = val
            if (this.row.puzzle.initalized)
                this.emit('update', this);
        }
    }

    get marked() {
        return this._marked
    }

    set marked(val) {
        if (val != this._marked) {
            this._marked = val
            if (this.row.puzzle.initalized) {
                this.emit('update', this)
            }
        }
    }

    constructor(row) {
        super()
        this.row = row
        this.index = this.row.cols.length
        this.id = this.row.puzzle.rows.length * this.row.puzzle.size + this.index;
        this.initial = false
        this.selected = false
        this._value = 0;
        this.marked = false
        this.valid = true
    }

    update() {
        this.emit('update', this)
    }

    toggleSelected() {
        this.selected = !this.selected
    }

    toggleMarked() {
        this.marked = !this.marked
    }

    get data() {
        return {
            values: this.values,
            value: this.value,
            index: this.index,
            id: this.id,
            row: this.row.index,
            col: this.index,
            valid: this.valid,
            initial: this.initial,
            selected: this.selected,
            marked: this.marked
        }
    }

    toString() {
        return String(this.value)
    }

    toText() {
        return this.toString().replace("0", " ");
    }
}

class Row extends EventHandler {
    cols = []
    puzzle = null
    index = 0
    initialized = false

    constructor(puzzle) {
        super()
        this.puzzle = puzzle
        this.cols = []
        this.index = this.puzzle.rows.length
        const me = this
        this.initialized = false
        for (let i = 0; i < puzzle.size; i++) {
            const col = new Col(this);
            this.cols.push(col);
            col.on('update', (field) => {
                me.emit('update', field)
            })
        }
        this.initialized = true
    }

    get data() {
        return {
            cols: this.cols.map(col => col.data),
            index: this.index
        }
    }

    toText() {
        let result = ''
        for (let col of this.cols) {
            result += col.toText();
        }
        return result
    }

    toString() {
        return this.toText().replaceAll(" ", "0");
    }
}

class Puzzle extends EventHandler {
    rows = []
    size = 0
    hash = 0
    states = []
    parsing = false
    _initialized = false
    initalized = false
    _fields = null

    constructor(arg) {
        super()
        this.debugEvents = true;
        this.initalized = false
        this.rows = []
        if (isNaN(arg)) {
            // load session
        } else {
            this.size = Number(arg)
        }
        for (let i = 0; i < this.size; i++) {
            const row = new Row(this);
            this.rows.push(row);
            row.on('update', (field) => {
                this.onFieldUpdate(field)
            })
        }
        this._initialized = true
        this.initalized = true
        this.commitState()
    }

    validate() {
        return this.valid;
    }

    _onEventHandler() {
        this.eventCount++;
    }

    makeInvalid() {
        if (!app.valid) {
            let invalid = this.invalid;
            return invalid[invalid.length - 1];
        }
        this.rows.forEach(row => {
            row.cols.forEach(col => {
                if (col.value) {
                    let modify = null;
                    if (col.index == this.size) {
                        modify = this.get(row.index, col.index - 2);
                    } else {
                        modify = this.get(row.index, col.index + 1);
                    }
                    modify.value = col.value
                    // last one is invalid
                    return modify.index > col.index ? modify : col;
                }
                col.valid = false
            })
        })
        this.get(0, 0).value = 1;
        this.get(0, 1).value = 1;
        return this.get(0, 1);
    }

    reset() {
        this._initialized = false
        this.initalized == false;
        this.parsing = true
        this.fields.forEach(field => {
            field.initial = false
            field.selected = false
            field.marked = false
            field.value = 0
        })
        this.hash = 0
        this.states = []
        this.parsing = false
        this.initalized = true
        this._initialized = true
        this.commitState()
    }

    get valid() {
        return this.invalid.length == 0
    }

    get invalid() {
        this.emit('validating', this)
        const result = this.fields.filter(field => !field.validate())
        this.emit('validated', this)
        return result
    }

    get selected() {
        return this.fields.filter(field => field.selected)
    }

    get marked() {
        return this.fields.filter(field => field.marked)
    }

    loadString(content) {
        this.emit('parsing', this)
        this.reset()
        this.parsing = true
        this.initalized = false;
        this._initialized = false;

        const regex = /\d/g;
        const matches = [...content.matchAll(regex)]
        let index = 0;
        const max = this.size * this.size;
        matches.forEach(match => {
            const digit = Number(match[0]);
            let field = this.fields[index]
            field.value = digit;
            field.initial = digit != 0
            index++;
        });
        this._initialized = true;
        this.parsing = false
        this.deselect();
        this.initalized = true;
        this.suppres(() => {
            this.fields.forEach((field) => {
                field.update()
            })
        })
        this.commitState()
        this.emit('parsed', this)
        this.emit('update', this)
    }

    get state() {
        return this.getData(true)
    }

    get previousState() {
        if (this.states.length == 0)
            return null;
        return this.states.at(this.states.length - 1)
    }

    get stateChanged() {
        if (!this._initialized)
            return false
        return !this.previousState || this.state != this.previousState
    }

    commitState() {
        if (!this.initalized)
            return false;
        this.hash = this._generateHash()
        if (this.stateChanged) {
            this.states.push(this.state)
            this.emit('commitState', this)
            return true
        }
        return false
    }

    onFieldUpdate(field) {
        if (!this.initalized)
            return false;
        if (!this._initialized)
            return;
        this.validate();
        this.commitState();
        this.emit('update', this)
    }

    get data() {
        return this.getData(true)
    }

    popState() {
        let prevState = this.previousState
        if (!prevState) {
            this.deselect()
            return null
        }
        while (prevState && prevState.hash == this.state.hash)
            prevState = this.states.pop()
        if (!prevState) {
            this.deselect()
            return null
        }
        this.applyState(prevState)
        this.emit('popState', this)
        return prevState
    }

    applyState(newState) {

        this._initialized = false
        newState.fields.forEach(stateField => {
            let field = this.get(stateField.row, stateField.col)
            field.selected = stateField.selected
            field.values = stateField.values
            field.value = stateField.value
            field.initial = stateField.initial
            field.validate()
        })
        this._initialized = true
        this.emit('stateApplied', this)
        this.emit('update', this)
    }

    getData(withHash = false) {
        let result = {
            fields: this.fields.map(field => field.data),
            size: this.size,
            valid: this.valid
        }
        if (withHash) {
            result['hash'] = this._generateHash()
        }
        return result;
    }

    get(row, col) {
        if (!this.initalized)
            return null;
        if (!this.rows.length)
            return null;
        if (!this.rows[row])
            return null;
        return this.rows[row].cols[col];
    }

    get fields() {
        if (this._fields == null) {
            this._fields = []
            for (let row of this.rows) {
                for (let col of row.cols) {
                    this._fields.push(col)
                }
            }
        }
        return this._fields
    }

    _generateHash() {
        var result = 0;
        JSON.stringify(this.getData(false)).split('').map(char => {
            return char.charCodeAt(0) - '0'.charCodeAt(0)
        }).forEach(num => {
            result += 26
            result = result + num
        })
        return result
    }

    get text() {
        let result = ''
        for (let row of this.rows) {
            result += row.toText() + "\n"
        }
        result = result.slice(0, result.length - 1)
        return result
    }

    get initialFields() {
        return this.fields.filter(field => field.initial)
    }

    get json() {
        return JSON.stringify(this.data)
    }

    get zeroedText() {
        return this.text.replaceAll(" ", "0")
    }

    get string() {
        return this.toString()
    }

    toString() {
        return this.text.replaceAll("\n", "").replaceAll(" ", "0")
    }

    get humanFormat() {
        return ' ' + this.text.replaceAll(" ", "0").split("").join(" ")
    }

    getRandomField() {
        const emptyFields = this.empty;
        return emptyFields[randInt(0, emptyFields.length - 1)]
    }

    update(callback) {
        this.commitState()
        this.intalized = false
        callback(this);
        this.intalized = true
        this.validate()
        this.commitState()
        this.intalized = false
        if (this.valid)
            this.deselect()
        this.intalized = true
        this.emit('update', this)
    }

    get empty() {
        return this.fields.filter(field => field.value == 0)
    }

    getRandomEmptyField() {
        let field = this.getRandomField()
        if (!field)
            return null
        return field
    }

    deselect() {
        this.fields.forEach(field => field.selected = false)
    }

    generate() {
        this.reset()
        this.initalized = false
        for (let i = 0; i < 17; i++) {
            this.fillRandomField()
        }
        this.deselect()
        this.initalized = true
        this.commitState()
        this.emit('update', this)
    }

    fillRandomField() {
        let field = this.getRandomEmptyField()
        if (!field)
            return
        this.deselect()
        field.selected = true
        let number = 0
        number++;

        while (number <= 9) {
            field.value = randInt(1, 9)
            field.update()
            if (this.validate()) {
                field.initial = true
                return field
            }
            number++;
        }
        return false;
    }

}

class PuzzleManager {
    constructor(size) {
        this.size = size
        this.puzzles = []
        this._activePuzzle = null
    }

    addPuzzle(puzzle) {
        this.puzzles.push(puzzle)
    }

    get active() {
        return this.activePuzzle
    }

    set activePuzzle(puzzle) {
        this._activePuzzle = puzzle
    }

    get activePuzzle() {
        return this._activePuzzle
    }
}

const puzzleManager = new PuzzleManager(9)

class Sudoku extends HTMLElement {
    styleSheet = `
        .sudoku {
            font-size: 13px;
            color:#222;
            display: grid;
            grid-template-columns: repeat(9, 1fr);
            grid-template-rows: auto;
            gap: 0px;
            user-select: none;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            background-color: #e5e5e5;
            border-radius: 5px;
            aspect-ratio: 1/1;
        }
        .sudoku-field-initial {
            color: #777;
        }
        .sudoku-field-selected {
            background-color: lightgreen;
        }
        .soduku-field-marked {
            background-color: blue;
        }
        .sudoku-field-invalid {
            color: red;
        }
        .sudoku-field {
            border: 1px solid #ccc;
            text-align: center;
            padding: 2px;
            aspect-ratio: 1/1;
        }`

    set fieldSize(val) {
        this._fieldSize = val ? Number(val) : null
        this.fieldElements.forEach(field => {
            field.style.fontSize = this._fieldSize ? this._fieldSize.toString() + 'px' : ''
        })
    }

    get fieldSize() {
        return this._fieldSize
    }

    get eventCount() {
        return this.puzzle.eventCount
    }

    get puzzleContent() {
        return this.puzzle.humanFormat
    }

    set puzzleContent(val) {
        if (val == "generate") {
            this.puzzle.generate()
        } else if (val) {
            this.puzzle.loadString(val)
        } else {
            this.puzzle.reset()
        }
    }

    connectedCallback() {
        this.puzzleContent = this.getAttribute('puzzle') ? this.getAttribute('puzzle') : null
        this._fieldSize = null
        this.fieldSize = this.getAttribute('size') ? this.getAttribute('size') : null
        this.readOnly = !!this.getAttribute('read-only')
        this.attachShadow({mode: 'open'});
        this.shadowRoot.appendChild(this.styleElement)
        this.shadowRoot.appendChild(this.puzzleDiv)
    }

    toString() {
        return this.puzzleContent
    }

    set active(val) {
        this._active = val
        if (this._active)
            this.manager.activePuzzle = this
    }

    get active() {
        return this._active
    }

    set readOnly(val) {
        this._readOnly = !!val
    }

    get readOnly() {
        return this._readOnly
    }

    constructor() {
        super();
        this._readOnly = false;
        this._active = false
        this.fieldElements = []
        this.puzzle = new Puzzle(9)
        this.fields = []
        this.styleElement = document.createElement('style');
        this.styleElement.textContent = this.styleSheet
        this.puzzleDiv = document.createElement('div')
        this.puzzleDiv.classList.add('sudoku');
        this._bind()
        this.manager.addPuzzle(this)
    }

    get manager() {
        return puzzleManager
    }

    _bind() {
        this._bindFields()
        this._bindEvents()
        this._sync()
    }

    _bindFields() {
        const me = this
        this.puzzle.rows.forEach((row) => {
            row.cols.forEach((field) => {
                const fieldElement = document.createElement('div');
                fieldElement.classList.add('sudoku-field');
                fieldElement.field = field
                field.on('update', (field) => {
                    me._sync()
                })
                fieldElement.addEventListener('click', (e) => {
                    if (!me.readOnly)
                        field.toggleSelected()
                })
                fieldElement.addEventListener('contextmenu', (e) => {
                    e.preventDefault()
                    field.row.puzzle.update(() => {
                        field.selected = false
                        field.value = 0
                    })
                })
                this.fields.push(field)
                this.fieldElements.push(fieldElement)
                this.puzzleDiv.appendChild(fieldElement);
            });
        });
    }

    _bindEvents() {
        const me = this
        this.puzzle.on('update', () => {
            me._sync()
        });
        this.puzzleDiv.addEventListener('mouseenter', (e) => {
            me.active = true
        })
        this.puzzleDiv.addEventListener('mouseexit', (e) => {
            me.active = false
        })
        document.addEventListener('keydown', (e) => {
            if (me.readOnly)
                return
            if (!puzzleManager.active)
                return
            const puzzle = puzzleManager.active.puzzle
            if (e.key == 'u') {
                puzzle.popState();
            } else if (e.key == 'd') {
                puzzle.update((target) => {
                    puzzle.selected.forEach(field => {
                        field.value = 0
                    });
                })
            } else if (e.key == 'a') {
                puzzle.autoSolve()
            } else if (e.key == 'r') {
                puzzle.fillRandomField();
            } else if (!isNaN(e.key)) {
                puzzle.update((target) => {
                    puzzle.selected.forEach(field => {
                        field.value = Number(e.key)
                    })
                });
            } else if (e.key == 'm') {
                let fields = [];
                puzzle.update((target) => {
                    target.selected.forEach(field => {
                        field.selected = false;
                        fields.push(field)
                    });
                });
                puzzle.update((target) => {
                    fields.forEach((field) => {
                        field.toggleMarked();
                    })
                });
                puzzle.emit('update', puzzle);
            }
        })
    }

    autoSolve() {
        window.requestAnimationFrame(() => {
            if (this.fillRandomField()) {
                if (this.empty.length)
                    return this.autoSolve()
            }
        })
    }

    get(row, col) {
        return this.puzzle.get(row, col)
    }

    _syncField(fieldElement) {
        const field = fieldElement.field
        fieldElement.classList.remove('sudoku-field-selected', 'sudoku-field-empty', 'sudoku-field-invalid', 'sudoku-field-initial', 'sudoku-field-marked')
        console.info('Removed marked class');
        fieldElement.innerHTML = field.value ? field.value.toString() : '&nbsp;'

        if (field.selected) {
            fieldElement.classList.add('sudoku-field-selected')
            window.selected = field.field
        }
        if (!field.valid) {
            fieldElement.classList.add('sudoku-field-invalid')
        }
        if (!field.value) {
            fieldElement.classList.add('sudoku-field-empty')
        }
        if (field.initial) {
            fieldElement.classList.add('sudoku-field-initial')
        }
        if (field.marked) {
            fieldElement.classList.add('sudoku-field-marked')
            console.info("added marked lcass")
        }

    }

    _sync() {
        this.fieldElements.forEach(fieldElement => {
            this._syncField(fieldElement);
        })
    }

}

customElements.define("my-sudoku", Sudoku);

function generateIdByPosition(element) {
    const parent = element.parentNode;
    const index = Array.prototype.indexOf.call(parent.children, element);
    const generatedId = `${element.tagName.toLowerCase()}-${index}`;
    element.id = generatedId.replace('div-', 'session-key-');
    return element.id;
}