import { EventEmitter } from 'events'
import { ConfigManager, ConfigSections } from './ConfigManager'
import { DeviceManager } from './devices/DeviceManager'
import { VideohubAdapterInterface, VideohubState, OutputInterface, PortLockState, VideohubConfigInterface, InputInterface, SerialInterface, VideohubCommand, CommandTypes, PortLockAction } from '../videohub'
import { VideohubConfiguration } from '../common/MappingTypes'
import { AtemDevice } from './devices/AtemDevice'
import { AtemState } from 'atem-connection'

interface VideohubMapping {
    config: VideohubConfiguration,
    device: VideohubMappedDevice | undefined
    active: boolean,
    port: OutputInterface
}

interface VideohubMappedDevice {
    getOutputPortName(): string
    setInput(id: number): boolean
    getInput(): number
    destroy(): void
}

class AtemVideohubAdapater implements VideohubMappedDevice {
    private device: AtemDevice

    private action: string
    
    private listener: any | undefined

    private static auxIdMapping: number[] = [ 
        // Inputs
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
        // Black
        0,
        // Color Bars
        1000,
        // Color 1 + 2
        2001, 2002,
        // MP1, MP1 Key
        3010, 3011,
        // MP2, MP2 Key
        3020, 3021,
        // MP3, MP3 Key
        3030, 3031,
        // MP4, MP4 Key
        3040, 3041,
        // SuperSource 1 + 2
        6000, 6001,
        // CF 1 .. 4
        7001, 7002, 7003, 7004,
        // ME1 PGM, PRV
        10010, 10011,
        // ME2 PGM, PRV
        10020, 10021,
        // ME3 PGM, PRV
        10030, 10031,
        // ME4 PGM, PRV
        10040, 10041
        ]

    constructor(device: AtemDevice, action: string, inputChanged: (id: number) => void, nameChanged: (name: string) => void) {
        this.device = device
        this.action = action

        this.listener = (_state: AtemState, _paths: string[]) => {
            //const auxBus = this.getAuxBus()
            //if (auxBus && path === ('video.auxilliaries.' + auxBus)) {
                inputChanged(this.getInput())
            //}

            nameChanged(this.getOutputPortName())
        }
        this.device.on('stateChanged', this.listener)
    }

    private getAuxBus(): string | undefined {
        if (this.action.startsWith('aux')) {
            return this.action.substr(3)
        }

        return undefined
    }

    public getOutputPortName(): string {
        if (this.action.startsWith('aux')) {
            const port = this.getAuxBus()
            const aux = this.device.getParams().aux.find(a => a.id === port)
            if (aux) {
                return this.device.getConfig().name + ' ' + aux.label
            }
        }

        return this.device.getConfig().name + ' ' + this.action
    }

    public setInput(id: number): boolean {
        if (this.action.startsWith('aux')) {
            const port = this.getAuxBus()
            if (id >= 0 && id < AtemVideohubAdapater.auxIdMapping.length) {
                this.device.trigger('aux', 1, { auxId: port, auxSrc: AtemVideohubAdapater.auxIdMapping[id], auxAction: 'latch' })
                return true
            }
        }
        return false
    }

    public getInput() {
        if (this.action.startsWith('aux')) {
            const port = this.getAuxBus()
            if (port) {
                const id = this.device.getAuxInput(port)
                if (id !== undefined) {
                    const index = AtemVideohubAdapater.auxIdMapping.indexOf(id)
                    if (index >= 0) {
                        return index
                    }
                }
            }
        }

        return -1
    }

    public destroy() {
        if (this.listener !== undefined) {
            this.device.removeListener('stateChanged', this.listener)
        }
    }
}

const INTERNAL_CHANGE_EVENT = 'internal'

export class VideohubMapper extends EventEmitter implements VideohubAdapterInterface {
    private config: ConfigManager
    private devices: DeviceManager

    private mappings: VideohubMapping[] = []

    private state: VideohubState

    public static Events = {
        MappingsChanged: 'mappingsChanged'
    }

