

function buildCaptcha(domElem, prefix) {
  var captcha_widget = document.createElement("div");

  captcha_widget.className = "livechan_captcha_inner";

  var outer = document.createElement("div");

  outer.className = "livechan_captcha";

  var text = document.createElement("div");
  text.textContent = "solve the captcha";
  captcha_widget.appendChild(text);

  var captcha_image = document.createElement("img");
  captcha_image.className = "livechan_captcha_image";
  var div = document.createElement("div");
  div.appendChild(captcha_image);
  captcha_widget.appendChild(div);

  var captcha_entry = document.createElement("input");
  captcha_entry.className = "livechan_captcha_input";
  var div = document.createElement("div");
  div.appendChild(captcha_entry);
  captcha_widget.appendChild(div);

  var captcha_submit = document.createElement("input");
  captcha_submit.setAttribute("type", "button");
  captcha_submit.value = "solve";
  var div = document.createElement("div");
  div.appendChild(captcha_submit);
  captcha_widget.appendChild(div);

  outer.appendChild(captcha_widget);
  domElem.appendChild(outer);

  return {
    widget: outer,
    button: captcha_submit,
    image: captcha_image,
    entry: captcha_entry,
    prefix: prefix,
  }
}

function Captcha(domElem, options, callback) {
  if (options) {
    this.options = options;
  } else {
    this.options = {};
  }

  this.prefix = options.prefix || "/";
  this.widget = buildCaptcha(domElem, this.prefix);
  var self = this;
  this.widget.button.addEventListener("click", function() { self.process(callback); });
}

Captcha.prototype.load = function() {
  var self = this;
  var xhr = new XMLHttpRequest();

  var url = location.protocol + "//" + location.host + this.prefix ;

  xhr.open('get', url +"captcha/new");
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {
      var jdata = JSON.parse(xhr.responseText);
      if ( jdata ) {
        self.setCaptchaId(jdata);
      }
    }
  }

  xhr.send();
}

/**
 * @brief set captcha id
 */
Captcha.prototype.setCaptchaId = function(data) {
  this.captcha_id = data.id;
  this.setImageUrl(data.url);
  this.show();
}

Captcha.prototype.setImageUrl = function(url) {
  this.widget.image.setAttribute("src", url);
}

/**
 * @brief process captcha form
 */
Captcha.prototype.process = function(callback) {
  console.log("process");
  console.log(this);
  if (this.captcha_id) {
    var solution = this.widget.entry.value;
    var self = this;
    callback(this.captcha_id, solution , function(solved) {
      if (solved) {
        // woot we solved it
        self.hide();
      } else {
        // TODO: inform user of bad solution
        self.load();
      }
    });
  } else {
    // TODO: inform user of no captcha entred
    self.load();
  }
}

/**
 * @brief show the captcha pane
 */
Captcha.prototype.show = function () {
  console.log("show captcha");
  var widget = this.widget.widget;
  if ( widget.style ) {
    widget.style.zIndex = 5;
    widget.style.visibility = "visible";
  } else {
    widget.style = {zIndex: 5, visibility: "visible"};
  }
}
/**
 * @brief hide the captcha pane
 */
Captcha.prototype.hide = function () {
  console.log("hide captcha");
  var widget = this.widget.widget;
  if ( widget.style ) {
    widget.style.zIndex = -1;
    widget.style.visibility = "hidden";
  } else {
    widget.style = {zIndex: -1, visibility: "hidden"};
  }
}

/**
 * build login widget
 * for captcha / mod login
 */
function buildLogin(domElem) {
  var widget = document.createElement("div");
  widget.className = "livechan_login_widget";
  widget.style.zIndex = -1;


  var mod_div = document.createElement("div");
  mod_div.className = "livechan_login";

  var mod_form = document.createElement("form");

  var mod_username = document.createElement("input");
  mod_form.appendChild(mod_username);
  mod_username.className = "livechan_login_username";

  var mod_password = document.createElement("input");
  mod_password.className = "livechan_login_password";
  mod_password.setAttribute("type", "password");
  mod_form.appendChild(mod_password);

  var mod_submit = document.createElement("input");
  mod_password.className = "livechan_login_submit";
  mod_submit.setAttribute("type", "submit");
  mod_submit.setAttribute("value", "login");
  mod_form.appendChild(mod_submit);
  mod_div.appendChild(mod_form);
  widget.appendChild(mod_div);
  domElem.appendChild(widget);
  return {
    widget: widget,
    mod: {
      form: mod_form,
      username: mod_username,
      password: mod_password,
      submit: mod_submit
    }
  }
}


function Login(domElem) {
  this._login = buildLogin(domElem);
}

Login.prototype.show = function() {
  var self = this;
  self._login.widget.style.zIndex = 5;
  console.log("show login widget");
}



/*
 *  @brief build livechan navbar
 *  @param domElem the root element to put the navbar in
 */
