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

/**
 * Modules
 */

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

var nextTick = global.setImmediate || process.nextTick.bind(process);

var Node = require('./node');
var Input = require('./input');

/**
 * Interfaces
 */

interface TextareaOptions {
  scrollable?: boolean;
  value?: string;
  inputOnFocus?: boolean;
  keys?: boolean;
  vi?: boolean;
  mouse?: boolean;
  [key: string]: any;
}

interface CursorPosition {
  x: number;
  y: number;
}

interface KeyData {
  name: string;
  ctrl?: boolean;
  shift?: boolean;
  meta?: boolean;
}

interface MouseData {
  button?: string;
  x: number;
  y: number;
}

interface TextareaScreen {
  focused: any;
  program: {
    x: number;
    y: number;
    showCursor(): void;
    hideCursor(): void;
    cuf(n: number): void;
    cub(n: number): void;
    cud(n: number): void;
    cuu(n: number): void;
    cup(y: number, x: number): void;
  };
  grabKeys: boolean;
  fullUnicode: boolean;
  saveFocus(): void;
  restoreFocus(): void;
  rewindFocus(): void;
  render(): void;
  readEditor(options: { value: string }, callback: Function): any;
  _listenKeys(element: any): void;
}

interface TextareaCoords {
  xi: number;
  xl: number;
  yi: number;
  yl: number;
}

interface WrapResult {
  real: string[];
  fake: string[];
  rtof: number[];
  length: number;
}

interface TextareaCLines {
  real: string[];
  fake: string[];
  rtof: number[];
  length: number;
}

interface TextareaInterface extends Input {
  type: string;
  screen: TextareaScreen;
  options: TextareaOptions;
  offsetY: number;
  offsetX: number;
  value: string;
  _value?: string;
  _reading?: boolean;
  _callback?: Function;
  _done?: Function;
  __listener?: Function;
  __done?: Function;
  __updateCursor: Function;
  lpos?: TextareaCoords;
  _clines: TextareaCLines;
  childBase?: number;
  iheight: number;
  itop: number;
  ileft: number;
  iwidth: number;
  width: number;
  
  // Methods
  getCursor(): CursorPosition;
  setCursor(x: number, y: number): void;
  moveCursor(x: number, y: number): void;
  _updateCursor(get?: boolean): void;
  input(callback?: Function): any;
  setInput(callback?: Function): any;
  readInput(callback?: Function): any;
  _listener(ch: string, key: KeyData): void;
  _typeScroll(): void;
  getValue(): string;
  setValue(value?: string): void;
  clearInput(): any;
  clearValue(): any;
  submit(): any;
  cancel(): any;
  render(): any;
  editor(callback?: Function): any;
  setEditor(callback?: Function): any;
  readEditor(callback?: Function): any;
  _render(): any;
  _getCoords(): TextareaCoords | null;
  strWidth(str: string): number;
  setContent(content: string): void;
  setScroll(offset: number): void;
  _wrapContent(content: string, width: number): WrapResult;
  emit(event: string, ...args: any[]): any;
  on(event: string, listener: Function): void;
  removeListener(event: string, listener: Function): void;
  focus(): void;
}

/**
 * Textarea
 */

function Textarea(this: TextareaInterface, options?: TextareaOptions) {
  var self = this;

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

  options = options || {};

  options.scrollable = options.scrollable !== false;

  Input.call(this, options);

  this.screen._listenKeys(this);

  this.offsetY = 0;
  this.offsetX = 0;

  this.value = options.value || '';

  this.__updateCursor = this._updateCursor.bind(this);
  this.on('resize', this.__updateCursor);
  this.on('move', this.__updateCursor);

  if (options.inputOnFocus) {
    this.on('focus', this.readInput.bind(this, null));
  }

  if (!options.inputOnFocus && options.keys) {
    this.on('keypress', function(ch: string, key: KeyData) {
      if (self._reading) return;
      if (key.name === 'enter' || (options.vi && key.name === 'i')) {
        return self.readInput();
      }
      if (key.name === 'e') {
        return self.readEditor();
      }
    });
  }

  if (options.mouse) {
    this.on('click', function(data: MouseData) {
      if (self._reading) return;
      if (data.button !== 'right') return;
      self.readEditor();
    });
  }
}

Textarea.prototype.__proto__ = Input.prototype;

Textarea.prototype.type = 'textarea';

Textarea.prototype.getCursor = function(this: TextareaInterface): CursorPosition {
  return {x: this.offsetX, y: this.offsetY};
}

