import { StartupTaskState, WindowsStoreAutoLaunch } from '@kydronepilot/electron-winstore-auto-launch'
import Axios from 'axios'
import { spawn } from 'child_process'
import { app, powerMonitor, Rectangle, screen, systemPreferences } from 'electron'
import { ipcMain as ipc } from 'electron-better-ipc'
import electronLog from 'electron-log'
import { cloneDeep } from 'lodash'
import { menubar } from 'menubar'
import moment from 'moment'
import net from 'net'
import * as path from 'path'
import * as url from 'url'

import { DownloadedThumbnailIpc, DownloadThumbnailIpcRequest, toBoolean } from '../shared'
import { RootSatelliteConfig } from '../shared/config_types'
import {
    DOWNLOAD_THUMBNAIL_CHANNEL,
    GET_AUTO_UPDATE,
    GET_CURRENT_VIEW_CHANNEL,
    GET_FIRST_RUN,
    GET_SATELLITE_CONFIG_CHANNEL,
    GET_START_ON_LOGIN,
    GET_VIEW_DOWNLOAD_TIME,
    OPEN_WINDOWS_ICON_SETTINGS,
    QUIT_APPLICATION_CHANNEL,
    RELOAD_VIEW,
    SET_AUTO_UPDATE,
    SET_FIRST_RUN,
    SET_START_ON_LOGIN,
    SET_WALLPAPER_CHANNEL,
    VISIBILITY_CHANGE_ALERT_CHANNEL
} from '../shared/IpcDefinitions'
import { AppConfigStore } from './app_config_store'
import { BUILD_TYPE } from './consts'
import { resolveDns } from './dns_handler'
import { SatelliteConfigStore } from './satellite_config_store'
import { Initiator } from './update_lock'
import { setWindowVisibility, startUpdateChecking } from './updater'
import { formatAxiosError } from './utils'
import { latestViewDownloadTimes, WallpaperManager } from './wallpaper_manager'

const HEARTBEAT_INTERVAL = 60000
let heartbeatHandle: number

// let win: BrowserWindow | null

const log = electronLog.scope('main')

Axios.defaults.adapter = require('axios/lib/adapters/http')

Axios.defaults.headers['User-Agent'] = `SpaceEye/${APP_VERSION} (${BUILD_TYPE})`

// Send HEAD requests to each DNS IP, using the IP first to respond
Axios.interceptors.request.use(async config => {
    const newConfig = cloneDeep(config)
    const requestUrl = new url.URL(config.url!)
    if (net.isIP(requestUrl.hostname)) {
        return config
    }
    const ip = await resolveDns(requestUrl, config.cancelToken)
    newConfig.headers = config.headers ?? {}
    newConfig.headers.Host = requestUrl.hostname
    requestUrl.hostname = ip
    newConfig.url = requestUrl.toString()
    return newConfig
})

startUpdateChecking()

/**
 * Heartbeat function which runs every `HEARTBEAT_INTERVAL` seconds to perform
 * any necessary tasks.
 */
async function heartbeat() {
    log.debug('Heartbeat triggered')
    if (powerMonitor.getSystemIdleState(100) === 'locked') {
        log.debug('Not updating from heartbeat because screen locked')
        return
    }
    await WallpaperManager.update(Initiator.heartbeatFunction)
}

const index = url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
})

// Initial window positioning (subject to change on Windows depending on taskbar location)
const windowPosition = process.platform === 'darwin' ? 'trayRight' : 'trayBottomRight'

const ICONS_DIR = path.join(__dirname, 'icons')
const MAC_TOOLBAR_ICON_PATH = path.join(ICONS_DIR, 'MacToolbarTemplate.png')
const WINDOWS_TOOLBAR_ICON_PATH = path.join(ICONS_DIR, 'windows_toolbar.ico')
const WINDOWS_TOOLBAR_LIGHT_ICON_PATH = path.join(ICONS_DIR, 'windows_toolbar_light.ico')

// Use ICO file for Windows
let toolbarIconPath =
    process.platform === 'win32' ? WINDOWS_TOOLBAR_ICON_PATH : MAC_TOOLBAR_ICON_PATH

/**
 * Set the toolbar icon to the path if not already set.
 *
 * @param newIconPath - Path to the new icon
 */