function LivechanNavbar(domElem) {

  this.navbar = document.createElement("div");
  this.navbar.className = 'livechan_navbar';

  var container = document.createElement("div");
  // channel name label
  var channelLabel = document.createElement("span");
  channelLabel.className = 'livechan_navbar_channel_label';

  this.channel = channelLabel;

  // mod indicator
  this.mod = document.createElement("span");
  this.mod.className = 'livechan_navbar_mod_indicator_inactive';

  // TODO: don't hardcode
  this.mod.textContent = "Anon";

  // usercounter
  this.status = document.createElement("span");
  this.status.className = 'livechan_navbar_status';

  container.appendChild(this.mod);
  container.appendChild(this.channel);
  container.appendChild(this.status);

  this.navbar.appendChild(container);

  domElem.appendChild(this.navbar);

}


/* @brief called when there is an "event" for the navbar */
LivechanNavbar.prototype.onLivechanEvent = function (evstr) {
  if ( evstr === "login:mod" ) {
    // set indicator
    this.mod.className = "livechan_mod_indicator_active";
    this.mod.textContent = "Moderator";
  } else if ( evstr === "login:admin" ) {
    this.mod.className = "livechan_mod_indicator_admin";
    this.mod.textContent = "Admin";
  }
}

/* @brief called when there is a notification for us */
LivechanNavbar.prototype.onLivechanNotify = function(evstr) {
  // do nothing for now
  // maybe have some indicator that shows number of messages unread?
}

/* @brief update online user counter */
LivechanNavbar.prototype.updateUsers = function(count) {
  this.updateStatus("Online: "+count);
}

/* @brief update status label */
LivechanNavbar.prototype.updateStatus = function(str) {
  this.status.textContent = str;
}

/* @brief set channel name */
LivechanNavbar.prototype.setChannel = function(str) {
  this.channel.textContent = str;
}

var modCommands = [
  // login command
  [/l(login)? (.*)/, function(m) {
    var chat = this;
    // mod login
      chat.modLogin(m[2]);
  },
   "login as user", "/l user:password",
  ],
  [/cp (\d+)/, function(m) {
    var chat = this;
    // permaban the fucker
    chat.modAction(3, 4, m[1], "CP", -1);
  },
   "handle illegal content", "/cp postnum",
  ],
  [/cnuke (\d+) (.*)/, function(m) {
    var chat = this;
    // channel ban + nuke files
    chat.modAction(2, 4, m[1], m[2], -1);
  },
   "channel level ban+nuke", "/cnuke postnum reason goes here",
  ],
  [/purge (\d+) (.*)/, function(m) {
    var chat = this;
    // channel ban + nuke files
    chat.modAction(2, 9, m[1], m[2], -1);
  },
   "channel level ban+nuke", "/cnuke postnum reason goes here",
  ],
  [/gnuke (\d+) (.*)/, function(m) {
    var chat = this;
    // global ban + nuke with reason
    chat.modAction(3, 4, m[1], m[2], -1);
  },
   "global ban+nuke", "/gnuke postnum reason goes here",
  ],
  [/gban (\d+) (.*)/, function(m) {
    var chat = this;
    // global ban with reason
    chat.modAction(3, 3, m[1], m[2], -1);
  },
   "global ban (no nuke)", "/gban postnum reason goes here",
  ],
  [/cban (\d+) (.*)/, function(m) {
    var chat = this;
    // channel ban with reason
    chat.modAction(2, 3, m[1], m[2], -1);
  },
   "channel level ban (no nuke)", "/cban postnum reason goes here",
  ],
  [/dpost (\d+)/, function(m) {
    var chat = this;
    // channel level delete post
    chat.modAction(1, 2, m[1]);
  },
   "delete post and file", "/dpost postnum",
  ],
  [/dfile (\d+)/, function(m) {
    var chat = this;
    // channel level delete file
    chat.modAction(1, 1, m[1]);
  },
   "delete just file", "/dpost postnum",
  ]
]


/*
 * @brief Build Notification widget
 * @param domElem root element to put widget in
 */
function buildNotifyPane(domElem) {
  var pane = document.createElement("div");
  pane.className = "livechan_notify_pane";
  domElem.appendChild(pane);
  return pane;
}

/*
 * @brief Livechan Notification system
 * @param domElem root element to put Notification Pane in.
 */
function LivechanNotify(domElem) {
  this.pane = buildNotifyPane(domElem);
}

/* @brief inform the user with a message */
LivechanNotify.prototype.inform = function(str) {
  // new Notify("livechan", {body: str}).show();
  var elem = document.createElement("div");
  elem.className = "livechan_notify_node";
  elem.textContent = Date.now() + ": " + str;
  this.pane.appendChild(elem);
  this.rollover();
}


/* @brief roll over old messages */
LivechanNotify.prototype.rollover = function() {
  while ( this.pane.childNodes.length > this.scrollback ) {
     this.pane.childNodes.removeChild(this.pane.childNodes[0]);
   }
}




