import { CoreService } from '..'
import { EncodingOutput, VideoEncodingJob, VideoInfo } from '../../../common/models/video.model'
import tmp from 'tmp'
import path from 'path'
import ffmpeg from 'fluent-ffmpeg'
import fs from 'fs'
import { uuidv4 } from 'uuid'
import { globSource } from 'ipfs-http-client'
import { EventEmitter } from 'events'
//import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
import { GetFfmpegPath } from '../ffmpeg_helper'

if (process.env.NODE_ENV === 'development') {
  //ffmpeg.setFfmpegPath(ffmpegPath)
} else {
  try {
    ffmpeg.setFfmpegPath(GetFfmpegPath())
  } catch (ex) {
    console.error(`Error getting ffmpeg path for production`)
  }
}

const MAX_BIT_RATE = {
  '1080': '2760k',
  '720': '1327k',
  '480': '763k',
  '360': '423k',
  '240': '155k',
  '144': '640k',
}
class tutils {
  /**
   * get an array of possible downsampled bitrates
   * @param  {number} height Video height, grabbed from ffmpeg probe
   * @return {array}        Array of possible downsample sizes.
   */
  static getPossibleBitrates(height) {
    if (!height) {
      return null
    }

    if (height < 144) {
      // very small bitrate, use the original format.
      return ['?x' + height]
    } else if (height < 240) {
      return ['?x144']
    } else if (height < 360) {
      return ['?x240', '?x144']
    } else if (height < 480) {
      return ['?x360', '?x240', '?x144']
    } else if (height < 720) {
      return ['?x480', '?x360', '?x240', '?x144']
    } else if (height < 1080) {
      return ['?x720', '?x480', '?x360', '?x240', '?x144']
    } else if (height < 1440) {
      return ['?x1080', '?x720', '?x480', '?x360', '?x240', '?x144']
    } else if (height < 2160) {
      return ['?x1440', '?x1080', '?x720', '?x480', '?x360', '?x240', '?x144']
    } else {
      return ['?x2160', '?x1440', '?x1080', '?x720', '?x480', '?x360', '?x240', '?x144']
    }
  }

  static getBandwidth(height) {
    if (!height) {
      return null
    }

    // default to the lowest height. in case the video is smaller than that.
    return MAX_BIT_RATE[String(height)] || MAX_BIT_RATE['144']
  }

  /**
   * get video height from size String
   * @param  {String} size string from ffmpeg @example '?x720'
   * @return {number}      height integer.
   */
  static getHeight(size: string) {
    return parseInt(size.split('x')[1])
  }

  static calculateWidth(codecData, currentHeight) {
    const resString = /^\d{3,}x\d{3,}/g // test
    // test all video_details against resString
    let res = codecData.video_details.filter((str) => {
      return resString.test(str)
    })
    if (res && res.length > 0) {
      res = res[0]
      res = res.split('x')
    } else {
      return null
    }
    const width = parseInt(res[0])
    const height = parseInt(res[1])

    const s = parseInt(currentHeight)

    return String(Math.floor((width * s) / height) + 'x' + s)
  }
}

export class EncoderService {
  events: any
  statusInfo: any
  self: CoreService
  jobOutput: any
  constructor(self: CoreService) {
    this.self = self

    this.statusInfo = {}
    this.jobOutput = {}
    this.events = new EventEmitter()
  }
  async status(id) {
    return this.statusInfo[id]
  }
  getJobOutput(jobId) {
    return new Promise((resolve, reject) => {
      if (this.jobOutput[jobId]) {
        return resolve(this.jobOutput[jobId])
      } else {
        this.events.once(`complete.${jobId}`, (error, output) => {
          if (error) return reject(error)
          return resolve(output)
        })
      }
    })
  }

  get logger() {
    return this.self.logger
  }

