import * as express from 'express'
import * as _ from 'lodash'
import { createServer } from 'http'
import { EventEmitter } from 'events'
import * as path from 'path'

import { Server, Socket } from 'socket.io'
import { BoardInterface, BoardStatus } from '../common/BoardTypes'
import { LogEvent, LogLevel } from '../common/LogTypes'

import { ConfigManager } from './ConfigManager'
import { LogManager } from './LogManager'

import { NetworkConfiguration } from '../common/SystemTypes'

import { GetHardwareService, GpioBoard, HwOutput } from './system/HardwareService'
import { DeviceManager } from './devices/DeviceManager'
import { AtemDeviceFactory } from './devices/AtemDevice'
import { GetNetworkService } from './system/NetworkService'
//import { OledService } from './system/OledService'
import { MappingManager } from './MappingManager'
import { MappingDirection, MappingActionConfiguration, VideohubConfiguration } from '../common/MappingTypes'
import { VideohubServer } from '../videohub'
import { VideohubMapper } from './VideohubMapper'
import { CompanionDeviceFactory } from './devices/CompanionDevice'
import { TallyArbiterDeviceFactory } from './devices/TallyArbiterDevice'
import { AtemKeySync } from './AtemKeySync'
import { spawn } from 'child_process'

const logger = new LogManager()

const config = new ConfigManager(logger)
config.load()

const app = express()
const server = createServer(app)
const io: Server = new Server(server, {
    cors: { origin: "http://localhost:3000", credentials: true }
})

const nwService = GetNetworkService(logger)

const BOARD_COUNT = 4

function createBoard (id: string, name: string, device?: GpioBoard) {
    const board: BoardInterface = { id, name, status: device === undefined ? BoardStatus.Unknown : BoardStatus.Ok, inputs: [ ], outputs: [ ], enabled: false }

    if (device) {
        for (let i = 0; i < device.inputs.length; i++) {
            board.inputs.push({ id: (id + '.i' + i), value: device.inputs[i].getValue() ? 1 : 0 })
        }

        for (let i = 0; i < device.outputs.length; i++) {
            board.outputs.push({ id: (id + '.o' + i), value: device.outputs[i].getValue() ? 1 : 0 })
        }
    }

    return board
}

const hw = GetHardwareService()
const internalBoards = hw.GetGpioBoards()

export class GpioBoardHandler extends EventEmitter {
    private board: BoardInterface
    private device: GpioBoard | undefined
    constructor(index: number, device: GpioBoard | undefined) {
        super()
        this.device = device
        this.board = createBoard(`local${index}`, `PSDIO8 (local ${index})`, device)
        this.board.enabled = this.device !== undefined

        if (this.device !== undefined) {
            this.device.on('change', (item: string, value: boolean) => {
                const changeType = item.split('.')[0]
                const changeId = parseInt(item.split('.')[1], 10)
                if (changeType === 'in') {
                    const input = this.board.inputs[changeId]
                    if (input) {
                        input.value = value ? 1 : 0
                        this.emit('change', input.id, input.value)
                    }
                } else if (changeType === 'out') {
                    const output = this.board.outputs[changeId]
                    if (output) {
                        output.value = value ? 1 : 0
                        this.emit('change', output.id, output.value)
                    }
                }
            })
        }
    }

    public get() : BoardInterface {
        return this.board
    }

    public setOutput(portId: string, value: number) {
        this.callOutputHandler(portId, h => h.setValue(value === 1))
    }

    public setTest(portId: string, value: number) {
        const output = this.board.outputs.find(o => o.id === portId)
        if (output) {
            this.callOutputHandler(portId, h => h.setTest(value === 1))
        } else {
            const input = this.board.inputs.find(i => i.id === portId)
            if (input) {
                this.emit('change', input.id, value)
            }
        }
    }

    private callOutputHandler(portId: string, action: (handler: HwOutput) => void) {
        if (this.device) {
            const output = this.board.outputs.find(o => o.id === portId)
            if (output) {
                const outputIndex = this.board.outputs.indexOf(output)
                const handler = this.device.outputs[outputIndex]
                if (handler) {
                    action(handler)
                }
            }
        }
    }
}

const boards: GpioBoardHandler[] = []
const boardModels: BoardInterface[] = []
for (let i = 0; i < BOARD_COUNT; i++) {
    const handler = new GpioBoardHandler(i, internalBoards.find(v => v.id === i))
    handler.on('change', (id, value) => {
        console.log('ioChange ' + id + ' -> ' + value)
        io.emit('ioChange', { data: { id, value } })
    })
    boards.push(handler)
    boardModels.push(handler.get())
}