/* @brief Creates a structure of html elements for the
 *        chat.
 *
 * @param domElem The element to be populated with the
 *        chat structure.
 * @param chatName The name of this chat
 * @return An object of references to the structure
 *         created.
 */
function buildChat(chat, domElem, channel) {
  channel = channel.toLowerCase();
  // build the navbar
  // see nav.js
  var navbar = new LivechanNavbar(domElem);

  // build the notification system
  // see notify.js
  var notify = new LivechanNotify(domElem);

  var output = document.createElement('div');
  output.className = 'livechan_chat_output';

  var input_left = document.createElement('div');
  input_left.className = 'livechan_chat_input_left';

  var input = document.createElement('form');
  input.className = 'livechan_chat_input';

  var name = document.createElement('input');
  name.className = 'livechan_chat_input_name';
  name.setAttribute('placeholder', 'Anonymous');

  var file = document.createElement('input');
  file.className = 'livechan_chat_input_file';
  file.setAttribute('type', 'file');
  file.setAttribute('value', 'upload');
  file.setAttribute('id', channel+'_input_file');


  var messageDiv = document.createElement('div');
  messageDiv.className = 'livechan_chat_input_message_div';

  var message = document.createElement('textarea');
  message.className = 'livechan_chat_input_message';

  var submit = document.createElement('input');
  submit.className = 'livechan_chat_input_submit';
  submit.setAttribute('type', 'submit');
  submit.setAttribute('value', 'send');
  var convobar = new ConvoBar(chat, domElem);
  input_left.appendChild(name);
  input_left.appendChild(convobar.subject);
  input_left.appendChild(file);
  input.appendChild(input_left);
  messageDiv.appendChild(message);
  input.appendChild(messageDiv);
  input.appendChild(submit);
  domElem.appendChild(output);
  domElem.appendChild(input);

  return {
    convobar : convobar,
    notify: notify,
    navbar: navbar,
    output: output,
    input: {
      subject: convobar.subject,
      form: input,
      message: message,
      name: name,
      submit: submit,
      file: file
    }
  };
}

function Connection(chat, ws, url) {
  this.ws = ws;
  this.chat = chat;
  this.url = url;
  this.ws.onmessage = function(ev) {
    chat.handleData(JSON.parse(ev.data));
  }
  this.ws.onclose = function() {
    self.ws = null;
  }
  var self = this;
  setInterval(function() {
    if (self.ws == null) {
      // try reconnecting
      initWebSocket(self.chat, self.url, self);
    }
  }, 5000);
}

Connection.prototype.ban = function(reason) {
  if (this.ws) {
    this.ws.close();
    this.ws.close = null;
    alert("You have been banned for the following reason: "+reason);
  }
}

Connection.prototype.send = function(obj) {
  /* Jsonify the object and send as string. */
  this.sendBinary(JSON.stringify(obj));
}

Connection.prototype.sendBinary = function(obj) {
  if (this.ws) {
    this.ws.send(obj);
  }
}

Connection.prototype.onclose = function(callback) {
  if(this.ws)
    this.ws.onclose = callback;
}

/* @brief Initializes the websocket connection.
 *
 * @param channel The channel to open a connection to.
 * @return A connection the the websocket.
 */
function initWebSocket(chat, url, connection) {
  var ws = null;
  if (window['WebSocket']) {
    try {
      ws = new WebSocket(url);
    } catch (ex) {
      ws = null;
    }
  }
  if (ws) {
    ws.onclose = function() {
      if (connection) {
        connection.ws = null;
      }
    };
    if (connection) {
      connection.ws = ws;
      connection.ws.onmessage = function(ev) {
        chat.handleData(JSON.parse(ev.data));
      }
      chat.clear();
      chat.handleMessage({Type: "post", PostMessage: "reconnecting..."});
      return connection;
    } else {
      return new Connection(chat, ws, url);
    }
  }
}

/* @brief Parses and returns a message div.
 *
 * @param data The message data to be parsed.
 * @return A dom element containing the message.
 */
function parse(text, rules, end_tag) {
  var output = document.createElement('div');
  var position = 0;
  var end_matched = false;
  if (end_tag) {
    var end_handler = function(m) {
      end_matched = true;
    }
    rules = [[end_tag, end_handler]].concat(rules);
  }
  do {
    var match = null;
    var match_pos = text.length;
    var handler = null;
    for (var i = 0; i < rules.length; i++) {
      rules[i][0].lastIndex = position;
      var result = rules[i][0].exec(text);
      if (result !== null && position <= result.index && result.index < match_pos) {
        match = result;
        match_pos = result.index;
        handler = rules[i][1];
      }
    }
    var unmatched_text = text.substring(position, match_pos);
    output.appendChild(document.createTextNode(unmatched_text));
    position = match_pos;
    if (match !== null) {
      position += match[0].length;
      output.appendChild(handler(match));
    }
  } while (match !== null && !end_matched);
  return output;
}

