import { Singleton } from '@eix/utils'
import { Component } from '../component'
import { Subject, BehaviorSubject, fromEvent } from 'rxjs'
import { svg, SVGTemplateResult, html } from 'lit-html'
import { subscribe } from 'lit-rx'
import { Screen } from '../screen.ts'
import { ManagerState, ComponentTemplate } from './interfaces'
import { Store } from '../store'
import { KeyboardInput } from '@eix/input'
import { success, error } from 'toastr'
import { ComponentTemplateStore } from './componentTemplateStore'
import { alertOptions } from './alertOptions'
import { WireManager } from '../wires'
import { runCounter } from '../component/runCounter'
import { Settings } from '../store/settings'
import { download } from './download'
import { modal } from '../modals'
import { map } from 'rxjs/operators'
import { persistent } from '../store/persistent'
import { MDCTextField } from '@material/textfield'
import { importComponent } from '../componentImporter/importComponent'

const defaultName = 'default'

@Singleton
export class ComponentManager {
    public components: Component[] = []
    public svgs = new Subject<SVGTemplateResult>()
    public placeholder = new BehaviorSubject('Create simulation')
    public barAlpha = new BehaviorSubject<string>('0')
    public wireManager = new WireManager()
    public onTop: Component
    public templateStore = new ComponentTemplateStore()

    private temporaryCommnad = ''
    private clicked = false
    private ignoreKeyDowns = false

    private screen = new Screen()
    private settings = new Settings()
    private standard: {
        offset: number
        scale: [number, number]
    } = {
        offset: 50,
        scale: [100, 100]
    }

    private commandHistoryStore = new Store<string>('commandHistory')
    private store = new Store<ManagerState>('simulationStates')

    private saveEvent = new KeyboardInput('s')
    private createEvent = new KeyboardInput('m')
    private closeInputEvent = new KeyboardInput('enter')
    private ctrlEvent = new KeyboardInput('ctrl')
    private palleteEvent = new KeyboardInput('p')
    private undoEvent = new KeyboardInput('z')
    private shiftEvent = new KeyboardInput('shift')
    private refreshEvent = new KeyboardInput('r')
    private gEvent = new KeyboardInput('g')
    private clearEvent = new KeyboardInput('delete')
    private upEvent = new KeyboardInput('up')
    private downEvent = new KeyboardInput('down')

    @persistent<ComponentManager, string>(defaultName, 'main')
    public name: string
    public alertOptions = alertOptions

    private commandHistory: string[] = []
    private commands: {
        [key: string]: (
            ctx: ComponentManager,
            args: string[],
            flags: string[]
        ) => any
    } = {
        clear(ctx: ComponentManager) {
            ctx.clear()
        },
        save(ctx: ComponentManager) {
            ctx.save()
        },
        ls(ctx: ComponentManager) {
            const data = ctx.store.ls()
            const message = data.join('\n')

            success(message, '', ctx.alertOptions)
        },
        help(ctx: ComponentManager) {
            success(
                `Usage: &ltcommand> <br>
            Where &ltcommand> is one of:
                <ul>
                ${Object.keys(ctx.commands)
                    .map(
                        val => `
                    <li>${val}</li>
                `
                    )
                    .join('')}
                </ul>
            `,
                '',
                ctx.alertOptions
            )
        },
        refresh(ctx: ComponentManager) {
            ctx.refresh()
        },
        rewind(ctx: ComponentManager) {
            localStorage.clear()
            success('Succesfully cleared localStorage!', '', ctx.alertOptions)
        },
        ctp: this.templateStore.commands.template,
        settings: this.settings.commands,
        download
    }
    private inputMode: string

    public gates = this.templateStore.store.lsChanges
    public saves = this.store.lsChanges

    public file: {
        [key: string]: () => void
    } = {
        clear: () => this.clear(),
        clean: () => this.smartClear(),
        save: () => this.save(),
        undo: () => this.refresh(),
        download: () => download(this, [], []),
        delete: () => this.delete(this.name),
        refresh: () => this.silentRefresh(true)
    }

    public shortcuts: {
        [key: string]: string
    } = {
        clear: 'shift delete',
        clean: 'delete',
        save: 'ctrl s',
        undo: 'ctrl z',
        refresh: 'ctrl r'
    }

