/**
 * overlayimage.js - w3m image element for blessed
 * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
 * https://github.com/chjj/blessed
 */

/**
 * Modules
 */
const fs = require('fs'),
  cp = require('child_process')
const helpers = require('../tools/helpers.js')
const Node = require('./node.js')
const Box = require('./box.js')

/**
 * OverlayImage
 * Good example of w3mimgdisplay commands:
 * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py
 */
function OverlayImage(options = {}) {
  const self = this
  if (!(this instanceof Node)) return new OverlayImage(options)
  Box.call(this, options)
  if (options.w3m) {
    OverlayImage.w3mdisplay = options.w3m
  }
  if (OverlayImage.hasW3MDisplay == null) {
    if (fs.existsSync(OverlayImage.w3mdisplay)) {
      OverlayImage.hasW3MDisplay = true
    } else if (options.search !== false) {
      const file = helpers.findFile('/usr', 'w3mimgdisplay')
        || helpers.findFile('/lib', 'w3mimgdisplay')
        || helpers.findFile('/bin', 'w3mimgdisplay')
      if (file) {
        OverlayImage.hasW3MDisplay = true
        OverlayImage.w3mdisplay = file
      } else {
        OverlayImage.hasW3MDisplay = false
      }
    }
  }
  this.on('hide', function () {
    self._lastFile = self.file
    self.clearImage()
  })
  this.on('show', function () {
    if (!self._lastFile) return
    self.setImage(self._lastFile)
  })
  this.on('detach', function () {
    self._lastFile = self.file
    self.clearImage()
  })
  this.on('attach', function () {
    if (!self._lastFile) return
    self.setImage(self._lastFile)
  })
  this.onScreenEvent('resize', function () {
    self._needsRatio = true
  })

  // Get images to overlap properly. Maybe not worth it:
  // this.onScreenEvent('render', function() {
  //   self.screen.program.flush();
  //   if (!self._noImage) return;
  //   function display(el, next) {
  //     if (el.type === 'w3mimage' && el.file) {
  //       el.setImage(el.file, next);
  //     } else {
  //       next();
  //     }
  //   }
  //   function done(el) {
  //     el.sub.forEach(recurse);
  //   }
  //   function recurse(el) {
  //     display(el, function() {
  //       var pending = el.sub.length;
  //       el.sub.forEach(function(el) {
  //         display(el, function() {
  //           if (!--pending) done(el);
  //         });
  //       });
  //     });
  //   }
  //   recurse(self.screen);
  // });
  this.onScreenEvent('render', function () {
    self.screen.program.flush()
    if (!self._noImage) {
      self.setImage(self.file)
    }
  })
  if (this.options.file || this.options.img) {
    this.setImage(this.options.file || this.options.img)
  }
}

OverlayImage.prototype.__proto__ = Box.prototype

OverlayImage.prototype.type = 'overlayimage'

OverlayImage.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay'

OverlayImage.prototype.spawn = function (file, args, opt, callback) {
  const spawn = require('child_process').spawn
  let ps

  opt = opt || {}
  ps = spawn(file, args, opt)

  ps.on('error', function (err) {
    if (!callback) return
    return callback(err)
  })

  ps.on('exit', function (code) {
    if (!callback) return
    if (code !== 0) return callback(new Error('Exit Code: ' + code))
    return callback(null, code === 0)
  })

  return ps
}

OverlayImage.prototype.setImage = function (img, callback) {
  const self = this
  if (this._settingImage) {
    this._queue = this._queue || []
    this._queue.push([ img, callback ])
    return
  }
  this._settingImage = true

  const reset = function () {
    self._settingImage = false
    self._queue = self._queue || []
    const item = self._queue.shift()
    if (item) {
      self.setImage(item[0], item[1])
    }
  }
  if (OverlayImage.hasW3MDisplay === false) {
    reset()
    if (!callback) return
    return callback(new Error('W3M Image Display not available.'))
  }
  if (!img) {
    reset()
    if (!callback) return
    return callback(new Error('No image.'))
  }
  this.file = img

  return this.getPixelRatio(function (err, ratio) {
    if (err) {
      reset()
      if (!callback) return
      return callback(err)
    }

    return self.renderImage(img, ratio, function (err, success) {
      if (err) {
        reset()
        if (!callback) return
        return callback(err)
      }
      if (self.shrink || self.options.autofit) {
        delete self.shrink
        delete self.options.shrink
        self.options.autofit = true
        return self.imageSize(function (err, size) {
          if (err) {
            reset()
            if (!callback) return
            return callback(err)
          }
          if (self._lastSize
            && ratio.tw === self._lastSize.tw
            && ratio.th === self._lastSize.th
            && size.width === self._lastSize.width
            && size.height === self._lastSize.height
            && self.aleft === self._lastSize.aleft
            && self.atop === self._lastSize.atop) {
            reset()
            if (!callback) return
            return callback(null, success)
          }

          self._lastSize = {
            tw: ratio.tw,
            th: ratio.th,
            width: size.width,
            height: size.height,
            aleft: self.aleft,
            atop: self.atop
          }

          self.position.width = size.width / ratio.tw | 0
          self.position.height = size.height / ratio.th | 0

          self._noImage = true
          self.screen.render()
          self._noImage = false

          reset()
          return self.renderImage(img, ratio, callback)
        })
      }

      reset()
      if (!callback) return
      return callback(null, success)
    })
  })
}