var messageRules = [
  [/>>([0-9a-f]+)/g, function(m) {
    var out = document.createElement('span');
    out.className = 'livechan_internallink';
    out.addEventListener('click', function() {
      var selected = document.getElementById('livechan_chat_'+m[1]);
      console.log(selected.convo);
      selected.select();
      selected.scrollIntoView(true);
      // TODO: highlight
    });
    out.appendChild(document.createTextNode('>>'+m[1]));
    return out;
  }],
  [/^>.+/mg, function(m) {
    var out = document.createElement('span');
    out.className = 'livechan_greentext';
    out.appendChild(document.createTextNode(m));
    return out;
  }],
  [/\[code\]\n?([\s\S]+)\[\/code\]/g, function(m) {
    var out;
    if (m.length >= 2 && m[1].trim !== '') {
      out = document.createElement('pre');
      out.textContent = m[1];
    } else {
      out = document.createTextNode(m);
    }
    return out;
  }],
  [/\[b\]\n?([\s\S]+)\[\/b\]/g, function(m) {
    var out;
    if (m.length >= 2 && m[1].trim !== '') {
      out = document.createElement('span');
      out.className = 'livechan_boldtext';
      out.textContent = m[1];
    } else {
      out = document.createTextNode(m);
    }
    return out;
  }],
  [/\[spoiler\]\n?([\s\S]+)\[\/spoiler\]/g, function(m) {
    var out;
    if ( m.length >= 2 && m[1].trim !== '') {
      out = document.createElement('span');
      out.className = 'livechan_spoiler';
      out.textContent = m[1];
    } else {
      out = document.createTextNode(m);
    }
    return out;
  }],
  [/\r?\n/g, function(m) {
    return document.createElement('br');
  }],
  [/==(.*)==/g, function(m) {
    var out;
    out = document.createElement("span");
    out.className = "livechan_redtext";
    out.textContent = m[1];
    return out;
  }],
  [/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/g, function(m) {
    var out = document.createElement("a");
    out.href = m[1];
    out.textContent = m[1];
    return out;
  }],
]

/* @brief build the convorsation bar's elements
*/
function buildConvoBar(domElem) {
  var elem = document.createElement("div");
  elem.className = "livechan_convobar_root";

  var subject = document.createElement("input");
  subject.className = "livechan_chat_input_convo";
  elem.appendChild(subject);
  domElem.appendChild(elem);

  return {
    subject: subject,
    widget: elem,
  }
}

/* @brief create the chat's convorsation bar
 * @param domElem the element to place everything in
 */
function ConvoBar(chat, domElem) {
  this.parent = chat;
  this.holder = {};
  this.domElem = domElem;
  var convo = buildConvoBar(domElem);
  this.widget = convo.widget;
  this.subject = convo.subject;
  this.active = null;
  this._num = 0;
}

ConvoBar.prototype.count = function() {
  return this._num;
}

/* @brief remove convorstation given root message-id */
ConvoBar.prototype.removeConvo = function(msgid) {
  var self = this;
  console.log("remove "+msgid);
  var c = self.holder[msgid];
  if (c) {
    var e = document.getElementById("livechan_convobar_item_"+c.id);
    if (e) e.remove();
    for(var idx = 0; idx < c.posts.length; idx ++ ) {
      var id = "livechan_chat_"+c.posts[idx].HashShort;
      var child = document.getElementById(id);
      if(child) child.remove();
    }

    delete self.holder[msgid];
    self._num -- ;
  }
}

/* @brief update the convo bar
 * @param convoId the name of this covnorsattion
 */
ConvoBar.prototype.update = function(msgid, data) {
  var self = this;
  if ( ! self.holder[msgid]) {
    // new convo
    // register convo
    self.registerConvo(msgid, data);
  }
  // bump existing convo
  var convoId = self.holder[msgid].id;
  var convoElem = document.getElementById("livechan_convobar_item_"+convoId);
  var convoParent = convoElem.parentElement;
  if ( convoParent.children.length > 1 ) {
    convoParent.removeChild(convoElem);
    convoParent.insertBefore(convoElem, convoParent.childNodes[0]);
  }
  // add post to convo
  self.holder[msgid].posts.push(data);
  // do roll over
  var scrollback = self.parent.options.scrollback || 30;
  while(self.holder[msgid].posts.length > scrollback) {
    // remove oldest from convo tracker
    var child_data = self.holder[msgid].posts.shift();
    var child = document.getElementById("livechan_chat_"+child_data.ShortHash);
    if(child) {
      child.remove();
    }
  }
  // lol quartic time
  while(self.count() > 10 ) {
    var minid = -1;
    var minmsgid = null;
    for( var i in self.holder ) {
      if (minid == -1 || self.holder[i].id < minid) {
        minid = self.holder[i].id;
        minmsgid = i;
      }
    }
    if(minmsgid)
      self.removeConvo(minmsgid);
  }
}



/** @brief register a new convorsation
 */