    constructor() {
        runCounter.increase()

        this.svgs.next(this.render())

        this.refresh()

        fromEvent(document.body, 'keydown').subscribe((e: KeyboardEvent) => {
            if (this.barAlpha.value == '1') {
                const elem = document.getElementById('nameInput')
                elem.focus()
            } else if (!this.ignoreKeyDowns) {
                e.preventDefault()
            }
        })

        fromEvent(document.body, 'keyup').subscribe((e: MouseEvent) => {
            if (this.barAlpha.value === '1') {
                if (this.closeInputEvent.value) this.create()
                else if (this.inputMode === 'command') {
                    const elem = <HTMLInputElement>(
                        document.getElementById('nameInput')
                    )
                    if (this.upEvent.value) {
                        document.body.focus()
                        e.preventDefault()
                        const index = this.commandHistory.indexOf(elem.value)

                        if (index) {
                            //save drafts
                            if (index === -1) this.temporaryCommnad = elem.value

                            const newIndex =
                                index === -1
                                    ? this.commandHistory.length - 1
                                    : index - 1
                            elem.value = this.commandHistory[newIndex]
                        }
                    }
                    if (this.downEvent.value) {
                        document.body.focus()
                        e.preventDefault()
                        const index = this.commandHistory.indexOf(elem.value)

                        if (index > -1) {
                            const maxIndex = this.commandHistory.length - 1
                            elem.value =
                                index === maxIndex
                                    ? this.temporaryCommnad
                                    : this.commandHistory[index + 1]
                        }
                    }
                }
            } else {
                if (this.ctrlEvent.value) {
                    if (this.createEvent.value) {
                        this.prepareNewSimulation()
                    } else if (
                        this.shiftEvent.value &&
                        this.palleteEvent.value
                    ) {
                        this.preInput()
                        this.inputMode = 'command'
                        this.placeholder.next('Command palette')
                    } else if (this.gEvent.value) {
                        this.importGate()
                    } else if (this.saveEvent.value) {
                        this.save()
                    } else if (this.undoEvent.value) {
                        this.refresh()
                    } else if (this.refreshEvent.value) {
                        this.silentRefresh(true)
                    }
                } else if (this.clearEvent.value) {
                    if (this.shiftEvent.value) this.clear()
                    else this.smartClear()
                }
            }
        })

        this.wireManager.update.subscribe(() => {
            // this.save()
            this.update()
            // this.save()
        })
        if (this.saves.value.length === 0) this.save()
    }

    private initEmptyGate(name: string) {
        const obj: ComponentTemplate = {
            inputs: 1,
            name,
            version: '1.0.0',
            outputs: 1,
            activation: '',
            editable: true,
            material: {
                mode: 'color',
                data: 'blue'
            }
        }

        this.templateStore.store.set(name, obj)

        this.edit(name)
    }

    public newGate() {
        this.preInput()
        this.inputMode = 'gate'
        this.placeholder.next('Gate name')
    }

    public importGate() {
        this.preInput()
        this.inputMode = 'importGate'
        this.placeholder.next('Gate url')
    }

    public prepareNewSimulation() {
        this.preInput()
        this.inputMode = 'create'
        this.placeholder.next('Create simulation')
    }

    private preInput() {
        const elem = <HTMLInputElement>document.getElementById('nameInput')
        elem.value = ''
        this.barAlpha.next('1')
    }

    private async create() {
        const elem = <HTMLInputElement>document.getElementById('nameInput')
        this.barAlpha.next('0')

        if (this.inputMode === 'create') {
            await this.createEmptySimulation(elem.value)
            success(
                `Succesfully created simulation ${elem.value}`,
                '',
                this.alertOptions
            )
        } else if (this.inputMode === 'command') this.eval(elem.value)
        else if (this.inputMode === 'gate') this.initEmptyGate(elem.value)
        else if (this.inputMode === 'importGate') {
            importComponent(this, elem.value)
        }
    }

    public succes(message: string) {
        success(message, '', this.alertOptions)
    }

    private async handleDuplicateModal(name: string) {
        const result = await modal({
            title: 'Warning',
            content: html`
                There was already a simulation called ${name}, are you sure you
                want to override it? All your work will be lost!
            `
        })

        return result
    }