Textarea.prototype.setCursor = function(this: TextareaInterface, x: number, y: number): void {
  this.offsetX = x;
  this.offsetY = y;
}

Textarea.prototype.moveCursor = function(this: TextareaInterface, x: number, y: number): void {
  var prevLine = (this._clines.length - 1) + this.offsetY;
  let sync = false;
  if (y <= 0 && y > (this._clines.length * -1)){
    sync = this.offsetY !== y;
    this.offsetY = y;
  }
  var currentLine = (this._clines.length - 1) + this.offsetY;
  var currentText = this._clines[currentLine];

  if (sync){
    var prevText = this._clines[prevLine];
    var positionFromBegin = Math.max(this.strWidth(prevText) + this.offsetX, 0);
    x = (Math.max(0, this.strWidth(currentText) - positionFromBegin)) * -1;
  }
  if (x <= 0 && x >= (this.strWidth(currentText) * -1)){
    this.offsetX = x;
  }
  this._updateCursor(true);
  this.screen.render();
}

Textarea.prototype._updateCursor = function(this: TextareaInterface, get?: boolean): void {
  if (this.screen.focused !== this) {
    return;
  }

  var lpos = get ? this.lpos : this._getCoords();

  if (!lpos) return;

  const currentLine = (this._clines.length - 1) + this.offsetY
  var currentText = this._clines[currentLine]
    , program = this.screen.program
    , line
    , cx
    , cy;

  // Stop a situation where the textarea begins scrolling
  // and the last cline appears to always be empty from the
  // _typeScroll `+ '\n'` thing.
  // Maybe not necessary anymore?
  if (currentText === '' && this.value[this.value.length - 1] !== '\n') {
    //currentText = this._clines[currentLine - 1] || '';
  }

  line = Math.min(
    currentLine - (this.childBase || 0),
    (lpos.yl - lpos.yi) - this.iheight - 1);

  // When calling clearValue() on a full textarea with a border, the first
  // argument in the above Math.min call ends up being -2. Make sure we stay
  // positive.
  line = Math.max(0, line);

  cy = lpos.yi + this.itop + line;
  cx = this.offsetX + lpos.xi + this.ileft + this.strWidth(currentText);

  // XXX Not sure, but this may still sometimes
  // cause problems when leaving editor.
  if (cy === program.y && cx === program.x) {
    return;
  }

  if (cy === program.y) {
    if (cx > program.x) {
      program.cuf(cx - program.x);
    } else if (cx < program.x) {
      program.cub(program.x - cx);
    }
  } else if (cx === program.x) {
    if (cy > program.y) {
      program.cud(cy - program.y);
    } else if (cy < program.y) {
      program.cuu(program.y - cy);
    }
  } else {
    program.cup(cy, cx);
  }
};

Textarea.prototype.input =
Textarea.prototype.setInput =
Textarea.prototype.readInput = function(this: TextareaInterface, callback?: Function) {
  var self = this
    , focused = this.screen.focused === this;

  if (this._reading) return;
  this._reading = true;

  this._callback = callback;

  if (!focused) {
    this.screen.saveFocus();
    this.focus();
  }

  this.screen.grabKeys = true;

  this._updateCursor();
  this.screen.program.showCursor();
  //this.screen.program.sgr('normal');

  this._done = function fn(err?: any, value?: string) {
    if (!self._reading) return;

    if (fn.done) return;
    fn.done = true;

    self._reading = false;

    delete self._callback;
    delete self._done;

    self.removeListener('keypress', self.__listener);
    delete self.__listener;

    self.removeListener('blur', self.__done);
    delete self.__done;

    self.screen.program.hideCursor();
    self.screen.grabKeys = false;

    if (!focused) {
      self.screen.restoreFocus();
    }

    if (self.options.inputOnFocus) {
      self.screen.rewindFocus();
    }

    // Ugly
    if (err === 'stop') return;

    if (err) {
      self.emit('error', err);
    } else if (value != null) {
      self.emit('submit', value);
    } else {
      self.emit('cancel', value);
    }
    self.emit('action', value);

    if (!callback) return;

    return err
      ? callback(err)
      : callback(null, value);
  };

  // Put this in a nextTick so the current
  // key event doesn't trigger any keys input.
  nextTick(function() {
    self.__listener = self._listener.bind(self);
    self.on('keypress', self.__listener);
  });

  this.__done = this._done.bind(this, null, null);
  this.on('blur', this.__done);
};

