import fs from 'fs'
import yargs from 'yargs'
import TOML from '@iarna/toml'
import * as Y from 'yjs'
import { create as createJSONDiffPatch } from 'jsondiffpatch'
import { Repeater } from '@repeaterjs/repeater'
import { app, shell, session, BrowserWindow } from 'electron'

import { ensureValidURL } from '../util'
import {
  pollDataURL,
  watchDataFile,
  StreamIDGenerator,
  markDataSource,
  combineDataSources,
} from './data'
import StreamWindow from './StreamWindow'
import TwitchBot from './TwitchBot'
import StreamdelayClient from './StreamdelayClient'
import initWebServer from './server'

function parseArgs() {
  return yargs
    .config('config', (configPath) => {
      return TOML.parse(fs.readFileSync(configPath, 'utf-8'))
    })
    .group(['grid.count'], 'Grid dimensions')
    .option('grid.count', {
      number: true,
      default: 3,
    })
    .group(
      [
        'window.width',
        'window.height',
        'window.x',
        'window.y',
        'window.frameless',
        'window.background-color',
      ],
      'Window settings',
    )
    .option('window.x', {
      number: true,
    })
    .option('window.y', {
      number: true,
    })
    .option('window.width', {
      number: true,
      default: 1920,
    })
    .option('window.height', {
      number: true,
      default: 1080,
    })
    .option('window.frameless', {
      boolean: true,
      default: false,
    })
    .option('window.background-color', {
      describe: 'Background color of wall (useful for chroma-keying)',
      default: '#000',
    })
    .group(['data.json-url', 'data.toml-file'], 'Datasources')
    .option('data.json-url', {
      describe: 'Fetch streams from the specified URL(s)',
      array: true,
      default: ['https://woke.net/api/streams.json'],
    })
    .option('data.toml-file', {
      describe: 'Fetch streams from the specified file(s)',
      normalize: true,
      array: true,
      default: [],
    })
    .group(
      [
        'twitch.channel',
        'twitch.username',
        'twitch.password',
        'twitch.color',
        'twitch.announce.template',
        'twitch.announce.interval-seconds',
      ],
      'Twitch Chat',
    )
    .option('twitch.channel', {
      describe: 'Name of Twitch channel',
      default: null,
    })
    .option('twitch.username', {
      describe: 'Username of Twitch bot account',
      default: null,
    })
    .option('twitch.password', {
      describe: 'Password of Twitch bot account',
      default: null,
    })
    .option('twitch.color', {
      describe: 'Color of Twitch bot username',
      default: '#ff0000',
    })
    .option('twitch.announce.template', {
      describe: 'Message template for stream announcements',
      default:
        'SingsMic <%- stream.source %> <%- stream.city && stream.state ? `(${stream.city} ${stream.state})` : `` %> <%- stream.link %>',
    })
    .option('twitch.announce.interval', {
      describe:
        'Minimum time interval (in seconds) between re-announcing the same stream',
      number: true,
      default: 60,
    })
    .option('twitch.announce.delay', {
      describe: 'Time to dwell on a stream before its details are announced',
      number: true,
      default: 30,
    })
    .group(
      [
        'control.username',
        'control.password',
        'control.address',
        'control.hostname',
        'control.port',
        'control.open',
      ],
      'Control Webserver',
    )
    .option('control.username', {
      describe: 'Web control server username',
    })
    .option('control.password', {
      describe: 'Web control server password',
    })
    .option('control.open', {
      describe: 'After launching, open the control website in a browser',
      boolean: true,
      default: true,
    })
    .option('control.address', {
      describe: 'Enable control webserver and specify the URL',
      implies: ['control.username', 'control.password'],
    })
    .option('control.hostname', {
      describe: 'Override hostname the control server listens on',
    })
    .option('control.port', {
      describe: 'Override port the control server listens on',
      number: true,
    })
    .group(
      ['cert.dir', 'cert.production', 'cert.email'],
      'Automatic SSL Certificate',
    )
    .option('cert.dir', {
      describe: 'Private directory to store SSL certificate in',
      implies: ['email'],
      default: null,
    })
    .option('cert.production', {
      describe: 'Obtain a real SSL certificate using production servers',
    })
    .option('cert.email', {
      describe: 'Email for owner of SSL certificate',
    })
    .group(['streamdelay.endpoint', 'streamdelay.key'], 'Streamdelay')
    .option('streamdelay.endpoint', {
      describe: 'URL of Streamdelay endpoint',
      default: 'http://localhost:8404',
    })
    .option('streamdelay.key', {
      describe: 'Streamdelay API key',
      default: null,
    })
    .help().argv
}