    constructor (config: ConfigManager, devices: DeviceManager) {
        super()

        this.config = config
        this.devices = devices

        this.state = new VideohubState()
        this.state.config = {
            inputCount: 72,
            outputCount: 72,
            monitoringCount: 0,
            serialCount: 0
        }

        this.state.setName('PS Broadcast IO 32')
        this.state.setDeviceName('Blackmagic Enterprise Videohub')

        for (let index = 0; index < this.state.config.outputCount; index++) {
            const port = this.state.getOutput(index)
            port.lock = PortLockState.LockedByOther
            port.lockHolder = undefined
            port.name = 'Inactive'
            port.route = this.state.config.inputCount - 1

            this.mappings.push({
                config: {
                    output: index,
                    deviceId: '',
                    action: ''
                },
                device: undefined,
                active: false,
                port
            })
        }

        // Set INPUT names (all for ATEM)
        

        for (let index = 0; index < 40; index++) {
            this.state.getInput(index).name = 'Input ' + (index + 1)
        }

        this.state.getInput(40).name = 'Black'
        this.state.getInput(41).name = 'Color Bars'
        this.state.getInput(42).name = 'Color 1'
        this.state.getInput(43).name = 'Color 2'
        
        for (let index = 0; index < 4; index++) {
            this.state.getInput(44 + (index * 2)).name = "MP" + (index + 1)
            this.state.getInput(44 + (index * 2) + 1).name = "MP" + (index + 1) + ' Key'
        }
        
        this.state.getInput(52).name = 'SuperSource 1'
        this.state.getInput(53).name = 'SuperSource 2'
        
        for (let index = 0; index < 4; index++) {
            this.state.getInput(54 + index).name = "Cleanfeed " + (index + 1)
        }
        
        for (let index = 0; index < 4; index++) {
            this.state.getInput(58 + (index * 2)).name = "ME" + (index + 1) + ' Program'
            this.state.getInput(58 + (index * 2) + 1).name = "MP" + (index + 1) + ' Preview'
        }

        for (let index = 66; index < 72; index++) {
            this.state.getInput(index).name = 'Undefined'
        }
        
        this.config.on(ConfigManager.Events.Loaded, () => this.onConfigLoaded())

        if (config.isLoaded()) {
            this.onConfigLoaded()
        }

        this.devices.on(DeviceManager.Events.DevicesChanged, () => this.registerAllToDevices())
    }

    public setMapping(mapping: VideohubConfiguration): void {
        this.applyMapping(mapping)

        this.config.updateConfigSection(ConfigSections.Videohub, this.mappings.map(m => m.config))
        this.emit(VideohubMapper.Events.MappingsChanged)
    }

    public deleteMapping(id: number): void {
        this.applyMapping({
            output: id,
            deviceId: '',
            action: ''
        })

        this.config.updateConfigSection(ConfigSections.Videohub, this.mappings.map(m => m.config))
        this.emit(VideohubMapper.Events.MappingsChanged)
    }

    public getMappings(): VideohubConfiguration[] {
        return this.mappings.map(m => m.config)
    }

    private onConfigLoaded(): void {
        const settings = this.config.getConfigSection(ConfigSections.Videohub)
        for (const setting of settings) {
            const mapping = this.mappings.find(m => m.config.output === setting.output)
            if (mapping) {
                mapping.config.deviceId = setting.deviceId
                mapping.config.action = setting.action
            }
        }

        this.registerAllToDevices()
        this.emit(VideohubMapper.Events.MappingsChanged)
    }

    private applyMapping(mapping: VideohubConfiguration): void {
        if (mapping.output === undefined) return

        // get original mapping
        const stored = this.mappings.find(m => m.config.output === mapping.output)
        if (!stored) return

        stored.config.deviceId = mapping.deviceId
        stored.config.action = mapping.action

        this.registerToDevices(stored)
    }

    private registerAllToDevices(): void {
        for (const mapping of this.mappings) {
            this.registerToDevices(mapping)
        }
    }

