const Path = require('path')
const fs = require('../libs/fsExtra')
const workerThreads = require('worker_threads')
const Logger = require('../Logger')
const filePerms = require('../utils/filePerms')
const { secondsToTimestamp } = require('../utils/index')
const { writeMetadataFile } = require('../utils/ffmpegHelpers')
const toneHelpers = require('../utils/toneHelpers')

class AudioMetadataMangaer {
  constructor(db, taskManager, emitter, clientEmitter) {
    this.db = db
    this.taskManager = taskManager
    this.emitter = emitter
    this.clientEmitter = clientEmitter
  }

  updateMetadataForItem(user, libraryItem, useTone = true) {
    if (useTone) {
      this.updateMetadataForItemWithTone(user, libraryItem)
    } else {
      this.updateMetadataForItemWithFfmpeg(user, libraryItem)
    }
  }

  //
  // TONE
  //
  getToneMetadataObjectForApi(libraryItem) {
    return toneHelpers.getToneMetadataObject(libraryItem)
  }

  async updateMetadataForItemWithTone(user, libraryItem) {
    var audioFiles = libraryItem.media.includedAudioFiles

    const itemAudioMetadataPayload = {
      userId: user.id,
      libraryItemId: libraryItem.id,
      startedAt: Date.now(),
      audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
    }

    this.emitter('audio_metadata_started', itemAudioMetadataPayload)

    // Write chapters file
    var chaptersFilePath = null
    const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
    await fs.ensureDir(itemCacheDir)

    if (libraryItem.media.chapters.length) {
      chaptersFilePath = Path.join(itemCacheDir, 'chapters.txt')
      try {
        await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
      } catch (error) {
        Logger.error(`[AudioMetadataManager] Write chapters.txt failed`, error)
        chaptersFilePath = null
      }
    }

    const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
    Logger.debug(`[AudioMetadataManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)

    const results = []
    for (const af of audioFiles) {
      const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneMetadataObject, itemCacheDir)
      results.push(result)
    }

    const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
    Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed)}`)
    itemAudioMetadataPayload.results = results
    itemAudioMetadataPayload.elapsed = elapsed
    itemAudioMetadataPayload.finishedAt = Date.now()
    this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
  }