ConvoBar.prototype.registerConvo = function(msgid, data) {
  var self = this;
  var max_id = 0;
  // get the highest convo id
  for ( c in self.holder ) {
    var id = self.holder[c].id;
    if (id > max_id ) {
      max_id = id
    }
  }

  self.holder[msgid] = {
    subject: data.PostSubject,
    msgid: data.Message_id,
    id: max_id + 1,
    posts: [],
    group: data.Newsgroup,
    ShortHash: data.ShortHash,
    select: function() {
      console.log("selected convo "+msgid);
      if ( self.active !== msgid ) {
        self.show(msgid);
      }
    },
  }
  // make a new entry in the convo bar
  var elem = document.createElement("div");
  elem.className = "livechan_convobar_item";
  elem.setAttribute("id", "livechan_convobar_item_"+ self.holder[msgid].id);
  var link = document.createElement("span");
  elem.addEventListener("click", function() { self.show(msgid); });
  link.appendChild(document.createTextNode(data.PostSubject));
  elem.appendChild(link);
  // prepend the element
  if (self.widget.children.length > 0 ) {
    self.widget.insertBefore(elem, self.widget.childNodes[0]);
  } else {
    self.widget.appendChild(elem);
  }
  self._num ++;
}

/* @brief Only Show chats from a convorsation
 */
ConvoBar.prototype.show = function(msgid) {
  var self = this;
  var sheet = null;
  for(var idx = 0; idx < document.styleSheets.length; idx++ ) {
    var s = document.styleSheets[idx];
    if (s.ownerNode && s.ownerNode.id === "convo_filter") {
      sheet = s;
      break;
    }
  }
  var rules = null;
  // delete all filtering rules
  if (sheet.rules) {
    rules = sheet.rules;
  } else {
    rules = sheet.cssRules;
  }
  while (rules.length > 0 ) {
    if (sheet.deleteRule) {
      sheet.deleteRule(0);
    } else if (sheet.removeRule) {
      sheet.removeRule(0);
    } else {
      break;
    }
  }

  if (msgid === self.active) {
    // this is resetting the view
    if (sheet.insertRule) {  // firefox
      sheet.insertRule(".livechan_chat_output_chat {  display: block; }", 0);
    } else if (sheet.addRule) { // not firefox
      sheet.addRule(".livechan_chat_output_chat", "display: block");
    }
    // unset active highlight
    var convoId = self.holder[self.active].id;
    var itemElem = document.getElementById("livechan_convobar_item_"+convoId);
    itemElem.style.background = null;
    self.active = null;
  } else {
    // unset active highlight if it's there
    if (self.active) {
      var convoId = self.holder[self.active].id;
      var itemElem = document.getElementById("livechan_convobar_item_"+convoId);
      itemElem.style.background = null;
    }
    // set active highlight to new element
    convoId = self.holder[msgid].id;
    itemElem = document.getElementById("livechan_convobar_item_"+convoId);
    itemElem.style.background = "red";
    var elemClass = ".livechan_chat_convo_" + convoId;
    if (sheet.insertRule) {  // firefox
      sheet.insertRule(elemClass+ " { display: block; }", 0);
      sheet.insertRule(".livechan_chat_output_chat {  display: none; }", 0);
    } else if (sheet.addRule) { // not firefox
      sheet.addRule(".livechan_chat_output_chat", "display: none");
      sheet.addRule(elemClass, "display: block");
    }
    // this convo is now active
    self.active = msgid;
  }
  // scroll view
  self.parent.scroll();
}

/* @brief Creates a chat.
 *
 * @param domElem The element to populate with chat
 *        output div and input form.
 * @param channel The channel to bind the chat to.
 *
 * @param options Channel Specific options
 */
function Chat(domElem, channel, options) {
  var self = this;
  this.pph = 0;
  this.name = channel.toLowerCase();
  this.domElem = domElem;
  this.lastOp = null;
  if (options) {
    this.options = options;
  } else {
    this.options = {};
  }


  this.chatElems = buildChat(this, this.domElem, this.name);
  this.prefix = this.options.prefix || "/";
  var scheme = "wss://";
  if (location.protocol == "http:") scheme = "ws://";
  var url = scheme + location.host + this.prefix + "live?"+ this.name;
  this.connection = initWebSocket(this, url);
  this.initOutput();
  this.initInput();
  // set navbar channel name
  this.chatElems.navbar.setChannel(this.name);
  // create captcha
  this.captcha = new Captcha(this.domElem, this.options, function(id, solution, callback) {
    // send captcha solution
    var ajax = new XMLHttpRequest();
    ajax.open("POST", self.prefix+"livechan/api/captcha");
    ajax.onreadystatechange = function() {
      if(ajax.readyState == 4) {
        var result = JSON.parse(ajax.responseText);
        callback(result && result.success);
      }
    }
    ajax.send(JSON.stringify({ID: id, Solution: solution}));
  });
  this.captcha.hide();
  setInterval(function() {
    self.tickPPHCount();
    self.tickUserCount();
  }, 5000);
}

Chat.prototype.clear = function () {
  for ( var convo in this.chatElems.convobar.holder ) {
    this.chatElems.convobar.removeConvo(convo);
  }
}

