import path from 'path'
import sharp from 'sharp'
import fs from 'fs/promises'
import config from '../config.mjs'
import { HttpError } from 'flaska'
import * as security from './security.mjs'
import * as formidable from './formidable.mjs'

export default class MediaRoutes {
  constructor(opts = {}) {
    Object.assign(this, {
      security: opts.security || security,
      formidable: opts.formidable || formidable,
      sharp: opts.sharp || sharp,
      fs: opts.fs || fs,
    })
    this.siteCache = new Map()
    this.collator = new Intl.Collator('is-IS', { numeric: false, sensitivity: 'accent' })
  }

  register(server) {
    this.init(server).then(function() {}, function(err) {
      server.core.log.error(err, 'Error initing media')
    })

    server.flaska.get('/media', [server.queryHandler()], this.listFiles.bind(this))
    server.flaska.get('/media/:site', this.listPublicFiles.bind(this))
    server.flaska.post('/media', [server.queryHandler()], this.upload.bind(this))
    server.flaska.post('/media/noprefix', [server.queryHandler()], this.uploadNoPrefix.bind(this))
    server.flaska.post('/media/resize', [server.queryHandler()], this.resize.bind(this))
    server.flaska.post('/media/resize/:filename', [server.queryHandler(), server.jsonHandler()], this.resizeExisting.bind(this))
    server.flaska.delete('/media/:filename', [server.queryHandler()], this.remove.bind(this))
  }

  init(server) {
    let folders = Object.keys(config.get('sites'))

    return Promise.all(folders.map(folder => {
      return fs.readdir(config.get('uploadFolder') + '/' + folder)
        .then(files => {
          return files.map(file => {
            return { filename: file, size: null }
          })
        })
        .catch(err => {
          server.core.log.error(err, `Error reading folder "${config.get('uploadFolder')}/${folder}"`)
          return []
        })
        .then(files => {
          this.siteCache.set(folder, files)
        })
    }))
  }

  filesCacheGet(site) {
    let files = this.siteCache.get(site) || []
    return files.sort((a, b) => {
      return this.collator.compare(a.filename, b.filename)
    })
  }

  filesCacheAdd(site, filename, size) {
    let arr = this.siteCache.get(site)
    if (!arr) {
      this.siteCache.set(site, arr = [])
    }
    let found = false
    for (let file of arr) {
      if (file.filename === filename) {
        found = true
        file.size = size
        break
      }
    }
    if (!found) {
      arr.push({ filename: filename, size: size })
    }
  }
  
  filesCacheRemove(site, filename) {
    let arr = this.siteCache.get(site)
    if (!arr) return

    for (let i = 0; i < arr.length; i++) {
      if (arr[i].filename === filename) {
        arr.splice(i, 1)
        break
      }
    }
  }

  async listFiles(ctx) {
    let site = await this.security.verifyToken(ctx)

    ctx.body = this.filesCacheGet(site)
  }
  
  async listPublicFiles(ctx) {
    this.security.throwIfNotPublic(ctx.params.site)

    ctx.body = this.filesCacheGet(ctx.params.site)
  }

  async upload(ctx, noprefix = false) {
    let site = await this.security.verifyToken(ctx)
    ctx.state.site = site

    let result = await this.formidable.uploadFile(ctx, ctx.state.site, noprefix)

    ctx.log.info(`Uploaded ${result.filename}`)

    let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${result.filename}`)
    this.filesCacheAdd(ctx.state.site, result.filename, stat.size)

    ctx.body = {
      filename: result.filename,
      path: `/${ctx.state.site}/${result.filename}`
    }
  }

  uploadNoPrefix(ctx) {
    return this.upload(ctx, true)
  }

  async resizeFile(ctx, sourceFile) {
    this.security.verifyBody(ctx)

    let keys = Object.keys(ctx.req.body)

    let allowedOperations = [
      'trim',
      'flatten',
      'resize',
      'blur',
      'extend',
    ]

    await Promise.all(keys.map(key => {
      return Promise.resolve()
      .then(async () => {
        let item = ctx.req.body[key]
        let sharp = this.sharp(`${config.get('uploadFolder')}/${ctx.state.site}/${sourceFile}`)
          .rotate()
  
        for (let operation of allowedOperations) {
          if (item[operation] != null) {
            sharp = sharp[operation](item[operation])
          }
        }
        sharp = sharp[item.format](item[item.format])
  
        let target = sourceFile
        if (path.extname(target).length > 0) {
          target = target.slice(0, -path.extname(target).length)
        }
        target += `_${key}.${item.format.replace('jpeg', 'jpg')}`
  
        if (item.out === 'base64') {
          let buffer = await sharp.toBuffer()
          ctx.body[key] = {
            base64: `data:image/${item.format};base64,` + buffer.toString('base64'),
          }
          return
        }
        await sharp.toFile(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`)
  
        let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`)
        this.filesCacheAdd(ctx.state.site, target, stat.size)
  
        ctx.body[key] = {
          filename: target,
          path: `/${ctx.state.site}/${target}`,
        }
      }).then(
        function() {},
        function(err) {
          throw new HttpError(422, `Error processing ${key}: ${err.message}`)
        }
      )
    }))
  }

  async resizeExisting(ctx) {
    let site = await this.security.verifyToken(ctx)
    ctx.state.site = site
    
    ctx.body = {}
    await this.resizeFile(ctx, ctx.params.filename)
  }

  async resize(ctx) {
    await this.upload(ctx)

    await this.resizeFile(ctx, ctx.body.filename)
  }

  async remove(ctx) {
    let site = await this.security.verifyToken(ctx)

    this.filesCacheRemove(site, ctx.params.filename)

    let root = path.join(config.get('uploadFolder'), site)
    var unlinkPath = path.join(root, decodeURIComponent(ctx.params.filename))

    if (unlinkPath.indexOf(root) !== 0) {
      throw new HttpError(403, `Error removing ${unlinkPath}: Traversing folder is not allowed`)
    }

    await this.fs.unlink(unlinkPath)
      .catch(function(err) {
        throw new HttpError(422, `Error removing ${unlinkPath}: ${err.message}`)
      })

    ctx.status = 204
  }
}