const deviceManager = new DeviceManager(
    config, 
    logger,
    [
        new AtemDeviceFactory(logger),
        new CompanionDeviceFactory(logger),
        new TallyArbiterDeviceFactory(logger)
    ])

const mappingManager = new MappingManager(
    config,
    deviceManager,
    logger,
    boards)

const videohubAdapter = new VideohubMapper(config, deviceManager)
const atemKeySync = new AtemKeySync(deviceManager, config, logger)

//const oled = new OledService(logger, nwService)
//oled.init()

const serviceIndex = function(_req: express.Request, res: express.Response) {
    res.sendFile(path.join(__dirname, '../../../webapp/dist/index.html'))
}
app.get('/', serviceIndex);
app.get('/index.html', serviceIndex)

app.use('/assets', express.static(path.join(__dirname, '../../../webapp/dist/assets')))

function convert2Digit(num: number): string {
    return (num < 10) ? ("0" + num) : ("" + num);
}

function getTimestamp(): string {
    var d       = new Date();
    var year    = d.getFullYear().toString();
    var month   = convert2Digit(d.getMonth() + 1);
    var day     = convert2Digit(d.getDate());
    var hrs     = convert2Digit(d.getHours());
    var mins    = convert2Digit(d.getMinutes());
    var out     = year + month + day + "-" + hrs + mins;
    return out;
}

app.get('/config', function (_req: express.Request, res: express.Response) {
    res.writeHead(
        200,
        { 
            'Content-Type': 'application/json', 
            'Content-Disposition': 'attachment; filename="psBroadcastIo32_config' + '_' + getTimestamp() + '.config"'
        });
    res.end(JSON.stringify(config.getConfig()));
})

app.get('/backup/:backupId', function (req: express.Request, res: express.Response) {
    const backups = config.getBackups()
    if (req.params.backupId && backups.includes(req.params.backupId)) {
        res.writeHead(
            200,
            { 
                'Content-Type': 'application/json', 
                'Content-Disposition': 'attachment; filename="' + req.params.backupId + '.config"'
            });
        res.end(config.getBackupData(req.params.backupId));
    } else {
        res.writeHead(404)
        res.end()
    }
})

io.engine.on('connection_error', (err: any) => {
    console.log(err)
})