/**
 * @brief begin login sequence
 */
Chat.prototype.login = function() {
  this.captcha.load();
}

/**
 * @brief do mod login
 */
Chat.prototype.modLogin = function(str) {
  var self = this;
  self.connection.send({ModLogin: str});
}

Chat.prototype.modAction = function(scope, action, postID, reason, expire) {
  var self = this;
  self.connection.send({
    ModReason: reason,
    ModScope: parseInt(scope),
    ModAction: parseInt(action),
    ModPostID: parseInt(postID),
    ModExpire: parseInt(expire),
  });
}

/* @brief called when our post got mentioned
 *
 * @param event the event that has this mention
 */
Chat.prototype.Mentioned = function(event, chat) {
  var self = this;
  self.notify("mentioned: "+chat);
}

Chat.prototype.onNotifyShow = function () {

}


Chat.prototype.readImage = function (elem, callback) {
  var self = this;

  if (elem.files.length > 0 ) {
    var reader = new FileReader();
    var file = elem.files[0];
    reader.onloadend = function(ev) {
      if (ev.target.readyState == FileReader.DONE) {
        callback(window.btoa(ev.target.result), file.name, file.type);
      }
    }
    reader.readAsBinaryString(file);
  } else {
    callback(null, null, null);
  }
}

/* @brief Sends the message in the form.
 *
 * @param event The event causing a message to be sent.
 */
Chat.prototype.sendInput = function(event) {
  var inputElem = this.chatElems.input;
  var connection = this.connection;
  var self = this;

  if (inputElem.message.value[0] == '/') {
    var inp = inputElem.message.value;
    var helpRegex = /(help)? (.*)/;
    var helpMatch = helpRegex.exec(inp.slice(1));
    if (helpMatch) {

    }
    if ( self.options.customCommands ) {
      for (var i in self.options.customCommands) {
        var regexPair = self.options.customCommands[i];
        var match = regexPair[0].exec(inp.slice(1));
        if (match) {
          (regexPair[1]).call(self, match);
          inputElem.message.value = '';
        }
      }
    }
    // modCommands is defined in mod.js
    for ( var i in modCommands ) {
      var command = modCommands[i];
      var match = command[0].exec(inp.slice(1));
      if (match) {
        (command[1]).call(self, match);
        // don't clear input for mod command
      }
    }
    event.preventDefault();
    return false;
  }
  if (inputElem.submit.disabled == false) {
    var message = inputElem.message.value;
    var name = inputElem.name.value;
    var convo = self.chatElems.convobar.active;
    var board;
    if(convo)
      board = self.chatElems.convobar.holder[convo].group;
    if (!board) board = "overchan.live";
    console.log(board);
    var subject = self.chatElems.input.subject.value;
    var ajax = new XMLHttpRequest();
    ajax.open("POST", self.prefix+"livechan/api/post?newsgroup="+board, true);
    ajax.onreadystatechange = function() {
      if (ajax.readyState == 4) {
        console.log("post done");
        // responded
        var jdata = JSON.parse(ajax.responseText);
        if(!jdata) {
          // error parsing
          console.log("parse error: data="+ajax.responseText);
        } else if(jdata.captcha) {
          // we need to fill out captcha
          self.login();
        } else if (jdata.message_id) {
          // we posted
          console.log("post success:" +jdata.message_id);
          if (!convo) {
            self.lastOp = jdata.message_id;
          }
          // reset shit
          inputElem.file.value = "";
          inputElem.message.value = '';
        } else if (jdata.error) {
          console.log(jdata.error);
        }
      } else if (ajax.readyState == 3 ) {
        // processing
        console.log("post processing");
      } else if (ajax.readyState == 2 ) {
        // sent
        console.log("post sent");
      }
    }
    var data = new FormData();
    data.append("name", name);
    data.append("subject", subject);
    data.append("message", message);
    if (convo)
      data.append("reference", convo);
    if (inputElem.file.files[0])
      data.append("attachment_0", inputElem.file.files[0]);
    ajax.send(data);
    /**
    self.readImage(inputElem.file, function(fdata, fname, ftype) {
      if (fdata) {
        connection.send({Type: "post", Post: {
          message: message,
          subject: subject,
          name: name,
          reference: convo,
          newsgroup: board,
          files: [{name: fname, data: fdata, type: ftype}],
        }});
      } else {
        connection.send({Type: "post", Post: {
          subject: subject,
          message: message,
          reference: convo,
          name: name,
          newsgroup: board,
        }});
      }
      inputElem.file.value = "";
      inputElem.message.value = '';
    });
    */
    inputElem.submit.disabled = true;
    var i = parseInt(self.options.cooldown);
    // fallback
    if ( i == NaN ) { i = 4; }
    inputElem.submit.setAttribute('value', i);
    var countDown = setInterval(function(){
      inputElem.submit.setAttribute('value', --i);
    }, 1000);
    setTimeout(function(){
      clearInterval(countDown);
      inputElem.submit.disabled = false;
      inputElem.submit.setAttribute('value', 'send');
    }, i * 1000);
    event.preventDefault();
    return false;
  }
}