OverlayImage.prototype.renderImage = function (img, ratio, callback) {
  const self = this
  if (cp.execSync) {
    callback = callback || function (err, result) { return result }
    try {
      return callback(null, this.renderImageSync(img, ratio))
    } catch (e) {
      return callback(e)
    }
  }
  if (OverlayImage.hasW3MDisplay === false) {
    if (!callback) return
    return callback(new Error('W3M Image Display not available.'))
  }
  if (!ratio) {
    if (!callback) return
    return callback(new Error('No ratio.'))
  }

  // clearImage unsets these:
  const _file = self.file
  const _lastSize = self._lastSize
  return self.clearImage(function (err) {
    if (err) return callback(err)

    self.file = _file
    self._lastSize = _lastSize

    const opt = {
      stdio: 'pipe',
      env: process.env,
      cwd: process.env.HOME
    }

    const ps = self.spawn(OverlayImage.w3mdisplay, [], opt, function (err, success) {
      if (!callback) return
      return err
        ? callback(err)
        : callback(null, success)
    })

    const width = self.width * ratio.tw | 0,
      height = self.height * ratio.th | 0,
      aleft = self.aleft * ratio.tw | 0,
      atop = self.atop * ratio.th | 0

    const input = '0;1;'
      + aleft + ';'
      + atop + ';'
      + width + ';'
      + height + ';;;;;'
      + img
      + '\n4;\n3;\n'

    self._props = {
      aleft: aleft,
      atop: atop,
      width: width,
      height: height
    }

    ps.stdin.write(input)
    ps.stdin.end()
  })
}

OverlayImage.prototype.clearImage = function (callback) {
  if (cp.execSync) {
    callback = callback || function (err, result) { return result }
    try {
      return callback(null, this.clearImageSync())
    } catch (e) {
      return callback(e)
    }
  }
  if (OverlayImage.hasW3MDisplay === false) {
    if (!callback) return
    return callback(new Error('W3M Image Display not available.'))
  }
  if (!this._props) {
    if (!callback) return
    return callback(null)
  }

  const opt = {
    stdio: 'pipe',
    env: process.env,
    cwd: process.env.HOME
  }

  const ps = this.spawn(OverlayImage.w3mdisplay, [], opt, function (err, success) {
    if (!callback) return
    return err
      ? callback(err)
      : callback(null, success)
  })

  let width = this._props.width + 2,
    height = this._props.height + 2,
    aleft = this._props.aleft,
    atop = this._props.atop
  if (this._drag) {
    aleft -= 10
    atop -= 10
    width += 10
    height += 10
  }

  const input = '6;'
    + aleft + ';'
    + atop + ';'
    + width + ';'
    + height
    + '\n4;\n3;\n'

  delete this.file
  delete this._props
  delete this._lastSize

  ps.stdin.write(input)
  ps.stdin.end()
}

OverlayImage.prototype.imageSize = function (callback) {
  const img = this.file
  if (cp.execSync) {
    callback = callback || function (err, result) { return result }
    try {
      return callback(null, this.imageSizeSync())
    } catch (e) {
      return callback(e)
    }
  }
  if (OverlayImage.hasW3MDisplay === false) {
    if (!callback) return
    return callback(new Error('W3M Image Display not available.'))
  }
  if (!img) {
    if (!callback) return
    return callback(new Error('No image.'))
  }

  const opt = {
    stdio: 'pipe',
    env: process.env,
    cwd: process.env.HOME
  }

  const ps = this.spawn(OverlayImage.w3mdisplay, [], opt)

  let buf = ''

  ps.stdout.setEncoding('utf8')

  ps.stdout.on('data', function (data) {
    buf += data
  })

  ps.on('error', function (err) {
    if (!callback) return
    return callback(err)
  })

  ps.on('exit', function () {
    if (!callback) return
    const size = buf.trim().split(/\s+/)
    return callback(null, {
      raw: buf.trim(),
      width: +size[0],
      height: +size[1]
    })
  })

  const input = '5;' + img + '\n'

  ps.stdin.write(input)
  ps.stdin.end()
}

OverlayImage.prototype.termSize = function (callback) {
  const self = this
  if (cp.execSync) {
    callback = callback || function (err, result) { return result }
    try {
      return callback(null, this.termSizeSync())
    } catch (e) {
      return callback(e)
    }
  }
  if (OverlayImage.hasW3MDisplay === false) {
    if (!callback) return
    return callback(new Error('W3M Image Display not available.'))
  }

  const opt = {
    stdio: 'pipe',
    env: process.env,
    cwd: process.env.HOME
  }

  const ps = this.spawn(OverlayImage.w3mdisplay, [ '-test' ], opt)

  let buf = ''

  ps.stdout.setEncoding('utf8')

  ps.stdout.on('data', function (data) {
    buf += data
  })

  ps.on('error', function (err) {
    if (!callback) return
    return callback(err)
  })

  ps.on('exit', function () {
    if (!callback) return
    if (!buf.trim()) {
      // Bug: w3mimgdisplay will sometimes
      // output nothing. Try again:
      return self.termSize(callback)
    }

    const size = buf.trim().split(/\s+/)

    return callback(null, {
      raw: buf.trim(),
      width: +size[0],
      height: +size[1]
    })
  })

  ps.stdin.end()
}