function setToolbarIcon(newIconPath: string) {
    if (toolbarIconPath !== newIconPath) {
        log.info('Updating toolbar icon path to:', newIconPath)
        toolbarIconPath = newIconPath
        mb.tray.setImage(toolbarIconPath)
    }
}

/**
 * If on Window, check the taskbar theme and update the taskbar icon accordingly.
 */
function updateToolbarIcon() {
    if (process.platform !== 'win32') {
        return
    }
    // Get the registry value for whether the taskbar is set to light theme
    const command = spawn('reg.exe', [
        'query',
        'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize',
        '/v',
        'SystemUsesLightTheme'
    ])
    let stdout = ''
    command.stdout.setEncoding('utf8')
    command.stderr.setEncoding('utf8')

    // Some debugging
    command.stderr.on('data', data => {
        log.warn('Windows taskbar update stderr:', data)
    })
    command.on('exit', code => {
        log.debug('Windows taskbar update cmd exited with:', code)
    })
    // Accumulate stdout text
    command.stdout.on('data', data => {
        stdout += data
    })
    command.stdout.on('close', () => {
        // Regex the key's value
        const res = stdout.match(new RegExp('SystemUsesLightTheme\\s+REG_DWORD\\s+([^\\s]+)'))
        // Determine the icon path that should be used
        let newPath = WINDOWS_TOOLBAR_ICON_PATH
        if (res && res.length >= 2 && res[1] === '0x1') {
            newPath = WINDOWS_TOOLBAR_LIGHT_ICON_PATH
        }
        setToolbarIcon(newPath)
    })
}

global.mb = menubar({
    index,
    icon: toolbarIconPath,
    browserWindow: {
        width: 550,
        height: 640,
        darkTheme: true,
        frame: false,
        webPreferences: {
            nodeIntegration: true
        },
        backgroundColor: '#222222',
        resizable: false,
        movable: false
    },
    windowPosition,
    preloadWindow: true
})

/**
 * Different locations of the Windows taskbar, with respective window positions.
 */
enum WindowsTaskbarPosition {
    Right = 'trayBottomRight',
    Left = 'trayBottomLeft',
    Top = 'trayCenter',
    Bottom = 'trayBottomCenter'
}

/**
 * Get the position of the Windows taskbar based on the tray bounds.
 *
 * @param trayBounds - Current bounds of menubar tray
 * @returns Position of taskbar
 */
function getWindowsTaskbarLocation(trayBounds: Rectangle): WindowsTaskbarPosition {
    if (trayBounds.y === 0) {
        return WindowsTaskbarPosition.Top
    }
    if (trayBounds.x < 50) {
        return WindowsTaskbarPosition.Left
    }
    const currentScreen = screen.getDisplayMatching(trayBounds)
    if (trayBounds.y + trayBounds.height === currentScreen.bounds.height) {
        return WindowsTaskbarPosition.Bottom
    }
    return WindowsTaskbarPosition.Right
}

// No multi-instance checking on MAS builds (causes app to close immediately)
if (!process.mas) {
    // Only let one instance be opened
    if (!app.requestSingleInstanceLock()) {
        app.quit()
    } else {
        // If another instance is opened (and then closed), focus on this one
        app.on('second-instance', () => {
            mb.showWindow()
            if (mb.window) {
                mb.window.focus()
            }
        })
    }
}

/**
 * Alert the renderer that the window visibility has changed.
 *
 * @param visible Whether the window became visible or not visible
 */
function visibilityChangeAlert(visible: boolean) {
    ipc.callRenderer<boolean>(mb.window!, VISIBILITY_CHANGE_ALERT_CHANNEL, visible)
}

