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

/**
 * Modules
 */

var fs = require('fs'),
  cp = require('child_process');

var helpers = require('../helpers');

var Node = require('./node');
var Box = require('./box');

/**
 * Interfaces
 */

interface OverlayImageOptions {
  w3m?: string;
  search?: boolean;
  file?: string;
  img?: string;
  shrink?: boolean;
  autofit?: boolean;
  [key: string]: any;
}

interface ImageSize {
  raw: string;
  width: number;
  height: number;
}

interface PixelRatio {
  tw: number;
  th: number;
}

interface ImageProps {
  aleft: number;
  atop: number;
  width: number;
  height: number;
}

interface LastSize {
  tw: number;
  th: number;
  width: number;
  height: number;
  aleft: number;
  atop: number;
}

interface OverlayImageScreen {
  width: number;
  height: number;
  program: {
    flush(): void;
  };
  render(): void;
  displayImage(file: string, callback?: Function): any;
}

interface OverlayImagePosition {
  width?: number;
  height?: number;
  [key: string]: any;
}

interface OverlayImageInterface extends Box {
  type: string;
  file?: string;
  _lastFile?: string;
  _needsRatio?: boolean;
  _noImage?: boolean;
  _settingImage?: boolean;
  _queue?: [string, Function?][];
  _props?: ImageProps;
  _lastSize?: LastSize;
  _ratio?: PixelRatio;
  _drag?: boolean;
  screen: OverlayImageScreen;
  options: OverlayImageOptions;
  position: OverlayImagePosition;
  width: number;
  height: number;
  aleft: number;
  atop: number;
  shrink?: boolean;

  // Methods
  spawn(file: string, args: string[], opt?: any, callback?: Function): any;
  setImage(img: string, callback?: Function): any;
  renderImage(img: string, ratio: PixelRatio, callback?: Function): any;
  clearImage(callback?: Function): any;
  imageSize(callback?: Function): any;
  termSize(callback?: Function): any;
  getPixelRatio(callback?: Function): any;
  renderImageSync(img: string, ratio: PixelRatio): boolean;
  clearImageSync(): boolean;
  imageSizeSync(): ImageSize;
  termSizeSync(unused?: any, recurse?: number): ImageSize;
  getPixelRatioSync(): PixelRatio;
  displayImage(callback?: Function): any;
  onScreenEvent(event: string, listener: Function): void;
}

/**
 * OverlayImage
 * Good example of w3mimgdisplay commands:
 * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py
 */

function OverlayImage(
  this: OverlayImageInterface,
  options?: OverlayImageOptions
) {
  var self = this;

  if (!(this instanceof Node)) {
    return new OverlayImage(options);
  }

  options = 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) {
      var 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.children.forEach(recurse);
  //   }
  //   function recurse(el) {
  //     display(el, function() {
  //       var pending = el.children.length;
  //       el.children.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 (
  this: OverlayImageInterface,
  file: string,
  args: string[],
  opt?: any,
  callback?: Function
) {
  var spawn = require('child_process').spawn,
    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 (
  this: OverlayImageInterface,
  img: string,
  callback?: Function
) {
  var self = this;

  if (this._settingImage) {
    this._queue = this._queue || [];
    this._queue.push([img, callback]);
    return;
  }
  this._settingImage = true;

  var reset = function () {
    self._settingImage = false;
    self._queue = self._queue || [];
    var 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 (
  this: OverlayImageInterface,
  img: string,
  ratio: PixelRatio,
  callback?: Function
) {
  var 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:
  var _file = self.file;
  var _lastSize = self._lastSize;
  return self.clearImage(function (err) {
    if (err) return callback(err);

    self.file = _file;
    self._lastSize = _lastSize;

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

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

    var width = (self.width * ratio.tw) | 0,
      height = (self.height * ratio.th) | 0,
      aleft = (self.aleft * ratio.tw) | 0,
      atop = (self.atop * ratio.th) | 0;

    var 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 (
  this: OverlayImageInterface,
  callback?: Function
) {
  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);
  }

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

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

  var 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;
  }

  var 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 (
  this: OverlayImageInterface,
  callback?: Function
) {
  var 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.'));
  }

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

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

  var 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;
    var size = buf.trim().split(/\s+/);
    return callback(null, {
      raw: buf.trim(),
      width: +size[0],
      height: +size[1],
    });
  });

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

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

OverlayImage.prototype.termSize = function (
  this: OverlayImageInterface,
  callback?: Function
) {
  var 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.'));
  }

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

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

  var 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);
    }

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

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

  ps.stdin.end();
};

OverlayImage.prototype.getPixelRatio = function (
  this: OverlayImageInterface,
  callback?: Function
) {
  var 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 (
  this: OverlayImageInterface,
  img: string,
  ratio: PixelRatio
): boolean {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.');
  }

  if (!ratio) {
    throw new Error('No ratio.');
  }

  // clearImage unsets these:
  var _file = this.file;
  var _lastSize = this._lastSize;

  this.clearImageSync();

  this.file = _file;
  this._lastSize = _lastSize;

  var width = (this.width * ratio.tw) | 0,
    height = (this.height * ratio.th) | 0,
    aleft = (this.aleft * ratio.tw) | 0,
    atop = (this.atop * ratio.th) | 0;

  var 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 (
  this: OverlayImageInterface
): boolean {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.');
  }

  if (!this._props) {
    return false;
  }

  var 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;
  }

  var 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 (
  this: OverlayImageInterface
): ImageSize {
  var img = this.file;

  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.');
  }

  if (!img) {
    throw new Error('No image.');
  }

  var buf = '';
  var input = '5;' + img + '\n';

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

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

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

OverlayImage.prototype.termSizeSync = function (
  this: OverlayImageInterface,
  _?: any,
  recurse?: number
): ImageSize {
  if (OverlayImage.hasW3MDisplay === false) {
    throw new Error('W3M Image Display not available.');
  }

  var 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);
  }

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

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

OverlayImage.prototype.getPixelRatioSync = function (
  this: OverlayImageInterface
): PixelRatio {
  // 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;

  var dimensions = this.termSizeSync();

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

  return this._ratio;
};

OverlayImage.prototype.displayImage = function (
  this: OverlayImageInterface,
  callback?: Function
) {
  return this.screen.displayImage(this.file, callback);
};

/**
 * Expose
 */

module.exports = OverlayImage;
