import { Camera } from './Camera'
import { Simulation } from '../../simulation/classes/Simulation'
import { Subject } from 'rxjs'
import { MouseEventInfo } from '../../core/components/MouseEventInfo'
import { pointInSquare } from '../../../common/math/helpers/pointInSquare'
import { vector2 } from '../../../common/math/types/vector2'
import { relativeTo, add, invert } from '../../vector2/helpers/basic'
import { SimulationRendererOptions } from '../types/SimulationRendererOptions'
import {
    defaultSimulationRendererOptions,
    mouseButtons,
    shiftInput
} from '../constants'
import { getPinPosition } from '../helpers/pinPosition'
import { pointInCircle } from '../../../common/math/helpers/pointInCircle'
import { SelectedPins } from '../types/SelectedPins'
import { Wire } from '../../simulation/classes/Wire'
import { currentStore } from '../../saving/stores/currentStore'
import { saveStore } from '../../saving/stores/saveStore'
import {
    fromSimulationState,
    fromCameraState
} from '../../saving/helpers/fromState'
import merge from 'deepmerge'
import { handleScroll } from '../helpers/scaleCanvas'
import { RefObject } from 'react'
import { dumpSimulation } from '../../saving/helpers/dumpSimulation'
import { modalIsOpen } from '../../modals/helpers/modalIsOpen'
import { SimulationError } from '../../errors/classes/SimulationError'
import { deleteWire } from '../../simulation/helpers/deleteWire'
import { RendererState, WireState } from '../../saving/types/SimulationSave'
import { setToArray } from '../../../common/lang/arrays/helpers/setToArray'
import { Transform } from '../../../common/math/classes/Transform'
import { gatesInSelection } from '../helpers/gatesInSelection'
import { selectionType } from '../types/selectionType'
import { addIdToSelection, idIsSelected } from '../helpers/idIsSelected'
import { GateInitter } from '../types/GateInitter'
import {
    open as propsAreOpen,
    id as propsModalId
} from '../../logic-gates/subjects/LogicGatePropsSubjects'

export class SimulationRenderer {
    public mouseDownOutput = new Subject<MouseEventInfo>()
    public mouseUpOutput = new Subject<MouseEventInfo>()
    public mouseMoveOutput = new Subject<MouseEventInfo>()
    public wheelOutput = new Subject<unknown>()

    public selectedGates: Record<selectionType, Set<number>> = {
        temporary: new Set<number>(),
        permanent: new Set<number>()
    }

    public options: SimulationRendererOptions
    public camera = new Camera()

    public selectedArea = new Transform()
    public clipboard: GateInitter[] = []
    public wireClipboard: WireState[] = []

    // first bit = dragging
    // second bit = panning
    // third bit = selecting
    public mouseState = 0b000

    public lastMousePosition: vector2 = [0, 0]

    // this is used for spawning gates
    public spawnCount = 0

    public selectedPins: SelectedPins = {
        start: null,
        end: null
    }

    public constructor(
        options: Partial<SimulationRendererOptions> = {},
        public simulation = new Simulation('project', 'default')
    ) {
        this.options = merge(defaultSimulationRendererOptions, options)

        this.init()
    }