Textarea.prototype._listener = function(this: TextareaInterface, ch: string, key: KeyData): void {
  var done = this._done
    , value = this.value;

  if (key.name === 'return') return;
  if (key.name === 'enter') {
    ch = '\n';
  }
  const cursor = this.getCursor();

  // TODO: Handle directional keys.
  if (key.name === 'left' || key.name === 'right'
      || key.name === 'up' || key.name === 'down'
      || key.name === 'end'|| key.name === 'home') {
      
        if (key.name === "left") {
          cursor.x--;
        } else if (key.name === "right") {
          cursor.x++;
        }
        if (key.name === "up") {
          cursor.y--;
        } else if (key.name === "down") {
          cursor.y++;
        }

        if (key.name === "end") {
          cursor.x = 0;
        } else if (key.name === "home") {
          const currentLine = (this._clines.length - 1) + this.offsetY
          const currentLineLength = this.strWidth(this._clines[currentLine] ?? '')
          cursor.x = -currentLineLength;
        }
      
        this.moveCursor(cursor.x, cursor.y);
  }

  if (this.options.keys && key.ctrl && key.name === 'e') {
    return this.readEditor();
  }

  // TODO: Optimize typing by writing directly
  // to the screen and screen buffer here.
  if (key.name === 'escape') {
    done(null, null);
  } else if (key.name === 'backspace') {
    if (this.value.length) {
      if (this.screen.fullUnicode) {
      } else {
        if (cursor.x === 0 && cursor.y === 0){
          this.value = this.value.slice(0, -1);
        } else {          

          const realLines = this._clines.real.slice();
          const fakeLines = this._clines.fake.slice();
          const mapper = this._clines.rtof;

          const currentLine = (realLines.length - 1) + cursor.y;

          const fakeLineIndex = mapper[currentLine];

          let fakeCursorPosition = 0; 
          for (let i = 0; i <= currentLine; i++) { 
            if (mapper[i] === fakeLineIndex) { 
              fakeCursorPosition += this.strWidth(realLines[i]); 
            } 
          } 
          fakeCursorPosition += cursor.x;
          
          let realCursorPosition =  this.strWidth(realLines[currentLine]) + cursor.x;

          if (fakeLines[fakeLineIndex] === ''){
            fakeLines.splice(fakeLineIndex, 1);
          } else if (cursor.x === -this.strWidth(realLines[currentLine])) {
            if (currentLine > 0){
              const lineLengthBefore = this.strWidth(realLines[currentLine - 1] ?? '')

              if (mapper[currentLine] !== mapper[currentLine - 1]){
                const currentLineString = fakeLines.splice(fakeLineIndex, 1);
                fakeLines[fakeLineIndex - 1] += currentLineString;
              } else {

              }

              const predict = this._wrapContent(fakeLines.join('\n'), this.width - this.iwidth)

              cursor.x = -(this.strWidth(predict[currentLine - 1] ?? '') - lineLengthBefore);
              if (predict.real.length === realLines.length){
                cursor.y--;
              }
            }
          } else {
            fakeLines[fakeLineIndex] = fakeLines[fakeLineIndex].slice(0, fakeCursorPosition - 1) + fakeLines[fakeLineIndex].slice(fakeCursorPosition);
            const predict = this._wrapContent(fakeLines.join('\n'), this.width - this.iwidth)
            cursor.x = -(this.strWidth(predict.real[currentLine]) - realCursorPosition + 1);
            if (predict.real.length !== realLines.length){
              cursor.y++;
            }
           
          }
          this.value = fakeLines.join('\n');
          this.setCursor(cursor.x, cursor.y);
        }
      }
    }
  } else if (key.name === 'delete') {
    if (this.value.length) {
      if (this.screen.fullUnicode) {
      } else {
        const currentLine = (this._clines.length - 1) + cursor.y
        if (cursor.x === 0 && cursor.y === 0){

        } else {
          const realLines = this._clines.real.slice();
          const fakeLines = this._clines.fake.slice();
          const mapper = this._clines.rtof;

          const currentLine = (realLines.length - 1) + cursor.y
          
          const fakeLineIndex = mapper[currentLine];
          
          let fakeCursorPosition = 0; 
          for (let i = 0; i <= currentLine; i++) { 
            if (mapper[i] === fakeLineIndex) { 
              fakeCursorPosition += this.strWidth(realLines[i]); 
            } 
          } 
          fakeCursorPosition += cursor.x;
          
          let realCursorPosition =  this.strWidth(realLines[currentLine]) + cursor.x;
          
          if (fakeLines[fakeLineIndex] === ''){
            const nextLineLength = this.strWidth(fakeLines[fakeLineIndex + 1] ?? '')
            fakeLines.splice(fakeLineIndex, 1);
            cursor.y++;
            cursor.x = -nextLineLength;
          } else {
            if (fakeLineIndex < fakeLines.length - 1){
              if (cursor.x === -this.strWidth(realLines[currentLine])) {
                fakeLines[fakeLineIndex] = fakeLines[fakeLineIndex].substring(1);
              } else{
                fakeLines[fakeLineIndex] = fakeLines[fakeLineIndex].slice(0, fakeCursorPosition) + fakeLines[fakeLineIndex].slice(fakeCursorPosition + 1);
              }
              const predict = this._wrapContent(fakeLines.join('\n'), this.width - this.iwidth)
              cursor.x = -(this.strWidth(predict.real[currentLine]) - realCursorPosition);
              if (predict.real.length !== realLines.length){
                cursor.y++;
              }
            }
          }
          this.value = fakeLines.join('\n');
          this.setCursor(cursor.x, cursor.y);          
        }
      }
    }
  } else if (ch) {
    if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
      if (cursor.x === 0 && cursor.y === 0){
        this.value += ch;
      } else if (cursor.x >= (this.value.length * -1)) {
        const realLines = this._clines.real.slice();
        const fakeLines = this._clines.fake.slice();
        const mapper = this._clines.rtof;

        const currentLine = (realLines.length - 1) + cursor.y
        
        const fakeLineIndex = mapper[currentLine];
        let fakeCursorPosition = 0; 
        for (let i = 0; i <= currentLine; i++) { 
          if (mapper[i] === fakeLineIndex) { 
            fakeCursorPosition += this.strWidth(realLines[i]); 
          } 
        } 
        fakeCursorPosition += cursor.x;
        
        fakeLines[fakeLineIndex] = fakeLines[fakeLineIndex].slice(0, fakeCursorPosition) + ch + fakeLines[fakeLineIndex].slice(fakeCursorPosition);

        const predict = this._wrapContent(fakeLines.join('\n'), this.width - this.iwidth)
        if (ch === '\n'){
          if (predict.real.length === realLines.length){
          cursor.y++;
          }
          cursor.x = -this.strWidth(predict[predict.length - 1 + cursor.y]);
        }
        
        this.value = fakeLines.join('\n');     
        this.setCursor(cursor.x, cursor.y);
      }
    }
  }

  if (this.value !== value) {
    this.screen.render();
  }
};

