import _ from 'lodash'
import os from 'os'
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
import { app, nativeImage as image } from 'electron'

import * as cyIcons from '@packages/icons'
import * as savedState from '../saved_state'
import menu from '../gui/menu'
import * as Windows from '../gui/windows'
import { GracefulExit } from '../util/graceful-exit'
import { makeGraphQLServer } from '@packages/data-context/graphql/makeGraphQLServer'
import { globalPubSub, getCtx, clearCtx } from '@packages/data-context'
import { telemetry } from '@packages/telemetry'

// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
import type { WebContents } from 'electron'
import type { LaunchArgs, Preferences } from '@packages/types'

import debugLib from 'debug'
import { getPathToDesktopIndex } from '@packages/resolve-dist'

const debug = debugLib('cypress:server:interactive')

const isDev = () => {
  return Boolean(process.env['CYPRESS_INTERNAL_ENV'] === 'development')
}

export = {
  isMac () {
    return os.platform() === 'darwin'
  },

  getWindowArgs (url: string, state: Preferences) {
    // Electron Window's arguments
    // These options are passed to Electron's BrowserWindow
    const minWidth = Math.round(/* 13" MacBook Air */ 1792 / 3) // Thirds

    const preferredWidth = 1200
    const minHeight = 400
    const preferredHeight = 800

    const chooseDimensions = ({ preferred, previous, minimum }) => {
      // If the user doesn't have a previous size that's valid or big
      // enough, use the preferred size instead.
      if (!previous || previous < minimum) {
        return preferred
      }

      return previous
    }

    const common = {
      url,
      // The backgroundColor should match the value we will show in the
      // launchpad frontend.

      // When we use a dist'd launchpad (production), this color won't be
      // as visible. However, in our local dev setup (launchpad running via
      // a dev server), the backgroundColor will flash if it is a
      // different color.
      backgroundColor: 'white',

      // Dimensions of the Electron window on initial launch.
      // Because we are migrating users that may have
      // a width smaller than the min dimensions, we will
      // force the dimensions to be within the minimum bounds.
      //
      // Doing this before launch (instead of relying on minW + minH)
      // prevents the window from jumping.
      width: chooseDimensions({
        preferred: preferredWidth,
        minimum: minWidth,
        previous: state.appWidth,
      }),

      height: chooseDimensions({
        preferred: preferredHeight,
        minimum: minHeight,
        previous: state.appHeight,
      }),

      minWidth,
      minHeight,

      x: state.appX,
      y: state.appY,
      type: 'INDEX',
      devTools: state.isAppDevToolsOpen,
      trackState: {
        width: 'appWidth',
        height: 'appHeight',
        x: 'appX',
        y: 'appY',
        devTools: 'isAppDevToolsOpen',
      },
      onBlur (this: { webContents: WebContents }) {
        if (this.webContents.isDevToolsOpened()) {
          return
        }

        return Windows.hideAllUnlessAnotherWindowIsFocused()
      },
      onFocus () {
        // hide internal dev tools if in production and previously focused
        // window was the electron browser
        menu.set({ withInternalDevTools: isDev() })

        return Windows.showAll()
      },
      onClose () {
        app.quit()
      },
    }

    return _.extend(common, this.platformArgs())
  },

  platformArgs () {
    const args = {
      darwin: {
        show: true,
        frame: true,
        transparent: false,
      },

      linux: {
        show: true,
        frame: true,
        transparent: false,
        // @ts-ignore
        icon: image.createFromPath(cyIcons.getPathToIcon('icon_64x64.png')),
      },
    }

    return args[os.platform()]
  },

  async ready (options: LaunchArgs, launchpadPort: number) {
    const { projectRoot } = options
    const ctx = getCtx()

    menu.set({
      withInternalDevTools: isDev(),
      onLogOutClicked () {
        return globalPubSub.emit('menu:item:clicked', 'log:out')
      },
      getGraphQLPort: () => {
        return ctx?.coreData.servers.gqlServerPort
      },
    })

    const State = await savedState.create(projectRoot, false)
    const state = await State.get()
    const url = getPathToDesktopIndex(launchpadPort)
    const win = await Windows.open(projectRoot, this.getWindowArgs(url, state))

    ctx?.actions.electron.setBrowserWindow(win)

    return win
  },

  async run (options: LaunchArgs, _loading: Promise<void>) {
    // Need to set this for system notifications to appear as "Cypress" on Windows
    if (app.setAppUserModelId) {
      app.setAppUserModelId('Cypress')
    }

    // Note: We do not await the `_loading` promise here since initializing
    // the data context can significantly delay initial render of the UI
    // https://github.com/cypress-io/cypress/issues/26388#issuecomment-1492616609

    const [, port] = await Promise.all([
      app.whenReady(),
      makeGraphQLServer(),
    ])

    // Ctrl+C (SIGINT) on macOS/Unix is routed to app.quit() in Electron; the
    // Node process.on('SIGINT') path often does not run. Use before-quit (not
    // will-quit), which still fires for that quit path.
    // The dev Node launcher relays SIGINT to this process and must not exit first
    // or quit lifecycle never completes (see cypress runElectron + GracefulExit.detachProcessSignalHandlers).
    // https://github.com/cypress-io/cypress/issues/22026

    app.on('before-quit', async (event: Electron.Event) => {
      event.preventDefault()
      setImmediate(async () => {
        try {
          await GracefulExit.exitGracefully(0)
        } catch (e) {
          debug(`graceful exit errored during quit: ${(e as Error)?.message}`)
          process.exit(1)
        }
      })
    })

    telemetry.getSpan('startup:time')?.end()

    return this.ready(options, port)
  },
}
