/*
 * player.js
 * hidden audio engine. talks to the main window via IPC.
 *
 * you can read this following stuff if you
 * want to self-host vnk:
 *
 * HOW MUSIC IS FETCHED:
 *   - baseUrl is the CDN/server root.
 *   - files are expected at:
 *     <baseUrl>/<vnId>/<trackName>.mp3
 *   e.g. http://example.com/clannad/Nagisa.mp3
 *
 *   to add a self-hosted track, point BASE_URL at
 *   your server and add the track name to app/vn.json.
 *
 * ADDING NEW VNs:
 *   1. add an entry to app/vn.json
 *   2. put audio files at <baseUrl>/<id>/<trackName>.mp3
 *
 * RAIN SOUNDS:
 *   <baseUrl>/rain/rain.mp3
 *   <baseUrl>/rain/no-thunder-rain.mp3
 */

const ipc = require('electron').ipcRenderer
const storage = require('electron-json-storage')
const Wad = window.Wad
const AudioContext = window.AudioContext

const BASE_URL = 'https://vgm.czkove.cc/'
const path = require('path')
const vnGames = require(path.join(__dirname, '../../app/vn.json'))

// runtime state
let sound; let soundGain; let soundSources = []
let rain; let rainGain; let rainSources = []
let soundVol = 0.5
let rainVol = 0.5
let currentVn = 'clannad'
let currentTrack = null
let paused = false
let grandFather = false
let gameRain = false
let peacefulRain = false
let preferNoDownload = false
let customBaseUrl = ''
let userSettingsPath = '' // eslint-disable-line no-unused-vars
let lang = 'en'
let chime
let tune = []
let tuneEnabled = true
let beep
let beepTimeout
let chimeInt

