/* ------------------------------------------------------------------------*
 * Copyright 2013-2014 Arne F. Claassen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *-------------------------------------------------------------------------*/

var Josh = Josh || {};
Josh.Version = "0.2.10";
(function(root) {
  Josh.Keys = {
    Special: {
      Backspace: 8,
      Tab: 9,
      Enter: 13,
      Pause: 19,
      CapsLock: 20,
      Escape: 27,
      Space: 32,
      PageUp: 33,
      PageDown: 34,
      End: 35,
      Home: 36,
      Left: 37,
      Up: 38,
      Right: 39,
      Down: 40,
      Insert: 45,
      Delete: 46
    }
  };

  Josh.ReadLine = function(config) {
    config = config || {};

    // instance fields
    var _console = config.console || (Josh.Debug && root.console ? root.console : {
      log: function() {
      }
    });
    var _history = config.history || new Josh.History();
    var _killring = config.killring || new Josh.KillRing();
    var _boundToElement = config.element ? true : false;
    var _element = config.element || root;
    var _isInput = false;
    var _active = false;
    var _onActivate;
    var _onDeactivate;
    var _onCompletion;
    var _onEnter;
    var _onChange;
    var _onCancel;
    var _onEOT;
    var _onClear;
    var _onSearchStart;
    var _onSearchEnd;
    var _onSearchChange;
    var _inSearch = false;
    var _searchMatch;
    var _lastSearchText = '';
    var _text = '';
    var _cursor = 0;
    var _lastCmd;
    var _completionActive;
    var _cmdQueue = [];
    var _suspended = false;
    var _cmdMap = {
      complete: cmdComplete,
      done: cmdDone,
      noop: cmdNoOp,
      history_top: cmdHistoryTop,
      history_end: cmdHistoryEnd,
      history_next: cmdHistoryNext,
      history_previous: cmdHistoryPrev,
      end: cmdEnd,
      home: cmdHome,
      left: cmdLeft,
      right: cmdRight,
      cancel: cmdCancel,
      'delete': cmdDeleteChar,
      backspace: cmdBackspace,
      kill_eof: cmdKillToEOF,
      kill_wordback: cmdKillWordBackward,
      kill_wordforward: cmdKillWordForward,
      yank: cmdYank,
      clear: cmdClear,
      search: cmdReverseSearch,
      wordback: cmdBackwardWord,
      wordforward: cmdForwardWord,
      yank_rotate: cmdRotate
  };
    var _keyMap = {
      'default': {
        8: cmdBackspace,    // Backspace
        9: cmdComplete,     // Tab
        13: cmdDone,        // Enter
        27: cmdEsc,         // Esc
        33: cmdHistoryTop,  // Page Up
        34: cmdHistoryEnd,  // Page Down
        35: cmdEnd,         // End
        36: cmdHome,        // Home
        37: cmdLeft,        // Left
        38: cmdHistoryPrev, // Up
        39: cmdRight,       // Right
        40: cmdHistoryNext, // Down
        46: cmdDeleteChar,  // Delete
        10: cmdNoOp,        // Pause
        19: cmdNoOp,        // Caps Lock
        45: cmdNoOp         // Insert
      },
      control: {
        65: cmdHome,          // A
        66: cmdLeft,          // B
        67: cmdCancel,        // C
        68: cmdDeleteChar,    // D
        69: cmdEnd,           // E
        70: cmdRight,         // F
        80: cmdHistoryPrev,   // P
        78: cmdHistoryNext,   // N
        75: cmdKillToEOF,     // K
        89: cmdYank,          // Y
        76: cmdClear,         // L
        82: cmdReverseSearch  // R
      },
      meta: {
        8: cmdKillWordBackward, // Backspace
        66: cmdBackwardWord,    // B
        68: cmdKillWordForward, // D
        70: cmdForwardWord,     // F
        89: cmdRotate           // Y
      }
    };

    // public methods
    var self = {
      isActive: function() {
        return _active;
      },
      activate: function() {
        _active = true;
        if(_onActivate) {
          _onActivate();
        }
      },
      deactivate: function() {
        _active = false;
        if(_onDeactivate) {
          _onDeactivate();
        }
      },
      bind: function(key, action) {
        var k = getKey(key);
        var cmd = _cmdMap[action];
        if(!cmd) {
          return;
        }
        _keyMap[k.modifier][k.code] = cmd;
      },
      unbind: function(key) {
        var k = getKey(key);
        delete _keyMap[k.modifier][k.code];
      },
      attach: function(el) {
        if(_element) {
          self.detach();
        }
        _console.log("attaching");
        _console.log(el);
        _element = el;
        _boundToElement = true;
        _isInput = _element.nodeType == 1 && _element.tagName.toLowerCase() == 'input';
        addEvent(_element, "focus", self.activate);
        addEvent(_element, "blur", self.deactivate);
        subscribeToKeys();
      },
      detach: function() {
        removeEvent(_element, "focus", self.activate);
        removeEvent(_element, "blur", self.deactivate);
        _element = null;
        _boundToElement = false;
      },
      onActivate: function(completionHandler) {
        _onActivate = completionHandler;
      },
      onDeactivate: function(completionHandler) {
        _onDeactivate = completionHandler;
      },
      onChange: function(changeHandler) {
        _onChange = changeHandler;
      },
      onClear: function(completionHandler) {
        _onClear = completionHandler;
      },
      onEnter: function(enterHandler) {
        _onEnter = enterHandler;
      },
      onCompletion: function(completionHandler) {
        _onCompletion = completionHandler;
      },
      onCancel: function(completionHandler) {
        _onCancel = completionHandler;
      },
      onEOT: function(completionHandler) {
        _onEOT = completionHandler;
      },
      onSearchStart: function(completionHandler) {
        _onSearchStart = completionHandler;
      },
      onSearchEnd: function(completionHandler) {
        _onSearchEnd = completionHandler;
      },
      onSearchChange: function(completionHandler) {
        _onSearchChange = completionHandler;
      },
      getLine: function() {
        return {
          text: _text,
          cursor: _cursor
        };
      },
      setLine: function(line) {
        _text = line.text;
        _cursor = line.cursor;
        refresh();
      }
    };

    // private methods
    function inputSearchStart() {
        _console.log('binding for input search start');
        self.bind({keyCode: 8}, 'backspace');
        self.bind({keyCode: 35}, 'end');
        self.bind({keyCode: 36}, 'home');
        self.bind({keyCode: 37}, 'left');
        self.bind({keyCode: 39}, 'right');
        self.bind({keyCode: 46}, 'delete');
    }

    function inputSearchEnd() {
        _console.log('unbinding for input search end');
        self.unbind({keyCode: 8});
        self.unbind({keyCode: 35});
        self.unbind({keyCode: 36});
        self.unbind({keyCode: 37});
        self.unbind({keyCode: 39});
        self.unbind({keyCode: 46});
    }

    function addEvent(element, name, callback) {
      if(element.addEventListener) {
        element.addEventListener(name, callback, false);
      } else if(element.attachEvent) {
        element.attachEvent('on' + name, callback);
      }
    }

    function removeEvent(element, name, callback) {
      if(element.removeEventListener) {
        element.removeEventListener(name, callback, false);
      } else if(element.detachEvent) {
        element.detachEvent('on' + name, callback);
      }
    }

    function getKeyInfo(e) {
      var code = e.keyCode || e.charCode;
      var c = String.fromCharCode(code);
      return {
        code: code,
        character: c,
        shift: e.shiftKey,
        control: e.controlKey,
        alt: e.altKey,
        isChar: true
      };
    }

    function getKey(key) {
      var k = {
        modifier: 'default',
        code: key.keyCode
      };
      if(key.metaKey || key.altKey) {
        k.modifier = 'meta';
      } else if(key.ctrlKey) {
        k.modifier = 'control';
      }
      if(key['char']) {
        k.code = key['char'].charCodeAt(0);
      }
      return k;
    }

    function queue(cmd) {
      if(_suspended) {
        _cmdQueue.push(cmd);
        return;
      }
      call(cmd);
    }

    function call(cmd) {
      _console.log('calling: ' + cmd.name + ', previous: ' + _lastCmd);
      if(_inSearch && cmd.name != "cmdKeyPress" && cmd.name != "cmdReverseSearch") {
        _inSearch = false;
        if(cmd.name == 'cmdEsc') {
          _searchMatch = null;
        }
        if(_searchMatch) {
          if(_searchMatch.text) {
            _cursor = _searchMatch.cursoridx;
            _text = _searchMatch.text;
            _history.applySearch();
          }
          _searchMatch = null;
        }
        if(_onSearchEnd) {
          if (_isInput)
            inputSearchEnd();
          _onSearchEnd();
        }
      }
      if(!_inSearch && _killring.isinkill() && cmd.name.substr(0, 7) != 'cmdKill') {
        _killring.commit();
      }
      _lastCmd = cmd.name;
      cmd();
    }

    function suspend(asyncCall) {
      _suspended = true;
      asyncCall(resume);
    }

    function resume() {
      var cmd = _cmdQueue.shift();
      if(!cmd) {
        _suspended = false;
        return;
      }
      call(cmd);
      resume();
    }

    function cmdNoOp() {
      // no-op, used for keys we capture and ignore
    }

    function cmdEsc() {
      // no-op, only has an effect on reverse search and that action was taken in call()
    }

    function cmdBackspace() {
      if(_cursor == 0) {
        return;
      }
      --_cursor;
      _text = remove(_text, _cursor, _cursor + 1);
      refresh();
    }

    function cmdComplete() {
      if(!_onCompletion) {
        return;
      }
      suspend(function(resumeCallback) {
        _onCompletion(self.getLine(), function(completion) {
          if(completion) {
            _text = insert(_text, _cursor, completion);
            updateCursor(_cursor + completion.length);
          }
          _completionActive = true;
          resumeCallback();
        });
      });
    }

    function cmdDone() {
      if(!_text) {
        return;
      }
      var text = _text;
      _history.accept(text);
      _text = '';
      _cursor = 0;
      if(!_onEnter) {
        return;
      }
      suspend(function(resumeCallback) {
        _onEnter(text, function(text) {
          if(text) {
            _text = text;
            _cursor = _text.length;
          }
          refresh();
          resumeCallback();
        });
      });

    }

    function cmdEnd() {
      updateCursor(_text.length);
    }

    function cmdHome() {
      updateCursor(0);
    }

    function cmdLeft() {
      if(_cursor == 0) {
        return;
      }
      updateCursor(_cursor - 1);
    }

    function cmdRight() {
      if(_cursor == _text.length) {
        return;
      }
      updateCursor(_cursor + 1);
    }

    function cmdBackwardWord() {
      if(_cursor == 0) {
        return;
      }
      updateCursor(findBeginningOfPreviousWord());
    }

    function cmdForwardWord() {
      if(_cursor == _text.length) {
        return;
      }
      updateCursor(findEndOfCurrentWord());
    }

    function cmdHistoryPrev() {
      if(!_history.hasPrev()) {
        return;
      }
      getHistory(_history.prev);
    }

    function cmdHistoryNext() {
      if(!_history.hasNext()) {
        return;
      }
      getHistory(_history.next);
    }

    function cmdHistoryTop() {
      getHistory(_history.top);
    }

    function cmdHistoryEnd() {
      getHistory(_history.end);
    }

    function cmdDeleteChar() {
      if(_text.length == 0) {
        if(_onEOT) {
          _onEOT();
          return;
        }
      }
      if(_cursor == _text.length) {
        return;
      }
      _text = remove(_text, _cursor, _cursor + 1);
      refresh();
    }

    function cmdCancel() {
      if(_onCancel) {
        _onCancel();
      }
    }

    function cmdKillToEOF() {
      _killring.append(_text.substr(_cursor));
      _text = _text.substr(0, _cursor);
      refresh();
    }

    function cmdKillWordForward() {
      if(_text.length == 0) {
        return;
      }
      if(_cursor == _text.length) {
        return;
      }
      var end = findEndOfCurrentWord();
      if(end == _text.length - 1) {
        return cmdKillToEOF();
      }
      _killring.append(_text.substring(_cursor, end))
      _text = remove(_text, _cursor, end);
      refresh();
    }

    function cmdKillWordBackward() {
      if(_cursor == 0) {
        return;
      }
      var oldCursor = _cursor;
      _cursor = findBeginningOfPreviousWord();
      _killring.prepend(_text.substring(_cursor, oldCursor));
      _text = remove(_text, _cursor, oldCursor);
      refresh();
    }

    function cmdYank() {
      var yank = _killring.yank();
      if(!yank) {
        return;
      }
      _text = insert(_text, _cursor, yank);
      updateCursor(_cursor + yank.length);
    }

    function cmdRotate() {
      var lastyanklength = _killring.lastyanklength();
      if(!lastyanklength) {
        return;
      }
      var yank = _killring.rotate();
      if(!yank) {
        return;
      }
      var oldCursor = _cursor;
      _cursor = _cursor - lastyanklength;
      _text = remove(_text, _cursor, oldCursor);
      _text = insert(_text, _cursor, yank);
      updateCursor(_cursor + yank.length);
    }

    function cmdClear() {
      if(_onClear) {
        _onClear();
      } else {
        refresh();
      }
    }

    function cmdReverseSearch() {
      if(!_inSearch) {
        _inSearch = true;
        if(_onSearchStart) {
          if (_isInput)
            inputSearchStart();
          _onSearchStart();
        }
        if(_onSearchChange) {
          _onSearchChange({});
        }
      } else {
        if(!_searchMatch) {
          _searchMatch = {term: ''};
        }
        search();
      }
    }

    function updateCursor(position) {
      _cursor = position;
      refresh();
    }

    function addText(c) {
      _text = insert(_text, _cursor, c);
      ++_cursor;
      refresh();
    }

    function addSearchText(c) {
      if(!_searchMatch) {
        _searchMatch = {term: ''};
      }
      _searchMatch.term += c;
      search();
    }

    function search() {
      _console.log("searchtext: " + _searchMatch.term);
      var match = _history.search(_searchMatch.term);
      if(match != null) {
        _searchMatch = match;
        _console.log("match: " + match);
        if(_onSearchChange) {
          _onSearchChange(match);
        }
      }
    }

    function refresh(changed) {
      changed = (typeof changed == 'undefined') || changed;
      if(_onChange) {
        _onChange(self.getLine(), changed);
      }
    }

    function getHistory(historyCall) {
      _history.update(_text);
      _text = historyCall();
      updateCursor(_text.length);
    }

    function findBeginningOfPreviousWord() {
      var position = _cursor - 1;
      if(position < 0) {
        return 0;
      }
      var word = false;
      for(var i = position; i > 0; i--) {
        var word2 = isWordChar(_text[i]);
        if(word && !word2) {
          return i + 1;
        }
        word = word2;
      }
      return 0;
    }

    function findEndOfCurrentWord() {
      if(_text.length == 0) {
        return 0;
      }
      var position = _cursor + 1;
      if(position >= _text.length) {
        return _text.length - 1;
      }
      var word = false;
      for(var i = position; i < _text.length; i++) {
        var word2 = isWordChar(_text[i]);
        if(word && !word2) {
          return i;
        }
        word = word2;
      }
      return _text.length - 1;
    }

    function isWordChar(c) {
      if(c == undefined) {
        return false;
      }
      var code = c.charCodeAt(0);
      return (code >= 48 && code <= 57)
        || (code >= 65 && code <= 90)
        || (code >= 97 && code <= 122);
    }

    function remove(text, from, to) {
      if(text.length <= 1 || text.length <= to - from) {
        return '';
      }
      if(from == 0) {

        // delete leading characters
        return text.substr(to);
      }
      var left = text.substr(0, from);
      var right = text.substr(to);
      return left + right;
    }

    function insert(text, idx, ins) {
      if(idx == 0) {
        return ins + text;
      }
      if(idx >= text.length) {
        return text + ins;
      }
      var left = text.substr(0, idx);
      var right = text.substr(idx);
      return left + ins + right;
    }

    function getCaretPosition() {
        if (_element.createTextRange) {
            var range = _element.createTextRange();
            range.moveStart('character', -_element.value.length);
            return range.text.length;
        } else if (_element.selectionStart !== null) {
            return _element.selectionStart;
        }
        return 0;
    }

    function fromInput() {
        _text = _element.value;
        _cursor = getCaretPosition(_element);
        refresh(false);
    }

    function subscribeToKeys() {

      if (_isInput) {
        inputSearchEnd();
        
        // keep internal state sync'd with input
        _element.oninput = _element.onclick = fromInput;
      }

      // set up key capture
      _element.onkeydown = function(e) {
        e = e || window.event;

        // return as unhandled if we're not active or the key is just a modifier key
        if(!_active || e.keyCode == 16 || e.keyCode == 17 || e.keyCode == 18 || e.keyCode == 91) {
          return true;
        }

        // check for some special first keys, regardless of modifiers
        _console.log("key: " + e.keyCode);
        var cmd = _keyMap['default'][e.keyCode];
        // intercept ctrl- and meta- sequences (may override the non-modifier cmd captured above
        var mod;
        if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
          mod = _keyMap.control[e.keyCode];
          if(mod) {
            cmd = mod;
          }
        } else if((e.altKey || e.metaKey) && !e.ctrlKey && !e.shiftKey) {
          mod = _keyMap.meta[e.keyCode];
          if(mod) {
            cmd = mod;
          }
        }
        if(!cmd) {
          return true;
        }
        queue(cmd);
        e.preventDefault();
        e.stopPropagation();
        e.cancelBubble = true;
        return false;
      };

      _element.onkeypress = function(e) {
        if(!_active) {
          return true;
        }
        var key = getKeyInfo(e);
        if(key.code == 0 || e.defaultPrevented || e.metaKey || e.altKey || e.ctrlKey) {
          return true;
        }
        if (!_isInput) {
            queue(function cmdKeyPress() {
              if(_inSearch) {
                addSearchText(key.character);
              } else {
                addText(key.character);
              }
            });
            e.preventDefault();
            e.stopPropagation();
            e.cancelBubble = true;
            return false;
        } else if (_inSearch) {
            queue(function cmdKeyPress() {
              addSearchText(key.character);
            });
            e.preventDefault();
            e.stopPropagation();
            e.cancelBubble = true;
            return false;
        } else {
            fromInput();
        }
        return true;
      };
    }
    if(_boundToElement) {
      self.attach(_element);
    } else {
      subscribeToKeys();
    }
    return self;
  };
})(this);