  async executeJob(jobInfo: VideoEncodingJob): Promise<EncodingOutput> {
    this.logger.info(`Executing encoding job...`)
    this.logger.info(`job info: ${JSON.stringify(jobInfo, null, 2)}`)

    const workfolder = tmp.dirSync().name as string

    this.logger.info(`workfolder: ${workfolder}`)
    const command = ffmpeg(jobInfo.sourceUrl)
    this.statusInfo[jobInfo.id] = {
      progress: {},
      stage: 0,
      nstages: jobInfo.profiles.length,
    }

    this.logger.info('got ffmpeg command')

    const codec = await new Promise((resolve, reject) =>
      ffmpeg.getAvailableEncoders(async (e, enc) => {
        if (jobInfo.options.hwaccel !== null || jobInfo.options.hwaccel !== 'none') {
          for (const key of Object.keys(enc)) {
            if (key.includes(`h264_${jobInfo.options.hwaccel}`)) {
              return resolve(key)
            }
          }
        }
        return resolve('libx264')
      }),
    )
    this.logger.info(`Using video codec ${codec}`)
    command.videoCodec(codec)
    command.audioCodec('aac')
    command.audioBitrate('256k')

    command
      .addOption('-hls_time', 5)
      // include all the segments in the list
      .addOption('-hls_list_size', 0)
      .addOption('-segment_time', 10)
      .addOption('-f', 'segment')
    //command.output(path.join(workfolder, "480p/index.m3u8")).outputFormat("hls")

    const sizes = []
    let codecData
    let duration
    this.logger.info(`Processing profiles`)
    for (const profile of jobInfo.profiles) {
      const ret = command.clone()
      sizes.push(profile.size)
      ret.size(profile.size)
      this.logger.info(`Profile size ${profile.size}`)
      ret.on(
        'progress',
        ((progress) => {
          this.events.emit('progress', jobInfo.id, progress)
          this.statusInfo[jobInfo.id].progress = progress
        }).bind(this),
      )
      ret.on('end', () => {
        this.events.emit('done', jobInfo.id)
        //this.statusInfo[jobInfo.id].done = true;
        delete this.statusInfo[jobInfo.id].progress
      })
      const promy = new Promise<void>((resolve, reject) => {
        ret
          .on('end', () => {
            resolve()
          })
          .on('error', (err) => {
            reject(err)
          })
          .on('codecData', (data) => {
            codecData = data
            duration = codecData.duration
          })
      })
      ret.videoBitrate(MAX_BIT_RATE[String(profile.size.split('x')[1])])
      fs.mkdirSync(path.join(workfolder, `${String(profile.size.split('x')[1])}p`))
      //ret.save(path.join(workfolder, `${String(size.split('x')[1])}p`, 'index.m3u8'))
      ret.addOption(`-segment_format`, 'mpegts')
      ret.addOption(
        '-segment_list',
        path.join(workfolder, `${String(profile.size.split('x')[1])}p`, 'index.m3u8'),
      )
      ret.save(
        path.join(
          workfolder + '/' + `${String(profile.size.split('x')[1])}p`,
          `${String(profile.size.split('x')[1])}p_%d.ts`,
        ),
      )
      await promy

      this.statusInfo[jobInfo.id].stage += 1
    }

    const manifest = this.generateManifest(codecData, sizes)
    fs.writeFileSync(path.join(workfolder, 'manifest.m3u8'), manifest)

    this.logger.info(`Wrote manifest to ${workfolder}`)

    const ipfsHash = this.self.ipfs.addAll(globSource(workfolder, '**/*'), {
      pin: false,
    })

    //     for await (const file of globSource(workfolder, '**/*')) {
    //       this.logger.info(`Matched file path ${file.path}`)
    //       const ipfsHash = await this.self.ipfs.add(file, {
    //         pin: false,
    //       })

    //       //       this.logger.info(`Generated IPFS hash `)
    //       //       console.log(ipfsHash)

    //       output.push({
    //         ipfsHash: ipfsHash.cid.toString(),
    //         size: ipfsHash.size,
    //         playUrl: path.join(ipfsHash.cid.toString(), 'manifest.m3u8'),
    //         folderPath: workfolder,
    //         duration,
    //         path: 'manifest.m3u8',
    //       })
    //     }

    for await (const item of ipfsHash) {
      const output: EncodingOutput = {
        ipfsHash: item.cid.toString(),
        size: item.size,
        playUrl: path.join(item.cid.toString(), 'manifest.m3u8'),
        folderPath: workfolder,
        duration,
        path: 'manifest.m3u8' as any,
      }

      this.events.emit(`complete.${jobInfo.id}`, null, output)

      return output
    }
  }

  getScreenshot(path) {
    return new Promise((resolve, reject) => {
      const workfolder = tmp.dirSync()
      let screenshotPath
      ffmpeg(path)
        .on('filenames', function (filenames) {
          screenshotPath = path.join(workfolder, filenames[0])
        })
        .on('end', function () {
          return resolve(screenshotPath)
        })
        .screenshots({
          // Will take screens at 20%, 40%, 60% and 80% of the video
          timestamps: [Math.floor(Math.random() * 100) + 1 + '%'],
          count: 1,
          folder: workfolder,
        })
    })
  }
  /**
   * generate the master manifest for the transcoded video.
   * @return {Promise<String>}      generated Manifest string.
   */
  generateManifest(codecData, sizes) {
    let master = '#EXTM3U\n'
    master += '#EXT-X-VERSION:6\n'
    const resolutionLine = (size) => {
      return `#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=${tutils.getBandwidth(
        tutils.getHeight(size),
      )},CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=${tutils.calculateWidth(
        codecData,
        tutils.getHeight(size),
      )},NAME=${tutils.getHeight(size)}\n`
    }
    let result = master
    sizes.forEach((size) => {
      // log(`format: ${JSON.stringify(formats[size])} , size: ${size}`)
      result += resolutionLine(size)
      result += String(size.split('x')[1]) + 'p/index.m3u8\n'
    })

    return result
  }

  createJob(job: VideoEncodingJob): VideoEncodingJob {
    if (job.id) {
      job.id == uuidv4()
    }
    void this.executeJob(job)
    return job
  }

  async ready(): Promise<boolean> {
    return true
  }
}