io.on('connection', function (socket: Socket) {
    console.log('a user connected')

    socket.emit('boards', { data: boardModels })
    socket.emit('devices', { data: deviceManager.getDevices() })
    socket.emit('gpiMappings', { data: mappingManager.getMappings().filter(m => m.direction === MappingDirection.Input) })
    socket.emit('gpoMappings', { data: mappingManager.getMappings().filter(m => m.direction === MappingDirection.Output) })
    socket.emit('logEntries', { data: logger.getAll() })
    socket.emit('videohubMappings', { data: videohubAdapter.getMappings() })
    socket.emit('keySync', { data: atemKeySync.getSettings() })
    socket.emit('networkConfig', { data: nwService.GetConfiguration() })
    socket.emit('backups', { data: config.getBackups() })
    setTimeout(async () => {
        socket.emit('remoteConnection', { data: { enabled: await nwService.GetWireguardEnabledAsync(), connected: await nwService.GetWireguardStatusAsync() }})
    }, 10)

    socket.on('boards', () => socket.emit('boards', { data: boardModels }))
    
    socket.on('setTest', (data: any) => {
        console.log('setTest')
        if (!data.id) return
        const parts = data.id.split('.')
        if (parts.length >= 2) {
            const board = boards.find(b => b.get().id === parts[0])
            if (board) {
                board.setTest(data.id, data.value)
            }
        }
    })

    socket.on('saveDevice', (data: any) => {
        console.log('saveDevice')
        deviceManager.createDevice(data)
    })

    socket.on('deleteDevice', (id: string) => {
        console.log('deleteDevice')
        deviceManager.deleteDevice(id)
    })

    socket.on('setMapping', (data: any) => {
        console.log('setMapping')
        mappingManager.setMapping(data)
    })

    socket.on('addMappingAction', (data: any) => {
        console.log('addMappingAction')
        mappingManager.addAction(data.portId, data.action as MappingActionConfiguration)
    })

    socket.on('deleteMappingAction', (data: any) => {
        console.log('deleteMappingAction')
        mappingManager.deleteAction(data.portId, data.actionId)
    })

    socket.on('setVideohubMapping', (data: any) => {
        console.log('setVideohubMapping')
        videohubAdapter.setMapping(data as VideohubConfiguration)
    })

    socket.on('deleteVideohubMapping', (data: any) => {
        console.log('deleteVideohubMapping')
        videohubAdapter.deleteMapping(data.id as number)
    })

    socket.on('setKeySync', (data: any) => {
        console.log('setKeySync')
        atemKeySync.setSetting(data)
    })

    socket.on('deleteKeySync', (data: any) => {
        console.log('deleteKeySync')
        atemKeySync.deleteSetting(data.id as string)
    })

    socket.on('setNetworkConfig', (data: any) => {
        console.log('getNetworkConfig')
        nwService.UpdateConfiguration(data as NetworkConfiguration)
        socket.emit('networkConfig', { data: nwService.GetConfiguration() })
    })

    socket.on('setDateTime', (data: any) => {
        console.log('setDateTime: ' + data)
        nwService.SetDateTime(data as string)
    })

    socket.on('updateCompanion', (data: string) => {
        if (!/^v?\d+\.\d+.\d+$/.test(data)) return

        const outBuffer: string[] = []
        const sendBuffer = () => socket.emit('companionUpdateResult', { data: outBuffer.join('\n') })

        outBuffer.push(`Starting update to ${data}...`)
        sendBuffer()        
        const updateProc = spawn('/home/pi/upgrade_companion.sh', [data])
        
        updateProc.stdout.on('data', (line) => {
            outBuffer.push(line.toString())
            sendBuffer()
        })

        updateProc.stderr.on('data', (line) => {
            outBuffer.push(line.toString())
            sendBuffer()
        })

        updateProc.on('exit', function (code) {
            if (code == 0) {
                logger.log(`Updated companion to ${data}`, LogLevel.Info, { type: 'SYSTEM', id: '' })
                outBuffer.push('Update done.')
                sendBuffer()
            } else {
                logger.log(`Error updating companion: ${outBuffer.at(outBuffer.length - 1)}`, LogLevel.Error, { type: 'SYSTEM', id: '' })
                outBuffer.push('Error updating companion.')
                sendBuffer()
            }
        })
    })

    socket.on('getRemoteConnection', async (_data: any) => {
        socket.emit('remoteConnection', { data: { enabled: await nwService.GetWireguardEnabledAsync(), connected: nwService.GetWireguardStatusAsync() }})
    })

    socket.on('setRemoteConnection', (data: any) => {
        nwService.EnableWireguard(data == true)

        setTimeout(async () => {
            socket.emit('remoteConnection', { data: { enabled: await nwService.GetWireguardEnabledAsync(), connected: nwService.GetWireguardStatusAsync() }})
        }, 500)
    })

    socket.on('getBackups', (_data: any) => {
        socket.emit('backups', { data: config.getBackups() })
    })

    socket.on('backup', (data: any) => {
        console.log('backup: ' + data)
        config.backup(data as string)
    })

    socket.on('restore', (data: any) => {
        console.log('restore: ' + data)
        config.restore(data as string)
    })

    socket.on('deleteBackup', (data: any) => {
        console.log('deleteBackup: ' + data)
        config.deleteBackup(data as string)
    })
})

deviceManager.on(DeviceManager.Events.DevicesChanged, _.throttle(() => {
    io.emit('devices', { data: deviceManager.getDevices() })
}, 250))
deviceManager.on(DeviceManager.Events.DeviceStatusChanged, data => {
    console.log('deviceStatusChanged')
    io.emit('deviceStatusChanged', data)
})

mappingManager.on(MappingManager.Events.MappingsChanged, _.throttle(() => {
    io.emit('gpiMappings', { data: mappingManager.getMappings().filter(m => m.direction === MappingDirection.Input) })
    io.emit('gpoMappings', { data: mappingManager.getMappings().filter(m => m.direction === MappingDirection.Output) })
}, 250))

videohubAdapter.on(VideohubMapper.Events.MappingsChanged, _.throttle(() => {
    io.emit('videohubMappings', { data: videohubAdapter.getMappings() })
}, 250))

atemKeySync.on(AtemKeySync.Events.SettingsChanged, _.throttle(() => {
    io.emit('keySync', { data: atemKeySync.getSettings() })
}, 250))

logger.on(LogManager.Events.Log, (entry: LogEvent) => {
    io.emit('logEntry', { data: entry })
})

config.on(ConfigManager.Events.BackupListChanged, () => {
    io.emit('backups', { data: config.getBackups() })
})

server.listen(80, function () {
    console.log('listening on *:80')
})

//const oled = new OledService()
//console.log(GetNetworkService().GetIpAddress())

const videohubServer = new VideohubServer(videohubAdapter)
videohubServer.start()

hw.SetRunLed(true)