    public async edit(name: string) {
        this.ignoreKeyDowns = true
        const gate = this.templateStore.store.get(name)

        modal({
            no: '',
            yes: 'save',
            title: `Edit ${name}`,
            content: html`
                ${html`
                    <br />
                    <div class="mdc-text-field mdc-text-field--textarea">
                        <textarea
                            id="codeArea"
                            class="mdc-text-field__input js"
                            rows="8"
                            cols="40"
                        >
${gate.activation}</textarea
                        >
                        <div class="mdc-notched-outline">
                            <div class="mdc-notched-outline__leading"></div>
                            <div class="mdc-notched-outline__notch">
                                <label for="textarea" class="mdc-floating-label"
                                    >Activation function</label
                                >
                            </div>
                            <div class="mdc-notched-outline__trailing"></div>
                        </div>
                    </div>
                    <br /><br />
                    <div class="mdc-text-field" id="inputCount">
                        <input
                            type="number"
                            id="my-text-field"
                            class="mdc-text-field__input inputCount-i"
                            value=${gate.inputs}
                        />
                        <label class="mdc-floating-label" for="my-text-field"
                            >Inputs</label
                        >
                        <div class="mdc-line-ripple"></div>
                    </div>
                    <br /><br />
                    <div class="mdc-text-field" id="outputCount">
                        <input
                            type="number"
                            id="my-text-field"
                            class="mdc-text-field__input outputCount-i"
                            value=${gate.outputs}
                        />
                        <label class="mdc-floating-label" for="my-text-field"
                            >Outputs</label
                        >
                        <div class="mdc-line-ripple"></div>
                    </div>
                    <br /><br />
                    <div class="mdc-text-field" id="color">
                        <input
                            type="string"
                            id="my-text-field"
                            class="mdc-text-field__input color-i"
                            value=${gate.material.data}
                        />
                        <label class="mdc-floating-label" for="my-text-field"
                            >Color</label
                        >
                        <div class="mdc-line-ripple"></div>
                    </div>
                    <br />
                `}
            `
        }).then(val => {
            this.ignoreKeyDowns = false
            const elems: (HTMLInputElement | HTMLTextAreaElement)[] = [
                document.querySelector('#codeArea'),
                document.querySelector('.inputCount-i'),
                document.querySelector('.outputCount-i'),
                document.querySelector('.color-i')
            ]
            const data = elems.map(val => val.value)

            this.templateStore.store.set(name, {
                ...gate,
                activation: data[0],
                inputs: Number(data[1]),
                outputs: Number(data[2]),
                material: {
                    mode: 'color',
                    data: data[3]
                }
            })
        })

        new MDCTextField(document.querySelector('.mdc-text-field'))
        new MDCTextField(document.querySelector('#outputCount'))
        new MDCTextField(document.querySelector('#inputCount'))
        new MDCTextField(document.querySelector('#color'))
    }

    public add(template: string, position?: [number, number]) {
        const pos = position
            ? position
            : ([...Array(2)].fill(
                  this.standard.offset * this.components.length
              ) as [number, number])

        this.components.push(new Component(template, pos, this.standard.scale))
        this.update()
    }

    public async delete(name: string) {
        const res = await modal({
            title: 'Are you sure?',
            content: html`
                Deleting a simulations is ireversible, and all work will be
                lost!
            `
        })

        if (res) {
            if (this.name === name) {
                if (this.saves.value.length > 1) {
                    this.switchTo(this.saves.value.find(val => val !== name))
                } else {
                    let newName =
                        name === defaultName ? `${defaultName}(1)` : defaultName
                    await this.createEmptySimulation(newName)
                    this.switchTo(newName)
                }
            }
            this.store.delete(name)
        }
    }

    public createEmptySimulation(name: string) {
        const create = () => {
            this.store.set(name, {
                wires: [],
                components: [],
                position: [0, 0],
                scale: [1, 1]
            })

            if (name !== this.name) this.save()
            this.name = name
            this.refresh()
        }

        return new Promise(async res => {
            //get wheater theres already a simulation with that name
            if (
                (this.store.get(name) &&
                    (await this.handleDuplicateModal(name))) ||
                !this.store.get(name)
            ) {
                create()
                res(true)
            }
        })
    }

    public switchTo(name: string) {
        const data = this.store.get(name)
        if (!data)
            error(
                `An error occured when trying to load ${name}`,
                '',
                this.alertOptions
            )

        this.name = name
        this.refresh()
    }

    eval(command: string) {
        if (!this.commandHistory.includes(command))
            // no duplicates
            this.commandHistory.push(command)

        while (
            this.commandHistory.length > 10 // max of 10 elements
        )
            this.commandHistory.shift()

        const words = command.split(' ')

        if (words[0] in this.commands) {
            const remaining = words.slice(1)
            const flags = remaining.filter(val => val[0] == '-')
            const args = remaining.filter(val => val[0] != '-')
            this.commands[words[0]](this, args, flags)
        } else
            error(
                `Command ${words} doesn't exist. Run help to get a list of all commands.`,
                '',
                this.alertOptions
            )
    }

    public smartClear() {
        this.components = this.components.filter(({ id }) =>
            this.wireManager.wires.find(
                val => val.input.of.id == id || val.output.of.id == id
            )
        )
        this.update()
        success(
            'Succesfully cleared all unconnected components',
            '',
            this.alertOptions
        )
    }

    public clear() {
        this.components = []
        this.wireManager.dispose()
        this.update()

        success('Succesfully cleared all components', '', this.alertOptions)
    }

