const pickle = require('chromium-pickle-js')
const path = require('path')
const UINT64 = require('cuint').UINT64
const fs = require('fs')

/* global WIKI */

/**
 * Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar)
 * by Fenglin Li (https://github.com/toyobayashi)
 */

const packages = {
  'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)
}

module.exports = {
  fdCache: {},
  async serve (pkgName, req, res, next) {
    const file = this.readFilesystemSync(packages[pkgName])
    const { filesystem, fd } = file
    const info = filesystem.getFile(req.path.substring(1))
    if (info) {
      res.set({
        'Content-Type': 'image/svg+xml',
        'Content-Length': info.size
      })

      fs.createReadStream('', {
        fd,
        autoClose: false,
        start: 8 + filesystem.headerSize + parseInt(info.offset, 10),
        end: 8 + filesystem.headerSize + parseInt(info.offset, 10) + info.size - 1
      }).on('error', (err) => {
        WIKI.logger.warn(err)
        res.sendStatus(404)
      }).pipe(res.status(200))
    } else {
      res.sendStatus(404)
    }
  },
  async unload () {
    const fds = Object.values(this.fdCache)
    if (fds.length > 0) {
      WIKI.logger.info('Closing ASAR file descriptors...')
      const closeAsync = require('util').promisify(fs.close)
      await Promise.all(fds.map(x => closeAsync(x.fd)))
      this.fdCache = {}
    }
  },
  readArchiveHeaderSync (fd) {
    let size
    let headerBuf

    const sizeBuf = Buffer.alloc(8)
    if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) {
      throw new Error('Unable to read header size')
    }

    const sizePickle = pickle.createFromBuffer(sizeBuf)
    size = sizePickle.createIterator().readUInt32()
    headerBuf = Buffer.alloc(size)
    if (fs.readSync(fd, headerBuf, 0, size, null) !== size) {
      throw new Error('Unable to read header')
    }

    const headerPickle = pickle.createFromBuffer(headerBuf)
    const header = headerPickle.createIterator().readString()
    return { header: JSON.parse(header), headerSize: size }
  },
  readFilesystemSync (archive) {
    if (!this.fdCache[archive]) {
      const fd = fs.openSync(archive, 'r')
      const header = this.readArchiveHeaderSync(fd)
      const filesystem = new Filesystem(archive)
      filesystem.header = header.header
      filesystem.headerSize = header.headerSize
      this.fdCache[archive] = {
        fd,
        filesystem: filesystem
      }
    }

    return this.fdCache[archive]
  }
}

class Filesystem {
  constructor (src) {
    this.src = path.resolve(src)
    this.header = { files: {} }
    this.offset = UINT64(0)
  }

  searchNodeFromDirectory (p) {
    let json = this.header
    const dirs = p.split(path.sep)
    for (const dir of dirs) {
      if (dir !== '.') {
        json = json.files[dir]
      }
    }
    return json
  }

  getNode (p) {
    const node = this.searchNodeFromDirectory(path.dirname(p))
    const name = path.basename(p)
    if (name) {
      return node.files[name]
    } else {
      return node
    }
  }

  getFile (p, followLinks) {
    followLinks = typeof followLinks === 'undefined' ? true : followLinks
    const info = this.getNode(p)

    if (!info) {
      return false
    }

    if (info.link && followLinks) {
      return this.getFile(info.link)
    } else {
      return info
    }
  }
}