const tunes = ['G0', 'A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'A2', 'B2', 'C2', 'D2', 'E2']
const tunesBeepMap = ['G4', 'A4', 'B4', 'C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5', 'C6', 'D6', 'E6']

const getBaseUrl = () => {
  const u = (customBaseUrl || BASE_URL).replace(/\/$/, '')
  return u
}

// build full URL for track: <base>/<vnId>/<trackName>.mp3
const trackUrl = (vnId, trackName) => {
  return `${getBaseUrl()}/${vnId}/${encodeURIComponent(trackName)}.mp3`
}

const rainBaseUrl = 'https://d17orwheorv96d.cloudfront.net'
const rainUrl = (type) => {
  const name = type === 'game' ? 'game-rain' : type === 'peaceful' ? 'no-thunder-rain' : 'rain'
  return `${rainBaseUrl}/rain/${name}.ogg`
}

// pick a random track from the current VN's track list
const pickRandomTrack = () => {
  const vn = vnGames.find(v => v.id === currentVn)
  if (!vn || !vn.tracks.length) return null
  return vn.tracks[Math.floor(Math.random() * vn.tracks.length)]
}

/* audio stuff */

const fadeSound = async (mode = 'sound', fadeIn = true) => {
  const vol = mode === 'sound' ? soundVol : rainVol
  for (let i = 0; i <= vol * 100; i++) {
    await new Promise(resolve => setTimeout(resolve, 2))
    const activeObj = mode === 'sound' ? sound : rain
    if (fadeIn && activeObj) {
      if (mode === 'sound') soundGain.gain.value = i / 100
      else rainGain.gain.value = i / 100
    } else if (!fadeIn) {
      if (mode === 'sound' && soundGain) soundGain.gain.value = vol - (i / 100)
      if (mode === 'rain' && rainGain) rainGain.gain.value = vol - (i / 100)
    } else {
      return 'skip'
    }
  }
  return 'done'
}

const stopAudio = async (mode = 'sound') => {
  if (mode === 'rain' || mode === 'all') {
    if (rain) {
      await fadeSound('rain', false)
      rain.stop()
      rain = null
      rainSources.forEach(s => { try { s.stop() } catch (e) {} })
      rainSources = []
    }
    if (mode === 'rain') return
  }
  if (mode === 'sound' || mode === 'all') {
    ipc.send('playing', [])
    if (sound) {
      await fadeSound('sound', false)
      sound.removeEventListener('ended', onTrackEnded)
      sound.stop()
      sound = null
      soundSources.forEach(s => { try { s.stop() } catch (e) {} })
      soundSources = []
    }
  }
}

const playUrl = async (url) => {
  if (!url) return
  const context = new AudioContext()
  let arrayBuffer

  try {
    const res = await fetch(url)
    arrayBuffer = await res.arrayBuffer()
  } catch (err) {
    console.error('VNook: failed to fetch audio', url, err)
    ipc.send('toWindow', ['error', 'failedToLoadSound'])
    return
  }

  let audioBuffer
  try {
    audioBuffer = await context.decodeAudioData(arrayBuffer)
  } catch (err) {
    console.error('VNook: failed to decode audio', err)
    ipc.send('toWindow', ['error', 'failedToLoadSound'])
    return
  }

  const source = context.createBufferSource()
  soundSources.push(source)
  source.buffer = audioBuffer
  soundGain = context.createGain()
  soundGain.gain.value = 0
  source.connect(soundGain).connect(context.destination)
  source.loop = !grandFather
  source.addEventListener('ended', onTrackEnded)

  if (!paused) {
    sound = source
    sound.start()
    await fadeSound('sound', true)
    const vnName = vnGames.find(v => v.id === currentVn)?.name || currentVn
    ipc.send('playing', [vnName, currentTrack])
  }
}

const onTrackEnded = () => {
  // when a track finishes (grandFather / non-looping), pick next
  currentTrack = pickRandomTrack()
  if (currentTrack) playUrl(trackUrl(currentVn, currentTrack))
}

const playRain = async () => {
  const type = gameRain ? 'game' : peacefulRain ? 'peaceful' : 'normal'
  const url = rainUrl(type)

  const context = new AudioContext()
  let arrayBuffer
  try {
    const res = await fetch(url)
    arrayBuffer = await res.arrayBuffer()
  } catch (err) {
    ipc.send('toWindow', ['error', 'failedToLoadRainSound'])
    return
  }

  let audioBuffer
  try {
    audioBuffer = await context.decodeAudioData(arrayBuffer)
  } catch (err) {
    ipc.send('toWindow', ['error', 'failedToLoadRainSound'])
    return
  }

  const source = context.createBufferSource()
  rainSources.push(source)
  source.buffer = audioBuffer
  rainGain = context.createGain()
  rainGain.gain.value = 0
  source.connect(rainGain).connect(context.destination)
  source.loop = true

  if (!paused) {
    rain = source
    rain.start()
    await fadeSound('rain', true)
  }
}

const startPlayback = async () => {
  paused = false
  currentTrack = pickRandomTrack()
  if (!currentTrack) return
  await playUrl(trackUrl(currentVn, currentTrack))
}

const pauseToggle = async () => {
  if (!paused) {
    paused = true
    await stopAudio('all')
  } else {
    paused = false
    await playRain()
    await startPlayback()
  }
}

const playChime = async (play) => { // eslint-disable-line no-unused-vars
  return new Promise(resolve => {
    if (!play || !chime) return resolve()
    let i = 0
    chimeInt = setInterval(() => {
      chime.setVolume(soundVol)
      const note = tune[i]
      if (note && note !== 'zZz' && note !== '-' && note !== '?') {
        chime[note] && chime[note].play()
      } else if (note === '?') {
        const r = tunes[Math.floor(Math.random() * tunes.length)]
        chime[r] && chime[r].play()
      }
      i++
      if (i === tune.length) {
        clearInterval(chimeInt)
        resolve()
      }
    }, 900)
  })
}

const playBeep = async (note, delay) => {
  return new Promise(resolve => {
    if (note === '1') {
      beepTimeout = setTimeout(() => { beep.stop(); resolve() }, delay)
    } else if (note === '2') {
      resolve()
    } else {
      const pitchIndex = note === '16' ? Math.floor(Math.random() * 13) : +note - 3
      clearTimeout(beepTimeout)
      beep.stop()
      beep.play({ pitch: tunesBeepMap[pitchIndex] })
      beepTimeout = setTimeout(() => { beep.stop(); resolve() }, delay)
    }
  })
}

const playBeeps = async (beeps) => {
  // for (const [i, val] of beeps.entries()) {
  for (const val of beeps) { // since i is not needed otherwise i get stupid warnings
    await playBeep(val, 350)
  }
}

const handleIpc = async (event, arg) => {
  const command = arg[0]
  const params = arg.slice(1)

  switch (command) {
    case 'userSettingsPath': {
      userSettingsPath = params[0]
      storage.setDataPath(params[0])
      // storage.keys((err, k) => { doMain(k || []) })
      storage.keys((err, k) => { doMain(err ? [] : (k || [])) })
      break
    }
    case 'musicVol': {
      soundVol = +params[0] / 100
      if (soundGain) soundGain.gain.value = soundVol
      if (chime) chime.setVolume(soundVol)
      storage.set('soundVol', { volume: +params[0] })
      break
    }
    case 'rainVol': {
      rainVol = +params[0] / 100
      if (rainGain) rainGain.gain.value = rainVol
      storage.set('rainVol', { volume: +params[0] })
      break
    }
    case 'vn': {
      currentVn = params[0]
      storage.set('vn', { vn: params[0] })
      await stopAudio('sound')
      await startPlayback()
      break
    }
    case 'customBaseUrl': {
      customBaseUrl = params[0]
      storage.set('customBaseUrl', { url: params[0] })
      await stopAudio('all')
      await playRain()
      await startPlayback()
      break
    }
    case 'grandFather': {
      grandFather = params[0]
      storage.set('grandFather', { enabled: params[0] })
      await stopAudio('sound')
      await startPlayback()
      break
    }
    case 'tuneEnabled': {
      tuneEnabled = params[0]
      storage.set('tuneEnabled', { tuneEnabled: params[0] })
      break
    }
    case 'tune': {
      tune = params[0]
      storage.set('tune', { tune: params[0] })
      break
    }
    case 'preferNoDownload': {
      preferNoDownload = params[0]
      storage.set('preferNoDownload', { preferNoDownload: params[0] })
      break
    }
    case 'gameRain': {
      gameRain = params[0]
      peacefulRain = false
      storage.set('gameRain', { enabled: params[0] })
      storage.set('peacefulRain', { enabled: false })
      await stopAudio('rain')
      await playRain()
      break
    }
    case 'peacefulRain': {
      peacefulRain = params[0]
      gameRain = false
      storage.set('peacefulRain', { enabled: params[0] })
      storage.set('gameRain', { enabled: false })
      await stopAudio('rain')
      await playRain()
      break
    }
    case 'paused': {
      storage.set('paused', { paused: params[0] })
      await pauseToggle()
      break
    }
    case 'pauseIfPlaying': {
      if (!paused) {
        storage.set('paused', { paused: true })
        await pauseToggle()
        ipc.send('toWindow', ['pause'])
      }
      break
    }
    case 'lang': {
      lang = params[0]
      storage.set('lang', { lang: params[0] })
      break
    }
    case 'playNote': {
      await playBeep(params[0], 350)
      break
    }
    case 'playTune': {
      await playBeeps(params[0])
      break
    }
    case 'openOnStartup': {
      storage.set('openOnStartup', { enabled: params[0] })
      ipc.send('openOnStartup', [params[0]])
      break
    }
  }
}

const doMain = (keys) => {
  soundVol = (storage.getSync('soundVol').volume ?? 50) / 100
  rainVol = (storage.getSync('rainVol').volume ?? 50) / 100
  grandFather = storage.getSync('grandFather').enabled ?? false
  currentVn = storage.getSync('vn').vn ?? 'clannad'
  lang = storage.getSync('lang').lang ?? 'en'
  tuneEnabled = storage.getSync('tuneEnabled').tuneEnabled ?? true
  preferNoDownload = storage.getSync('preferNoDownload').preferNoDownload ?? false
  tune = storage.getSync('tune').tune ?? ['G1', 'E2', '-', 'G1', 'F1', 'D2', '-', 'B2', 'C2', 'zZz', '?', 'zZz', 'C1', '-', 'zZz', 'zZz']
  paused = storage.getSync('paused').paused ?? false
  gameRain = storage.getSync('gameRain').enabled ?? false
  peacefulRain = storage.getSync('peacefulRain').enabled ?? false
  customBaseUrl = storage.getSync('customBaseUrl').url ?? ''

  // send settings to ui window
  ipc.send('toWindow', ['configs', {
    soundVol: soundVol * 100,
    rainVol: rainVol * 100,
    grandFather,
    vn: currentVn,
    lang,
    tune,
    tuneEnabled,
    preferNoDownload,
    paused,
    gameRain,
    peacefulRain,
    customBaseUrl
  }])

  // set up chime instrument
  chime = new Wad({
    source: 'chime.ogg',
    sprite: {
      G0: [0, 4],
      A1: [4, 8],
      B1: [8, 12],
      C1: [12, 16],
      D1: [16, 20],
      E1: [20, 24],
      F1: [24, 28],
      G1: [28, 32],
      A2: [32, 36],
      B2: [36, 40],
      C2: [40, 44],
      D2: [44, 48],
      E2: [48, 52]
    },
    volume: soundVol
  })

  beep = new Wad({
    source: 'triangle',
    env: { hold: -1, release: 0.2, sustain: 0.1 },
    pitch: 'G4',
    volume: 0.50
  })

  if (!paused) {
    setTimeout(async () => {
      await playRain()
      await startPlayback()
    }, 0)
  }
}

ipc.on('toPlayer', handleIpc)
ipc.send('playerLoaded')