mb.on('after-create-window', () => {
    log.info('App window created')
    log.info('Production mode:', process.env.NODE_ENV === 'production')

    if (process.platform === 'darwin') {
        mb.window!.setWindowButtonVisibility(false)
    }

    if (process.env.NODE_ENV !== 'production') {
        process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1' // eslint-disable-line require-atomic-updates
        mb.window!.loadURL(`http://localhost:2003`)
    }

    if (process.env.NODE_ENV !== 'production') {
        // Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready
        mb.window!.webContents.once('dom-ready', () => {
            mb.window!.webContents.openDevTools({ mode: 'detach' })
        })
    }

    mb.on('hide', () => {
        visibilityChangeAlert(false)
        setWindowVisibility(false)
        updateToolbarIcon()
    })

    mb.on('show', () => {
        visibilityChangeAlert(true)
        setWindowVisibility(true)
        updateToolbarIcon()

        // If on Windows, make sure window position matches toolbar location
        if (process.platform === 'win32') {
            const newPosition = getWindowsTaskbarLocation(mb.tray.getBounds())
            const currentPosition = mb.getOption('windowPosition')
            if (newPosition !== currentPosition) {
                mb.setOption('windowPosition', newPosition)
            }
        }
    })
})

// mb.on('ready', () => {})

mb.on('ready', () => {
    log.info('Menubar ready')
    // createWindow()
    heartbeatHandle = setInterval(heartbeat, HEARTBEAT_INTERVAL)

    // Check if toolbar icon should be updated (Windows only)
    updateToolbarIcon()

    // Display config change triggers update
    screen.on('display-added', async () => {
        log.debug('Display added')
        await WallpaperManager.update(Initiator.displayChangeWatcher)
    })

    screen.on('display-removed', async () => {
        log.debug('Display removed')
        await WallpaperManager.update(Initiator.displayChangeWatcher)
    })

    screen.on('display-metrics-changed', async () => {
        log.debug('Display metrics changed')
        await WallpaperManager.update(Initiator.displayChangeWatcher)
    })

    // Update when machine is unlocked/resumed
    // TODO: Need a new initiator
    if (process.platform === 'darwin' || process.platform === 'win32') {
        powerMonitor.on('unlock-screen', async () => {
            log.debug('Screen unlocked')
            await WallpaperManager.update(Initiator.displayChangeWatcher)
        })
    }

    if (process.platform === 'linux' || process.platform === 'win32') {
        powerMonitor.on('resume', async () => {
            log.debug('System resumed')
            await WallpaperManager.update(Initiator.displayChangeWatcher)
        })
    }

    // If first run, show user the window
    if (AppConfigStore.firstRun) {
        mb.showWindow()
        // If macOS, no onboarding, so first run must be reset here
        if (process.platform === 'darwin') {
            AppConfigStore.firstRun = false
        }
    }

    // Run an update on start
    WallpaperManager.update(Initiator.user)
})

/**
 * Configure whether the app should start on login.
 *
 * @param shouldStart - Whether the app should start on login
 */
async function configureStartOnLogin(shouldStart: boolean) {
    // Handle differently if windows store build
    if (process.windowsStore === true) {
        let task
        try {
            task = await WindowsStoreAutoLaunch.getStartupTask('SpaceEyeStartup')
        } catch (error) {
            log.error('Failed to get start on login MS task:', error)
            return
        }
        if (task !== undefined && task.state !== StartupTaskState.disabledByUser) {
            if (task.state === StartupTaskState.disabled && shouldStart) {
                task.requestEnableAsync((error, _) => {
                    if (error) {
                        log.error('Failed to enable start on login for MS build')
                    }
                })
            } else if (task.state === StartupTaskState.enabled && !shouldStart) {
                task.disable()
            }
        } else {
            log.warn('User has disabled start on login from task manager; unable to change')
        }
        return
    }
    const loginItemSettings = app.getLoginItemSettings()

    // If not set to what it should be, update it
    if (loginItemSettings.openAtLogin !== shouldStart) {
        app.setLoginItemSettings({ openAtLogin: shouldStart })
    }
}

// Default to not start on login
if (AppConfigStore.startOnLogin === undefined) {
    AppConfigStore.startOnLogin = false
}

// Ensure configured on startup
configureStartOnLogin(AppConfigStore.startOnLogin)

app.on('will-quit', () => {
    log.info('Application will quit')
    clearInterval(heartbeatHandle)
})

