const {InstanceBase, Regex, runEntrypoint, InstanceStatus, combineRgb} = require('@companion-module/base')
const UpgradeScripts = require('./upgrades')
const UpdateActions = require('./actions')
const UpdateFeedbacks = require('./feedbacks')
const WebSocket = require('ws')
const ProjectUpdate = require("./receive/project_update");
const Project = require("./project");
const PadNameUpdate = require("./receive/pad_name_update");
const PadStatusUpdate = require("./receive/pad_status_update");
const CurrentPageUpdate = require("./receive/current_page_update");
const CurrentPageRequest = require("./receive/current_page_request");
const uuid = require('uuid');
const presets = require('./presets');

class ModuleInstance extends InstanceBase {
    isInitialized = false
    // language=RegExp
    wsRegex = '^wss?:\\/\\/([\\da-z.-]+)(:\\d{1,5})?(?:\\/(.*))?$'

    messageHandlers = {
        'project-current': new ProjectUpdate(),
        'pad-name-changed': new PadNameUpdate(),
        'pad-status-changed': new PadStatusUpdate(),
        'current-page-changed': new CurrentPageUpdate(),
        'current-page-request': new CurrentPageRequest()
    };

    currentProject = new Project({});

    constructor(internal) {
        super(internal)
    }

    async init(config) {
        this.config = config

        this.initWebSocket()
        this.isInitialized = true

        this.updateActions()
        await this.updateFeedbacks()

        this.setPresetDefinitions(presets());
    }

    // When module gets deleted
    async destroy() {
        this.isInitialized = false
        if (this.reconnect_timer) {
            clearTimeout(this.reconnect_timer)
            this.reconnect_timer = null
        }
        if (this.ws) {
            this.ws.close(1000)
            delete this.ws
        }
    }

    async configUpdated(config) {
        this.config = config
        this.initWebSocket()
    }

    // Return config fields for web config
    getConfigFields() {
        return [
            {
                type: 'textinput',
                id: 'host',
                label: 'PlayWall IP',
                default: '127.0.0.1',
                width: 8,
                regex: Regex.IP,
            },
            {
                type: 'textinput',
                id: 'port',
                label: 'PlayWall Port',
                default: '9876',
                width: 4,
                regex: Regex.PORT,
            },
            {
                type: 'checkbox',
                id: 'debug_messages',
                label: 'Debug messages',
                tooltip: 'Log incomming and outcomming messages',
                width: 6,
            },
            {
                type: 'checkbox',
                id: 'reconnect',
                label: 'Reconnect',
                tooltip: 'Reconnect on WebSocket error (after 5 secs)',
                width: 6,
                default: true,
            },
        ]
    }

    updateActions() {
        UpdateActions(this)
    }

    async updateFeedbacks() {
        await UpdateFeedbacks(this)
    }

    // Websocket handling

    maybeReconnect() {
        if (this.isInitialized && this.config.reconnect) {
            if (this.reconnect_timer) {
                clearTimeout(this.reconnect_timer)
            }
            this.reconnect_timer = setTimeout(() => {
                this.initWebSocket()
            }, 1000)
        }
    }

    initWebSocket() {
        if (this.reconnect_timer) {
            clearTimeout(this.reconnect_timer)
            this.reconnect_timer = null
        }

        if (this.config.host == null || this.config.port == null) {
            this.log('debug', `PlayWall host '${this.config.host}' or port '${this.config.port}' is invalid`);
            this.updateStatus(InstanceStatus.BadConfig, `PlayWall host '${this.config.host}' or port '${this.config.port}' is invalid`)
            return
        }
        const url = `ws://${this.config.host}:${this.config.port}/api`
        if (!url || url.match(new RegExp(this.wsRegex)) === null) {
            this.updateStatus(InstanceStatus.BadConfig, `WS URL is not defined or invalid`)
            return
        }

        this.updateStatus(InstanceStatus.Connecting)

        if (this.ws) {
            this.ws.close(1000)
            delete this.ws
        }

        this.ws = new WebSocket(url)

        this.ws.on('open', () => {
            this.updateStatus(InstanceStatus.Ok)
            this.log('debug', `Connection opened`)

            this.sendToWebSocket('project-current', {});
        })
        this.ws.on('close', (code) => {
            this.log('debug', `Connection closed with code ${code}`)
            this.updateStatus(InstanceStatus.Disconnected, `Connection closed with code ${code}`)
            this.maybeReconnect();
        })

        this.ws.on('message', this.messageReceivedFromWebSocket.bind(this))

        this.ws.on('error', (data) => {
            this.log('error', `WebSocket error: ${data}`)
        })
    }

    sendToWebSocket(type, payload) {
        this.ws.send(JSON.stringify({
            'type': type,
            'messageId': uuid.v4(),
            'payload': payload
        }));
    }

    messageReceivedFromWebSocket(data) {
        if (this.config.debug_messages) {
            this.log('debug', `Message received: ${data}`)
        }

        const message = JSON.parse(data)
        if (message.type != null) {
            if (this.messageHandlers[message.type] != null) {
                this.messageHandlers[message.type].handleMessage(this, message)
            } else {
                this.log('debug', `Cannot handle incoming message of type ${message.type}`)
            }
        } else if (message.updateType != null) {
            if (this.messageHandlers[message.updateType] != null) {
                this.messageHandlers[message.updateType].handleMessage(this, message)
            } else {
                this.log('debug', `Cannot handle incoming message of updateType ${message.updateType}`)
            }
        } else {
            this.log('debug', `Cannot handle incoming message ${data}`)
        }
    }

    updateVariables() {
        this.setVariableDefinitions([...Array(this.currentProject.getPadCount()).keys()].map(index => {
            return {variableId: `pad-${index}`, name: `Pad Index ${index + 1}`}
        }))
    }

    updateVariablesForCurrentPage() {
        let pads = this.currentProject.getAllPadsOfCurrentPage();
        for (let i in pads) {
            let pad = pads[i];
            this.log('debug', `Update variable 'pad-${pad.position}'`);
            this.setVariableValues({[`pad-${pad.position}`]: pad.name});
        }
    }
}

runEntrypoint(ModuleInstance, UpgradeScripts)