import { execSync } from 'child_process'
import * as os from 'os'
import { Gpio, BinaryValue } from 'onoff'
import { EventEmitter } from 'events'

export interface HardwareService {
    GetGpioBoards (): GpioBoard[]
    SetRunLed (v: boolean): void
    GetTemperature (): number
}

export class HwInput extends EventEmitter {

    private pin: number
    private gpio: Gpio
    private value: boolean | undefined

    constructor(pin: number) {
        super()
        this.pin = pin
        this.gpio = new Gpio(pin, 'in', 'both')

        this.gpio.watch((err, value) => {
            if (!err) {
                this.set(value)
            }
        })
        //setInterval(() => this.gpio.read((_err, value) => this.set(value)), 100)

        this.gpio.read((_err, value) => this.set(value))
    }

    private set(value: BinaryValue) {
        const b = value === 1
        if (this.value !== b) {
            this.value = b
            this.emit('change', this.value)
        }
    }

    public getValue(): boolean {
        return this.value || false
    }

    public getPin(): number {
        return this.pin
    }
}

export class HwOutput extends EventEmitter {

    private pin: number
    private gpio: Gpio
    private value: boolean | undefined
    private test: boolean = false

    constructor(pin: number) {
        super()
        this.pin = pin
        this.gpio = new Gpio(pin, 'out', 'both')

        this.gpio.watch((err, value) => {
            if (!err) {
                this.set(value)
            }
        })
        //setInterval(() => this.gpio.read((_err, value) => this.set(value)), 100)

        this.gpio.read((_err, value) => this.set(value))
    }

    private set(value: BinaryValue) {
        const b = value === 1
        if (this.value !== b) {
            this.value = b
            this.emit('change', this.getValue())
        }
    }

    public setValue(value: boolean) {
        return this.gpio.write(value ? 1 : 0).then(() => {
            this.set(value ? 1 : 0)
        }, err => console.log(err))
    }

    public setTest(value: boolean) {
        if (this.test === value) return Promise.resolve()

        this.test = value
        return this.gpio.write(this.getValue() ? 1 : 0).then(() => {
            this.emit('change', this.getValue())
        }, err => console.log(err))
    }

    public getValue(): boolean {
        return this.test || this.value || false
    }

    public getPin(): number {
        return this.pin
    }
}

export class GpioBoard extends EventEmitter {
    public inputs: HwInput[] = []
    public outputs: HwOutput[] = []
    public id: number
    constructor(id: number, base: number, count: number) {
        super()
        this.id = id

        const inputCount: number = count / 2
        for (let i = 0; i < inputCount; i++) {
            const item = new HwInput(base + i)
            item.on('change', v => {
                this.emit('change', 'in.' + i, v)
            })
            this.inputs.push(item)
        }

        const outputCount = count - inputCount
        for (let i = 0; i < outputCount; i++) {
            const item = new HwOutput(base + inputCount + i)
            item.on('change', v => {
                this.emit('change', 'out.' + i, v)
            })
            this.outputs.push(item)
        }
    }
}

let hwServiceSingleton: HardwareService | null = null

export function GetHardwareService (): HardwareService {
    if (hwServiceSingleton === null) {
        hwServiceSingleton = (os.platform() === 'linux') ? new RpiHardwareService() : new EmulatorHardwareService()
    }

    return hwServiceSingleton
}

const GPIO_RUN_LED: number = 7

class RpiHardwareService implements HardwareService {
    private boardList: GpioBoard[] = []

    private runLed: HwOutput

    constructor() {
        const runLedGpioNo = parseInt(execSync('cat /sys/kernel/debug/gpio | grep GPIO' + GPIO_RUN_LED + ' | sed \'s/gpio-//\' | awk \'{print $1 }\'', { encoding: 'utf8' }))
        this.runLed = new HwOutput(runLedGpioNo)
    }

    GetGpioBoards (): GpioBoard[] {
        if (this.boardList.length === 0) {
            // cache boards (boards don't change during run)

            const reGpioChips = /gpiochip\d+/
            const reAddress = /i2c-\d\/\d-00(2[0-7])$/m
            const boardIds = execSync('ls -1 /sys/class/gpio', { encoding: 'utf8' }).split('\n').filter(v => reGpioChips.test(v))

            for (const id of boardIds) {
                const label = execSync(`cat /sys/class/gpio/${id}/label`, { encoding: 'utf8' }).trim()

                if (label === 'mcp23017') {
                    const device = execSync(`readlink -f /sys/class/gpio/${id}/device`, { encoding: 'utf8' }).trim()
                    const addressMatch = reAddress.exec(device)
                    if (addressMatch) {
                        const address = parseInt(addressMatch[1], 16)
                        const index = address - 0x20
                        const base = parseInt(execSync(`cat /sys/class/gpio/${id}/base`, { encoding: 'utf8' }), 10)
                        const count = parseInt(execSync(`cat /sys/class/gpio/${id}/ngpio`, { encoding: 'utf8' }), 10)
                        this.boardList.push(new GpioBoard(index, base, count))
                    }
                }
            }
        }

        this.boardList = this.boardList.sort((a, b) => a.id === b.id ? 0 : (a.id > b.id ? 1 : -1))
        return this.boardList
    }

    SetRunLed (v: boolean): void {
        this.runLed.setValue(v)
    }

    GetTemperature (): number {
        const value = parseFloat(execSync('vcgencmd measure_temp', { encoding: 'utf8' }))
        return value
    }
}

class EmulatorHardwareService implements HardwareService {
    private boardList: GpioBoard[] = []

    GetGpioBoards (): GpioBoard[] {
        return this.boardList
    }

    SetRunLed (v: boolean): void {
        console.log(`Setting run LED to ${v ? '1' : '0'}`)
    }

    GetTemperature (): number {
        return 40.2
    }
}