    public init() {
        this.mouseDownOutput.subscribe(event => {
            const worldPosition = this.camera.toWordPostition(event.position)
            const gates = Array.from(this.simulation.gates)

            this.lastMousePosition = worldPosition

            // We need to iterate from the last to the first
            // because if we have 2 overlapping gates,
            // we want to select the one on top
            for (let index = gates.length - 1; index >= 0; index--) {
                const { transform, id, pins, template } = gates[index]

                if (pointInSquare(worldPosition, transform)) {
                    if (event.button === mouseButtons.drag) {
                        gates[index].onClick()

                        this.mouseState |= 1

                        if (!idIsSelected(this, id)) {
                            this.clearSelection()
                            addIdToSelection(this, 'temporary', id)
                        }

                        const gateNode = this.simulation.gates.get(id)

                        if (gateNode) {
                            return this.simulation.gates.moveOnTop(gateNode)
                        } else {
                            throw new SimulationError(
                                `Cannot find gate with id ${id}`
                            )
                        }
                    } else if (
                        event.button === mouseButtons.properties &&
                        template.properties.enabled
                    ) {
                        propsModalId.next(id)
                        return propsAreOpen.next(true)
                    }
                }

                for (const pin of pins) {
                    const position = getPinPosition(this, transform, pin)

                    if (
                        pointInCircle(
                            worldPosition,
                            position,
                            this.options.gates.pinRadius
                        )
                    ) {
                        if (pin.value.pairs.size) {
                            if (pin.value.type & 1) {
                                const wire = this.simulation.wires.find(
                                    wire => wire.end.value === pin.value
                                )

                                if (wire) {
                                    deleteWire(this.simulation, wire)
                                } else {
                                    throw new SimulationError(
                                        `Cannot find wire to remove`
                                    )
                                }

                                return
                            }
                        }

                        if (
                            this.selectedPins.start &&
                            pin.value === this.selectedPins.start.wrapper.value
                        ) {
                            this.selectedPins.start = null
                            this.selectedPins.end = null
                        } else if (
                            this.selectedPins.end &&
                            pin.value === this.selectedPins.end.wrapper.value
                        ) {
                            this.selectedPins.start = null
                            this.selectedPins.end = null
                        } else if ((pin.value.type & 2) >> 1) {
                            this.selectedPins.start = {
                                wrapper: pin,
                                transform
                            }
                        } else if (pin.value.type & 1) {
                            this.selectedPins.end = {
                                wrapper: pin,
                                transform
                            }
                        }

                        if (this.selectedPins.start && this.selectedPins.end) {
                            this.simulation.wires.push(
                                new Wire(
                                    this.selectedPins.start.wrapper,
                                    this.selectedPins.end.wrapper
                                )
                            )
                            this.selectedPins.start = null
                            this.selectedPins.end = null
                        }

                        return
                    }
                }
            }

            if (!shiftInput.value && event.button === mouseButtons.unselect) {
                this.clearSelection()
            }

            if (event.button === mouseButtons.pan) {
                // the second bit = pannning
                this.mouseState |= 0b10
            } else if (event.button === mouseButtons.select) {
                this.selectedArea.position = this.lastMousePosition
                this.selectedArea.scale = [0, 0]

                // the third bit = selecting
                this.mouseState |= 0b100
            }
        })

        this.mouseUpOutput.subscribe(event => {
            if (event.button === mouseButtons.drag && this.mouseState & 1) {
                const selected = this.getSelected()

                for (const gate of selected) {
                    gate.transform.rotation = 0
                }

                this.selectedGates.temporary.clear()

                // turn first bit to 0
                this.mouseState &= 6

                // for debugging
                if ((this.mouseState >> 1) & 1 || this.mouseState & 1) {
                    throw new SimulationError(
                        'First 2 bits of mouseState need to be set to 0'
                    )
                }
            }

            if (
                event.button === mouseButtons.pan &&
                (this.mouseState >> 1) & 1
            ) {
                // turn second bit to 0
                this.mouseState &= 5
            }

            if (event.button === mouseButtons.select && this.mouseState >> 2) {
                // turn the third bit to 0
                this.mouseState &= 3

                const selectedGates = gatesInSelection(
                    this.selectedArea,
                    Array.from(this.simulation.gates)
                )

                for (const { id } of selectedGates) {
                    addIdToSelection(this, 'permanent', id)

                    const node = this.simulation.gates.get(id)

                    if (node) {
                        this.simulation.gates.moveOnTop(node)
                    } else {
                        throw new SimulationError(
                            `Cannot find node in gate storage with id ${id}`
                        )
                    }
                }
            }
        })

        this.mouseMoveOutput.subscribe(event => {
            const worldPosition = this.camera.toWordPostition(event.position)

            const offset = invert(
                relativeTo(this.lastMousePosition, worldPosition)
            )

            const scaledOffset = offset.map(
                (value, index) => value * this.camera.transform.scale[index]
            ) as vector2

            if (this.mouseState & 1) {
                for (const gate of this.getSelected()) {
                    const { transform } = gate

                    transform.x -= offset[0]
                    transform.y -= offset[1]
                }
            }

            if ((this.mouseState >> 1) & 1) {
                this.camera.transform.position = add(
                    this.camera.transform.position,
                    invert(scaledOffset)
                )

                this.spawnCount = 0
            }

            if ((this.mouseState >> 2) & 1) {
                this.selectedArea.scale = relativeTo(
                    this.selectedArea.position,
                    this.camera.toWordPostition(event.position)
                )
            }

            this.lastMousePosition = this.camera.toWordPostition(event.position)
        })

        this.reloadSave()
    }

    public updateWheelListener(ref: RefObject<HTMLCanvasElement>) {
        if (ref.current) {
            ref.current.addEventListener('wheel', event => {
                if (!modalIsOpen() && location.pathname === '/') {
                    event.preventDefault()

                    handleScroll(event, this.camera)
                }
            })
        }
    }

    public loadSave(save: RendererState) {
        this.simulation = fromSimulationState(save.simulation)
        this.camera = fromCameraState(save.camera)
    }

    public reloadSave() {
        dumpSimulation(this)

        const current = currentStore.get()
        const save = saveStore.get(current)

        if (!save) return
        if (!(save.simulation || save.camera)) return

        this.loadSave(save)
    }

    /**
     * Gets all selected gates in the simulation
     *
     * @throws SimulationError if an id isnt valid
     * @throws SimulationError if the id doesnt have a data prop
     */
    public getSelected() {
        return setToArray(this.allSelectedIds()).map(id => {
            const gate = this.simulation.gates.get(id)

            if (!gate) {
                throw new SimulationError(`Cannot find gate with id ${id}`)
            } else if (!gate.data) {
                throw new SimulationError(
                    `Cannot find data of gate with id ${id}`
                )
            }

            return gate.data
        })
    }

    /**
     * helper to merge the temporary and permanent selection
     */
    public allSelectedIds() {
        return new Set([
            ...setToArray(this.selectedGates.permanent),
            ...setToArray(this.selectedGates.temporary)
        ])
    }

    /**
     * Helper to clear all selected sets
     */
    public clearSelection() {
        this.selectedGates.permanent.clear()
        this.selectedGates.temporary.clear()
    }

    /**
     * Clears the selected pins of the renderer
     */
    public clearPinSelection() {
        this.selectedPins.end = null
        this.selectedPins.start = null
    }
}