Textarea.prototype._typeScroll = function() {
  // XXX Workaround
  //var height = this.height - this.iheight;
  //if (this._clines.length - this.childBase > height) {
    const currentLine = (this._clines.length - 1) + this.offsetY;
    this.setScroll(currentLine);
  //}
};

Textarea.prototype.getValue = function(this: TextareaInterface): string {
  return this.value;
};

Textarea.prototype.setValue = function(this: TextareaInterface, value?: string): void {
  if (value == null) {
    value = this.value;
  }
  if (this._value !== value) {
    this.value = value;
    this._value = value;
    this.setContent(this.value);
    this._typeScroll();
    this._updateCursor();
  }
};

Textarea.prototype.clearInput =
Textarea.prototype.clearValue = function(this: TextareaInterface) {
  return this.setValue('');
};

Textarea.prototype.submit = function(this: TextareaInterface) {
  if (!this.__listener) return;
  return this.__listener('\x1b', { name: 'escape' });
};

Textarea.prototype.cancel = function(this: TextareaInterface) {
  if (!this.__listener) return;
  return this.__listener('\x1b', { name: 'escape' });
};

Textarea.prototype.render = function(this: TextareaInterface) {
  this.setValue();
  return this._render();
};

Textarea.prototype.editor =
Textarea.prototype.setEditor =
Textarea.prototype.readEditor = function(this: TextareaInterface, callback?: Function) {
  var self = this;

  if (this._reading) {
    var _cb = this._callback
      , cb = callback;

    this._done('stop');

    callback = function(err?: any, value?: string) {
      if (_cb) _cb(err, value);
      if (cb) cb(err, value);
    };
  }

  if (!callback) {
    callback = function() {};
  }

  return this.screen.readEditor({ value: this.value }, function(err?: any, value?: string) {
    if (err) {
      if (err.message === 'Unsuccessful.') {
        self.screen.render();
        return self.readInput(callback);
      }
      self.screen.render();
      self.readInput(callback);
      return callback(err);
    }
    self.setValue(value);
    self.screen.render();
    return callback(null, value);
  });
};

/**
 * Expose
 */

module.exports = Textarea;