OverlayImage.prototype.getPixelRatio = function (callback) {
  const self = this
  if (cp.execSync) {
    callback = callback || function (err, result) { return result }
    try {
      return callback(null, this.getPixelRatioSync())
    } catch (e) {
      return callback(e)
    }
  }

  // XXX We could cache this, but sometimes it's better
  // to recalculate to be pixel perfect.
  if (this._ratio && !this._needsRatio) {
    return callback(null, this._ratio)
  }

  return this.termSize(function (err, dimensions) {
    if (err) return callback(err)

    self._ratio = {
      tw: dimensions.width / self.screen.width,
      th: dimensions.height / self.screen.height
    }

    self._needsRatio = false

    return callback(null, self._ratio)
  })
}

OverlayImage.prototype.renderImageSync = function (img, ratio) {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.')
  }
  if (!ratio) {
    throw new Error('No ratio.')
  }

  // clearImage unsets these:
  const _file = this.file
  const _lastSize = this._lastSize
  this.clearImageSync()
  this.file = _file
  this._lastSize = _lastSize

  const width = this.width * ratio.tw | 0,
    height = this.height * ratio.th | 0,
    aleft = this.aleft * ratio.tw | 0,
    atop = this.atop * ratio.th | 0

  const input = '0;1;'
    + aleft + ';'
    + atop + ';'
    + width + ';'
    + height + ';;;;;'
    + img
    + '\n4;\n3;\n'
  this._props = {
    aleft: aleft,
    atop: atop,
    width: width,
    height: height
  }

  try {
    cp.execFileSync(OverlayImage.w3mdisplay, [], {
      env: process.env,
      encoding: 'utf8',
      input: input,
      timeout: 1000
    })
  } catch (e) {
  }

  return true
}

OverlayImage.prototype.clearImageSync = function () {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.')
  }
  if (!this._props) {
    return false
  }

  let width = this._props.width + 2,
    height = this._props.height + 2,
    aleft = this._props.aleft,
    atop = this._props.atop
  if (this._drag) {
    aleft -= 10
    atop -= 10
    width += 10
    height += 10
  }

  const input = '6;'
    + aleft + ';'
    + atop + ';'
    + width + ';'
    + height
    + '\n4;\n3;\n'

  delete this.file
  delete this._props
  delete this._lastSize

  try {
    cp.execFileSync(OverlayImage.w3mdisplay, [], {
      env: process.env,
      encoding: 'utf8',
      input: input,
      timeout: 1000
    })
  } catch (e) {
  }

  return true
}

OverlayImage.prototype.imageSizeSync = function () {
  const img = this.file
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.')
  }
  if (!img) {
    throw new Error('No image.')
  }

  let buf = ''
  const input = '5;' + img + '\n'

  try {
    buf = cp.execFileSync(OverlayImage.w3mdisplay, [], {
      env: process.env,
      encoding: 'utf8',
      input: input,
      timeout: 1000
    })
  } catch (e) {
  }

  const size = buf.trim().split(/\s+/)

  return {
    raw: buf.trim(),
    width: +size[0],
    height: +size[1]
  }
}

OverlayImage.prototype.termSizeSync = function (_, recurse) {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.')
  }

  let buf = ''

  try {
    buf = cp.execFileSync(OverlayImage.w3mdisplay, [ '-test' ], {
      env: process.env,
      encoding: 'utf8',
      timeout: 1000
    })
  } catch (e) {
  }
  if (!buf.trim()) {
    // Bug: w3mimgdisplay will sometimes
    // output nothing. Try again:
    recurse = recurse || 0
    if (++recurse === 5) {
      throw new Error('Term size not determined.')
    }
    return this.termSizeSync(_, recurse)
  }

  const size = buf.trim().split(/\s+/)

  return {
    raw: buf.trim(),
    width: +size[0],
    height: +size[1]
  }
}

OverlayImage.prototype.getPixelRatioSync = function () {
  // XXX We could cache this, but sometimes it's better
  // to recalculate to be pixel perfect.
  if (this._ratio && !this._needsRatio) {
    return this._ratio
  }
  this._needsRatio = false

  const dimensions = this.termSizeSync()
  this._ratio = {
    tw: dimensions.width / this.screen.width,
    th: dimensions.height / this.screen.height
  }

  return this._ratio
}

OverlayImage.prototype.displayImage = function (callback) {
  return this.screen.displayImage(this.file, callback)
}

/**
 * Expose
 */

module.exports = OverlayImage