app.on('window-all-closed', () => {
    log.info('All windows have been closed')
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

ipc.answerRenderer(QUIT_APPLICATION_CHANNEL, () => {
    log.info('Quit request received')
    app.quit()
})

ipc.answerRenderer<void, RootSatelliteConfig | undefined>(
    GET_SATELLITE_CONFIG_CHANNEL,
    async () => {
        log.info('Get satellite config request received')
        const configStore = SatelliteConfigStore.Instance
        try {
            return await configStore.getConfig()
        } catch (error) {
            log.error('Failed to get new satellite config:', error)
            return undefined
        }
    }
)

ipc.answerRenderer<number, void>(SET_WALLPAPER_CHANNEL, async viewId => {
    log.info('Wallpaper set request received for view:', viewId)
    AppConfigStore.currentViewId = viewId
    await WallpaperManager.update(Initiator.user)
})

ipc.answerRenderer<number, void>(RELOAD_VIEW, async viewId => {
    log.info('Reload request received for view:', viewId)
    await WallpaperManager.update(Initiator.user, true)
})

ipc.answerRenderer<void, number | undefined>(GET_CURRENT_VIEW_CHANNEL, () => {
    log.info('Current view request received')
    return AppConfigStore.currentViewId
})

ipc.answerRenderer<DownloadThumbnailIpcRequest, DownloadedThumbnailIpc>(
    DOWNLOAD_THUMBNAIL_CHANNEL,
    async request => {
        log.info('Download thumbnail request received')
        let webResponse
        try {
            webResponse = await Axios.get(request.url, {
                responseType: 'arraybuffer',
                headers: { 'If-None-Match': request.etag ?? '' },
                validateStatus: status => (status >= 200 && status < 300) || status === 304
            })
        } catch (error) {
            log.error('Error while downloading thumbnail:', formatAxiosError(error))
            return {}
        }
        if (webResponse.status === 304) {
            return { isModified: false }
        }
        const b64Image = Buffer.from(webResponse.data, 'binary').toString('base64')
        const contentType = webResponse.headers['content-type'] ?? 'image/jpeg'
        let timeTaken: number | undefined
        if (webResponse.headers['x-amz-meta-time-image-taken'] !== undefined) {
            timeTaken = moment.utc(webResponse.headers['x-amz-meta-time-image-taken']).valueOf()
        }
        return {
            isModified: true,
            dataUrl: `data:${contentType};base64,${b64Image}`,
            isBackup: toBoolean(webResponse.headers['x-amz-meta-is-backup'] ?? ''),
            timeTaken,
            etag: webResponse.headers.etag ?? undefined
        }
    }
)

ipc.answerRenderer<void, boolean | undefined>(GET_START_ON_LOGIN, () => {
    return AppConfigStore.startOnLogin
})

ipc.answerRenderer<boolean>(SET_START_ON_LOGIN, startOnLogin => {
    AppConfigStore.startOnLogin = startOnLogin
    configureStartOnLogin(startOnLogin)
})

ipc.answerRenderer<void, boolean>(GET_FIRST_RUN, () => {
    return AppConfigStore.firstRun
})

ipc.answerRenderer<boolean>(SET_FIRST_RUN, firstRun => {
    AppConfigStore.firstRun = firstRun
})

ipc.answerRenderer<void, boolean>(GET_AUTO_UPDATE, () => {
    return AppConfigStore.autoUpdate
})

ipc.answerRenderer<boolean>(SET_AUTO_UPDATE, autoUpdate => {
    AppConfigStore.autoUpdate = autoUpdate
})

ipc.answerRenderer(OPEN_WINDOWS_ICON_SETTINGS, () => {
    if (process.platform !== 'win32') {
        return
    }
    log.info('Opening Windows icon settings')
    // Special command that opens Windows notification area icon settings
    const command = spawn('cmd.exe', ['/c', 'start ms-settings:taskbar'])

    command.stdout.on('data', data => {
        log.debug('Windows icon settings stdout:', data)
    })
    command.stderr.on('data', data => {
        log.error('Windows icon settings stderr:', data)
    })
    command.on('exit', code => {
        log.info('Windows icon settings cmd exited with:', code)
    })
})

ipc.answerRenderer<number, number | undefined>(GET_VIEW_DOWNLOAD_TIME, viewId => {
    log.info('Getting latest download time for view', viewId)
    return latestViewDownloadTimes[viewId]
})

if (process.platform === 'darwin') {
    systemPreferences.subscribeWorkspaceNotification(
        'NSWorkspaceActiveSpaceDidChangeNotification',
        async () => {
            log.debug('macOS active space changed')
            await WallpaperManager.update(Initiator.displayChangeWatcher)
        }
    )
}