    public refresh() {
        if (this.store.get(this.name)) {
            this.loadState(this.store.get(this.name))
        }

        for (const i of this.commandHistoryStore.ls())
            this.commandHistory[Number(i)] = this.commandHistoryStore.get(i)

        this.update()

        success(
            'Succesfully refreshed to the latest save',
            '',
            this.alertOptions
        )
    }

    update() {
        this.svgs.next(this.render())
    }

    handleMouseDown() {
        this.clicked = true
    }

    handleMouseUp() {
        this.clicked = false
    }

    handleMouseMove(e: MouseEvent) {
        if (e.button === 0) {
            let toAddOnTop: number
            let outsideComponents = true

            for (let i = 0; i < this.components.length; i++) {
                const component = this.components[i]
                if (component.clicked) {
                    outsideComponents = false
                    component.move(e)
                    if (this.onTop != component) {
                        toAddOnTop = i
                    }
                }
            }

            // if (false) { }
            if (toAddOnTop >= 0) {
                this.top(this.components[toAddOnTop])
            } else if (outsideComponents && this.clicked) {
                const mousePosition = [e.clientX, e.clientY]
                const delta = mousePosition.map(
                    (value, index) => this.screen.mousePosition[index] - value
                ) as [number, number]
                this.screen.move(...delta)
            }
        }
    }

    public silentRefresh(verboose = false) {
        this.loadState(this.state)
        if (verboose)
            success(
                'Succesfully reloaded all components',
                '',
                this.alertOptions
            )
    }

    public top(component: Component) {
        if (this.onTop !== component) {
            this.onTop = component
            this.components.push(component)
        }
        this.update()
    }

    private render() {
        let toRemoveDuplicatesFor: Component

        const result = this.components.map(component => {
            const mouseupHandler = () => {
                component.handleMouseUp()
                toRemoveDuplicatesFor = component
            }

            const stroke = subscribe(
                component.clickedChanges.pipe(
                    map(val => (val ? 'yellow' : 'black'))
                )
            )

            return svg`
            <g>
                ${component.pinsSvg(10, 20)}
                ${component.pinsSvg(10, 20, 'output')}

                <g @mousedown=${(e: MouseEvent) => component.handleClick(e)}
                    @touchstart=${(e: MouseEvent) => component.handleClick(e)}
                    @mouseup=${mouseupHandler}
                    @touchend=${mouseupHandler}>
                    <rect width=${subscribe(component.width)}
                    height=${subscribe(component.height)}
                    x=${subscribe(component.x)}
                    y=${subscribe(component.y)}
                    stroke=${stroke}
                    fill=${
                        component.material.mode !== 'color'
                            ? 'rgba(0,0,0,0)'
                            : subscribe(component.material.color)
                    }
                    rx=20
                    ry=20>
                </rect>
                ${
                    component.material.mode !== 'color'
                        ? component.material.innerHTML(
                              subscribe(component.x),
                              subscribe(component.y),
                              subscribe(component.width),
                              subscribe(component.height)
                          )
                        : ''
                }
                </g>
            </g>
        `
        })

        if (toRemoveDuplicatesFor) this.removeDuplicates(toRemoveDuplicatesFor)

        return svg`${this.wireManager.svg} ${result}`
    }

    private removeDuplicates(component: Component) {
        let instances = this.components
            .map((value, index) => (value == component ? index : null))
            .filter(value => value)
        instances.pop()

        this.components = this.components.filter(
            (_, index) => instances.indexOf(index) != -1
        )
    }

    get state(): ManagerState {
        const components = Array.from(new Set(this.components).values())
        return {
            components: components.map(value => value.state),
            position: this.screen.position as [number, number],
            scale: this.screen.scale as [number, number],
            wires: this.wireManager.state
        }
    }

    get scaling(): Component {
        return this.components.find(val => val.scaling)
    }

    public getComponentById(id: number) {
        return this.components.find(val => val.id === id)
    }

    private loadState(state: ManagerState) {
        if (!state.wires)
            //old state
            return

        this.wireManager.dispose()
        this.clicked = false
        this.components = state.components.map(value =>
            Component.fromState(value)
        )
        this.onTop = null

        state.wires.forEach(val => {
            this.wireManager.start = this.getComponentById(
                val.from.owner
            ).outputPins[val.from.index]
            this.wireManager.end = this.getComponentById(
                val.to.owner
            ).inputPins[val.to.index]
            this.wireManager.tryResolving()
        })

        this.screen.scale = state.scale
        this.screen.position = state.position
        this.screen.update()

        this.update()
    }

    save() {
        for (let i = 0; i < this.commandHistory.length; i++) {
            const element = this.commandHistory[i]
            this.commandHistoryStore.set(i.toString(), element)
        }
        this.store.set(this.name, this.state)
        success(
            `Saved the simulation ${this.name} succesfully!`,
            '',
            this.alertOptions
        )
    }
}