async function main() {
  const argv = parseArgs()
  if (argv.help) {
    return
  }

  // Reject all permission requests from web content.
  session
    .fromPartition('persist:session')
    .setPermissionRequestHandler((webContents, permission, callback) => {
      callback(false)
    })

  const idGen = new StreamIDGenerator()
  let updateCustomStreams
  const customStreamData = new Repeater(async (push) => {
    await push([])
    updateCustomStreams = push
  })

  const streamWindow = new StreamWindow({
    gridCount: argv.grid.count,
    width: argv.window.width,
    height: argv.window.height,
    x: argv.window.x,
    y: argv.window.y,
    frameless: argv.window.frameless,
    backgroundColor: argv.window['background-color'],
  })
  streamWindow.init()

  let browseWindow = null
  let twitchBot = null
  let streamdelayClient = null

  let clientState = {
    config: {
      width: argv.window.width,
      height: argv.window.height,
      gridCount: argv.grid.count,
    },
    streams: [],
    customStreams: [],
    views: [],
    streamdelay: null,
  }

  const stateDoc = new Y.Doc()
  const viewsState = stateDoc.getMap('views')
  stateDoc.transact(() => {
    for (let i = 0; i < argv.grid.count ** 2; i++) {
      const data = new Y.Map()
      data.set('streamId', '')
      viewsState.set(i, data)
    }
  })
  viewsState.observeDeep(() => {
    const viewContentMap = new Map()
    for (const [key, viewData] of viewsState) {
      const stream = clientState.streams.find(
        (s) => s._id === viewData.get('streamId'),
      )
      if (!stream) {
        continue
      }
      viewContentMap.set(key, {
        url: stream.link,
        kind: stream.kind || 'video',
      })
    }
    streamWindow.setViews(viewContentMap)
  })

  const getInitialState = () => clientState
  let broadcast = () => {}
  const onMessage = (msg) => {
    if (msg.type === 'set-listening-view') {
      streamWindow.setListeningView(msg.viewIdx)
    } else if (msg.type === 'set-view-blurred') {
      streamWindow.setViewBlurred(msg.viewIdx, msg.blurred)
    } else if (msg.type === 'set-custom-streams') {
      updateCustomStreams(msg.streams)
    } else if (msg.type === 'reload-view') {
      streamWindow.reloadView(msg.viewIdx)
    } else if (msg.type === 'browse' || msg.type === 'dev-tools') {
      if (
        msg.type === 'dev-tools' &&
        browseWindow &&
        !browseWindow.isDestroyed()
      ) {
        // DevTools needs a fresh webContents to work. Close any existing window.
        browseWindow.destroy()
        browseWindow = null
      }
      if (!browseWindow || browseWindow.isDestroyed()) {
        browseWindow = new BrowserWindow({
          webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            partition: 'persist:session',
            sandbox: true,
          },
        })
      }
      if (msg.type === 'browse') {
        ensureValidURL(msg.url)
        browseWindow.loadURL(msg.url)
      } else if (msg.type === 'dev-tools') {
        streamWindow.openDevTools(msg.viewIdx, browseWindow.webContents)
      }
    } else if (msg.type === 'set-stream-censored' && streamdelayClient) {
      streamdelayClient.setCensored(msg.isCensored)
    }
  }

  const stateDiff = createJSONDiffPatch({
    objectHash: (obj, idx) => obj._id || `$$index:${idx}`,
  })
  function updateState(newState) {
    const lastClientState = clientState
    clientState = { ...clientState, ...newState }
    const delta = stateDiff.diff(lastClientState, clientState)
    if (!delta) {
      return
    }
    broadcast({
      type: 'state-delta',
      delta,
    })
    streamWindow.send('state', clientState)
    if (twitchBot) {
      twitchBot.onState(clientState)
    }
  }

  if (argv.control.address) {
    ;({ broadcast } = await initWebServer({
      certDir: argv.cert.dir,
      certProduction: argv.cert.production,
      email: argv.cert.email,
      url: argv.control.address,
      hostname: argv.control.hostname,
      port: argv.control.port,
      username: argv.control.username,
      password: argv.control.password,
      getInitialState,
      onMessage,
      stateDoc,
    }))
    if (argv.control.open) {
      shell.openExternal(argv.control.address)
    }
  }

  if (argv.streamdelay.key) {
    streamdelayClient = new StreamdelayClient({
      endpoint: argv.streamdelay.endpoint,
      key: argv.streamdelay.key,
    })
    streamdelayClient.on('state', (state) => {
      updateState({ streamdelay: state })
    })
    streamdelayClient.connect()
  }

  if (argv.twitch.token) {
    twitchBot = new TwitchBot(argv.twitch)
    twitchBot.connect()
  }

  streamWindow.on('state', (viewStates) => {
    updateState({ views: viewStates })
  })

  const dataSources = [
    ...argv.data['json-url'].map((url) =>
      markDataSource(pollDataURL(url), 'json-url'),
    ),
    ...argv.data['toml-file'].map((path) =>
      markDataSource(watchDataFile(path), 'toml-file'),
    ),
    markDataSource(customStreamData, 'custom'),
  ]

  for await (const rawStreams of combineDataSources(dataSources)) {
    const streams = idGen.process(rawStreams)
    updateState({ streams })
  }
}

if (require.main === module) {
  app
    .whenReady()
    .then(main)
    .catch((err) => {
      console.trace(err.toString())
      process.exit(1)
    })
}