/* @brief Binds the form submission to websockets.
 */
Chat.prototype.initInput = function() {
  var inputElem = this.chatElems.input;
  var connection = this.connection;
  var self = this;
  inputElem.form.addEventListener('submit', function(event) {
    self.sendInput(event);
  });

  inputElem.message.addEventListener('keydown', function(event) {
    /* If enter key. */
    if (event.keyCode === 13 && !event.shiftKey) {
      self.sendInput(event);
    }
  });
  inputElem.message.focus();
}


/* @brief show a notification to the user */
Chat.prototype.notify = function(message) {
  // show notification pane
  this.showNotifyPane();

  var notifyPane = this.chatElems.notify;

  notifyPane.inform(message);
}

/* @brief show the notification pane */
Chat.prototype.showNotifyPane = function () {
  var pane = this.chatElems.notify.pane;
  pane.style.zIndex = 5;
  pane.style.visibility = 'visible';
}

/* @brief hide the notification pane */
Chat.prototype.showNotifyPane = function () {
  var pane = this.chatElems.notify.pane;
  pane.style.zIndex = -1;
  pane.style.visibility = 'hidden';
}

Chat.prototype.error = function(message) {
  var self = this;
  console.log("error: "+message);
  self.notify("an error has occured: "+message);
}

/** handle inbound websocket message */
Chat.prototype.handleMessage = function (data) {
  var self = this;

  var mtype = data.Type.toLowerCase();

  if (mtype == "captcha" ) {
    if (self.captcha_callback) {
      // captcha reply
      self.captcha_callback(data.Success);
      // reset
      self.captcha_callback = null;
    } else {
      // captcha challenge
      self.login();
    }
  } else if (mtype == "post" ) {
    self.insertChat(self.generateChat(data), data);
  } else if (mtype == "count" ) {
    self.chatElems.navbar.updateUsers(data.UserCount);
  } else if (mtype == "ban" ) {
    self.connection.ban(data.Reason);
  } else if (mtype == "error") {
      self.insertChat(self.generateChat({PostMessage: data.Error, PostSubject: "Server Error", PostName: "Server"}));
    console.log("server error: "+data.Error);
  } else {
    console.log("unknown message type "+mtype);
  }
}


Chat.prototype.handleData = function(data) {
  var self = this;
  if( Object.prototype.toString.call(data) === '[object Array]' ) {
    for (var i = 0; i < data.length; i++) {
      self.handleMessage(data[i]);
    }
  } else {
    self.handleMessage(data);
  }
}

/* @brief Binds messages to be displayed to the output.
 */
Chat.prototype.initOutput = function() {
  var outputElem = this.chatElems.output;
  var connection = this.connection;
  var self = this;
}

Chat.prototype.tickUserCount = function () {
  var self=this;
  var ajax = new XMLHttpRequest();
  ajax.open("GET", self.prefix + "livechan/api/online");
  ajax.onreadystatechange = function () {
    if (ajax.readyState == 4 && ajax.status == 200 ) {
      var data = JSON.parse(ajax.responseText);
      if (data && data.online) {
        self.updateUserStats(data.online);
      }
    }
  }
  ajax.send();
}

Chat.prototype.tickPPHCount = function () {
  var self=this;
  var convo = self.chatElems.convobar.active;
  var board;
  if(convo)
    board = self.chatElems.convobar.holder[convo].group;
  if(!board) {
    // no board selected?
    var h = document.location.hash;
    if (h.length > 1 ) {
      board = "overchan." + h.substr(1);
    }
  }
  if(board) {
    var ajax = new XMLHttpRequest();
    ajax.open("GET", self.prefix + "livechan/api/pph?newsgroup="+board);
    ajax.onreadystatechange = function () {
      if (ajax.readyState == 4 && ajax.status == 200 ) {
        var data = JSON.parse(ajax.responseText);
        if (data && data.pph !== undefined) {
          self.pph = data.pph;
        }
      }
    }
    ajax.send();
  }
}

/* @brief update the user counter for number of users online
 */
Chat.prototype.updateUserStats = function(count) {
  var elem = this.chatElems.navbar.status;
  elem.textContent = "Online: "+count + " PPH: "+ this.pph;
}


/* @brief Scrolls the chat to the bottom.
 */
Chat.prototype.scroll = function() {
  this.chatElems.output.scrollTop = this.chatElems.output.scrollHeight;
}

/** @brief roll over old posts, remove them from ui */
Chat.prototype.rollover = function() {
  /*
  var self = this;
  var chatSize = self.options.scrollback || 50;
  var e = self.chatElems.output;
  while ( e.childNodes.length > chatSize ) {
    e.childNodes[0].remove();
  }
*/
}