  async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneMetadataObject, itemCacheDir) {
    const resultPayload = {
      libraryItemId,
      index: audioFile.index,
      ino: audioFile.ino,
      filename: audioFile.metadata.filename
    }
    this.emitter('audiofile_metadata_started', resultPayload)

    // Backup audio file
    try {
      const backupFilePath = Path.join(itemCacheDir, audioFile.metadata.filename)
      await fs.copy(audioFile.metadata.path, backupFilePath)
      Logger.debug(`[AudioMetadataManager] Backed up audio file at "${backupFilePath}"`)
    } catch (err) {
      Logger.error(`[AudioMetadataManager] Failed to backup audio file "${audioFile.metadata.path}"`, err)
    }

    const _toneMetadataObject = {
      ...toneMetadataObject,
      'TrackNumber': audioFile.index
    }

    resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
    if (resultPayload.success) {
      Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
    }

    this.emitter('audiofile_metadata_finished', resultPayload)
    return resultPayload
  }

  //
  // FFMPEG
  //
  async updateMetadataForItemWithFfmpeg(user, libraryItem) {
    var audioFiles = libraryItem.media.audioFiles

    const itemAudioMetadataPayload = {
      userId: user.id,
      libraryItemId: libraryItem.id,
      startedAt: Date.now(),
      audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
    }

    this.emitter('audio_metadata_started', itemAudioMetadataPayload)

    var downloadsPath = Path.join(global.MetadataPath, 'downloads')
    var outputDir = Path.join(downloadsPath, libraryItem.id)
    await fs.ensureDir(outputDir)

    var metadataFilePath = Path.join(outputDir, 'metadata.txt')
    await writeMetadataFile(libraryItem, metadataFilePath)

    if (libraryItem.media.coverPath != null) {
      var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
    }

    const proms = audioFiles.map(af => {
      return this.updateAudioFileMetadataWithFfmpeg(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
    })

    const results = await Promise.all(proms)

    Logger.debug(`[AudioMetadataManager] Finished`)

    await fs.remove(outputDir)

    const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
    Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed)}`)
    itemAudioMetadataPayload.results = results
    itemAudioMetadataPayload.elapsed = elapsed
    itemAudioMetadataPayload.finishedAt = Date.now()
    this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
  }

  updateAudioFileMetadataWithFfmpeg(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
    return new Promise((resolve) => {
      const resultPayload = {
        libraryItemId,
        index: audioFile.index,
        ino: audioFile.ino,
        filename: audioFile.metadata.filename
      }
      this.emitter('audiofile_metadata_started', resultPayload)

      Logger.debug(`[AudioFileMetadataManager] Starting audio file metadata encode for "${audioFile.metadata.filename}"`)

      var outputPath = Path.join(outputDir, audioFile.metadata.filename)
      var inputPath = audioFile.metadata.path
      const isM4b = audioFile.metadata.format === 'm4b'
      const ffmpegInputs = [
        {
          input: inputPath,
          options: isM4b ? ['-f mp4'] : []
        },
        {
          input: metadataFilePath
        }
      ]

      /*
        Mp4 doesnt support writing custom tags by default. Supported tags are itunes tags: https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libavformat/movenc.c;h=b6821d447c92183101086cb67099b2f4804293de;hb=HEAD#l2905

        Workaround -movflags use_metadata_tags found here: https://superuser.com/a/1208277      
        
        Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
      */

      const ffmpegOptions = ['-c copy', '-map_chapters 1', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']

      if (coverPath != '') {
        var ffmpegCoverPathInput = {
          input: coverPath,
          options: ['-f image2pipe']
        }
        var ffmpegCoverPathOptions = [
          '-c:v copy',
          '-map 2:v',
          '-map 0:a'
        ]

        ffmpegInputs.push(ffmpegCoverPathInput)
        Logger.debug(`[AudioFileMetaDataManager] Cover found for "${audioFile.metadata.filename}". Cover will be merged to metadata`)
      } else {
        // remove the video stream to account for the user getting rid an existing cover in abs
        var ffmpegCoverPathOptions = [
          '-map 0',
          '-map -0:v'
        ]

        Logger.debug(`[AudioFileMetaDataManager] No cover found for "${audioFile.metadata.filename}". Cover will be skipped or removed from metadata`)
      }

      ffmpegOptions.push(...ffmpegCoverPathOptions)

      var workerData = {
        inputs: ffmpegInputs,
        options: ffmpegOptions,
        outputOptions: isM4b ? ['-f mp4'] : [],
        output: outputPath,
      }
      var workerPath = Path.join(global.appRoot, 'server/utils/downloadWorker.js')
      var worker = new workerThreads.Worker(workerPath, { workerData })

      worker.on('message', async (message) => {
        if (message != null && typeof message === 'object') {
          if (message.type === 'RESULT') {
            Logger.debug(message)

            if (message.success) {
              Logger.debug(`[AudioFileMetadataManager] Metadata encode SUCCESS for "${audioFile.metadata.filename}"`)

              await filePerms.setDefault(outputPath, true)

              fs.move(outputPath, inputPath, { overwrite: true }).then(() => {
                Logger.debug(`[AudioFileMetadataManager] Audio file replaced successfully "${inputPath}"`)

                resultPayload.success = true
                this.emitter('audiofile_metadata_finished', resultPayload)
                resolve(resultPayload)
              }).catch((error) => {
                Logger.error(`[AudioFileMetadataManager] Audio file failed to move "${inputPath}"`, error)
                resultPayload.success = false
                this.emitter('audiofile_metadata_finished', resultPayload)
                resolve(resultPayload)
              })
            } else {
              Logger.debug(`[AudioFileMetadataManager] Metadata encode FAILED for "${audioFile.metadata.filename}"`)

              resultPayload.success = false
              this.emitter('audiofile_metadata_finished', resultPayload)
              resolve(resultPayload)
            }
          } else if (message.type === 'FFMPEG') {
            if (message.level === 'debug' && process.env.NODE_ENV === 'production') {
              // stderr is not necessary in production
            } else if (Logger[message.level]) {
              Logger[message.level](message.log)
            }
          }
        } else {
          Logger.error('Invalid worker message', message)
        }
      })
    })
  }
}
module.exports = AudioMetadataMangaer