    private registerToDevices(mapping: VideohubMapping): void {
        if (mapping.device) {
            mapping.device.destroy()
            mapping.device = undefined
        }

        if (mapping.config.deviceId) {
            const device = this.devices.getDevice(mapping.config.deviceId)

            if (device && device.getConfig().type === 'ATEM') {
                mapping.device = new AtemVideohubAdapater(
                    device as AtemDevice,
                    mapping.config.action, 
                    (input) => this.setRouting(mapping.port.id, input),
                    (name) => this.setName(mapping.port.id, name))
            }
        }
        
        mapping.active = mapping.device !== undefined

        if (mapping.active && mapping.device) {
            if (mapping.port.lockHolder === undefined) {
                this.setLocked(mapping.port.id, false)
            }
            this.setName(mapping.port.id, mapping.device.getOutputPortName())
            this.setRouting(mapping.port.id, mapping.device.getInput())
        } else {
            this.setLocked(mapping.port.id, true, undefined)
            this.setName(mapping.port.id, 'Inactive')
            this.setRouting(mapping.port.id, this.state.config.inputCount - 1)
        }
    }

    private setLocked(id: number, locked: boolean, lockHolder?: string): void {
        const output = this.state.getOutput(id)
        const currentLocked = output.lock !== PortLockState.Unlocked
        if (currentLocked !== locked) {
            output.lock = locked ? PortLockState.LockedByOther : PortLockState.Unlocked
            output.lockHolder = lockHolder

            const cmd = new VideohubCommand(CommandTypes.VideoOutputLocks)
            cmd.addParameter(output.id, locked ? 'L' : 'U')
            this.emit(INTERNAL_CHANGE_EVENT, cmd)
        }
    }

    private setName(id: number, name: string) {
        const output = this.state.getOutput(id)
        if (output.name !== name) {
            output.name = name

            const cmd = new VideohubCommand(CommandTypes.OutputLabels)
            cmd.addParameter(output.id, name)
            this.emit(INTERNAL_CHANGE_EVENT, cmd)
        }
    }

    private setRouting(id: number, input: number) {
        if (input < 0 || input >= this.state.config.inputCount) {
            input = this.state.config.inputCount - 1
        }

        const output = this.state.getOutput(id)
        if (output.route !== input) {
            output.route = input

            const cmd = new VideohubCommand(CommandTypes.VideoOutputRouting)
            cmd.addParameter(output.id, input)
            this.emit(INTERNAL_CHANGE_EVENT, cmd)
        }
    }

    //
    // Videohub Adapter

    
    getConfig(): VideohubConfigInterface { return this.state.config }
    getName(): string { return this.state.name || '' }
    getDeviceName(): string { return this.state.deviceName || '' }
    getInput(id: number): InputInterface { return this.state.getInput(id) }
    getMonitoringOutput(id: number): OutputInterface { return this.state.getMonitoringOutput(id) }
    getSerial(id: number): SerialInterface { return this.state.getSerial(id) }
    getOutput(id: number): OutputInterface { return this.state.getOutput(id) }

    process(cmd: VideohubCommand, clientId: string): boolean {
        // Only change processing is done here
        if (cmd.parameters.length === 0) return false

        if (cmd.header === CommandTypes.VideoOutputRouting) {
            for (const param of cmd.parameters) {
                if (param && param.keyNumber() !== undefined && param.valueNumber()  !== undefined) {
                    const output = this.mappings.find(m => m.config.output === param.keyNumber())
                    if (!output || !output.active || !output.device) return false

                    // Check lock
                    if (output.port.lock !== PortLockState.Unlocked && output.port.lockHolder !== clientId) return false
                    
                    const input = param.valueNumber()
                    return input !== undefined && output.device.setInput(input)
                } else {
                    return false
                }
            }
        } else if (cmd.header === CommandTypes.VideoOutputLocks) {
            for (const param of cmd.parameters) {
                if (param && param.keyNumber()) {
                    const output = this.mappings.find(m => m.config.output === param.keyNumber())
                    if (!output || !output.active) return false

                    if (output.port.lock === PortLockState.Unlocked && param.value === PortLockAction.Lock) {
                        this.setLocked(output.port.id, true, clientId)
                        return true
                    }

                    if (output.port.lock !== PortLockState.Unlocked && 
                        ((param.value === PortLockAction.Unlock && output.port.lockHolder === clientId) 
                        || (param.value === PortLockAction.ForceUnlock))) {
                        this.setLocked(output.port.id, false, undefined)
                        return true
                    }
                }
            }
        }

        return false
    }

    onChanges(handler: (cmd: VideohubCommand) => void): void {
        this.on(INTERNAL_CHANGE_EVENT, handler)
    }
}