/* @brief Inserts the chat into the DOM, overwriting if need be.
 *
 * @TODO: Actually scan and insert appropriately for varying numbers.
 *
 * @param outputElem The dom element to insert the chat into.
 * @param chat The dom element to be inserted.
 * @param number The number of the chat to keep it in order.
 */
Chat.prototype.insertChat = function(chat, data) {
  //var number = data.Count;
  //var convo = data.Convo;
  //if (!number) {
  //  this.error("Error: invalid chat number.");
  //}
  var self = this;
  // append to main output
  var outputElem = this.chatElems.output;
  outputElem.appendChild(chat);
  // scroll to end
  self.scroll();
  self.rollover();
  // show new thread
  if (self.lastOp) {
    self.chatElems.convobar.show(self.lastOp);
    self.lastOp = null;
  }
}


/* @brief Generates a chat div.
 *
 * @param data Data passed in via websocket.
 * @return A dom element.
 */
Chat.prototype.generateChat = function(data) {
  var self = this;

  var chat = document.createElement('div');
  self.chatElems.convobar.update(data.Parent, data);
  var convo = self.chatElems.convobar.holder[data.Parent];
  chat.select = function() {
    console.log("selecting...");
    convo.select();
  }
  chat.className = 'livechan_chat_output_chat livechan_chat_convo_' + convo.id;
  var convoLabel = document.createElement('span');
  convoLabel.className = 'livechan_convo_label';
  convoLabel.appendChild(document.createTextNode(data.PostSubject));

  var header = document.createElement('div');
  header.className = 'livechan_chat_output_header';
  var name = document.createElement('span');
  name.className = 'livechan_chat_output_name';
  var trip = document.createElement('span');
  trip.className = 'livechan_chat_output_trip';
  var date = document.createElement('span');
  date.className = 'livechan_chat_output_date';
  var count = document.createElement('span');
  count.className = 'livechan_chat_output_count';

  var body = document.createElement('div');
  body.className = 'livechan_chat_output_body';
  var message = document.createElement('div');
  message.className = 'livechan_chat_output_message';


  if (data.PostName) {
    name.appendChild(document.createTextNode(data.PostName));
  } else {
    name.appendChild(document.createTextNode('Anonymous'));
  }

  if (data.Files) {
    for (var idx = 0 ; idx < data.Files.length ; idx ++ ) {
      var file = data.Files[idx];
      if(!file) continue;
      var a = document.createElement('a');
      a.setAttribute('target', '_blank');
      // TODO: make these configurable
      var filepath = file.Path;
      var thumb_url = self.options.prefix + 'thm/'+filepath + ".jpg";
      var src_url = self.options.prefix + 'img/'+filepath;

      a.setAttribute('href',src_url);
      var fl = filepath.toLowerCase();
      var img = document.createElement('img');
      img.setAttribute('src', thumb_url);
      img.className = 'livechan_image_thumb';
      a.appendChild(img);
      message.appendChild(a);
      img.onload = function() { self.scroll(); }

      img.addEventListener('mouseover', function () {

        var e = document.createElement("div");
        e.setAttribute("id", "hover_"+data.ShortHash);
        e.setAttribute("class", "hover");

        if (fl.match(/\.(webm|mp4|mkv)$/)) {
          // video
          var v = document.createElement("video");
          v.src = src_url;
          e.appendChild(v);
        } else if (fl.match(/\.(mp3|ogg|oga|flac|opus)$/)) {
          // audio
          var a = document.createElement("audio");
          a.src = src_url;
          e.appendChild(a);
        } else if (fl.match(/\.txt$/)) {
          //
        } else {
          // image
          var i = document.createElement("img");
          i.src = src_url;
          e.appendChild(i);
        }
        chat.appendChild(e);
      });
      img.addEventListener('mouseout', function () {
        // unload image
        var e = document.getElementById("hover_"+data.ShortHash);
        e.parentElement.removeChild(e);
      });
    }
  }

  /* Note that parse does everything here.  If you want to change
   * how things are rendered modify messageRules. */
  if (data.PostMessage) {
    message.appendChild(parse(data.PostMessage, messageRules));
  } else {
    message.appendChild(document.createTextNode(''));
  }

  if (data.Posted) {
    date.appendChild(document.createTextNode((new Date(data.Posted * 1000)).toLocaleString()));
  }

  if (data.Tripcode) {
    var et = document.createElement('span');
    et.innerHTML = data.Tripcode;
    trip.appendChild(et);
  }

  if (data.HashShort) {
    var h = data.HashShort;
    chat.setAttribute('id', 'livechan_chat_'+h);
    count.appendChild(document.createTextNode(h));
    count.addEventListener('click', function() {
      self.chatElems.input.message.value += '>>'+h+'\n';
      self.chatElems.input.message.focus();
      chat.select();
    });
  }

  header.appendChild(name);
  header.appendChild(trip);
  header.appendChild(date);
  header.appendChild(convoLabel);
  header.appendChild(count);
  body.appendChild(message);

  chat.appendChild(header);
  chat.appendChild(body);
  return chat;
};
