function inject(frag) {
	var dest = this.buffer;
	for (var i = 0; i < this.state[1]; i++)
		dest = dest.children('del:last');
	if (this.state[0] & S_BIG)
		dest = dest.children('h4:last');
	if (this.state[0] & S_QUOTE)
		dest = dest.children('em:last');
	if (this.strong)
		dest = dest.children('strong:last');
	if (this.sup_level) {
		for (var i = 0; i < this.sup_level; i++)
			dest = dest.children('sup:last');
	}
	var out = null;
	if (frag.safe) {
		var m = frag.safe.match(/^<(\w+)>$/);
		if (m)
			out = document.createElement(m[1]);
		else if (/^<\/\w+>$/.test(frag.safe))
			out = '';
	}
	if (out === null) {
		if (Array.isArray(frag))
			out = $(flatten(frag).join(''));
		else
			out = escape_fragment(frag);
	}
	if (out)
		dest.append(out);
	return out;
}

// TODO: Unify self-updates with OneeSama; this is redundant
oneeSama.hook('insertOwnPost', function (info) {
	if (!postForm || !info.links)
		return;
	postForm.buffer.find('.nope').each(function () {
		var $a = $(this);
		var text = $a.text();
		var m = text.match(/^>>(\d+)/);
		if (!m)
			return;
		var num = m[1], op = info.links[num];
		if (!op)
			return;
		var realRef = postForm.imouto.post_ref(num, op, false);
		var $ref = $(flatten([realRef]).join(''));
		$a.attr('href', $ref.attr('href')).removeAttr('class');
		var refText = $ref.text();
		if (refText != text)
			$a.text(refText);
	});
});

/* Mobile */
function touchable_spoiler_tag(del) {
	del.html = '<del onclick="void(0)">';
}
oneeSama.hook('spoilerTag', touchable_spoiler_tag);

function get_focus() {
	var $focus = $(window.getSelection().focusNode);
	if ($focus.is('blockquote'))
		return $focus.find('textarea');
}

function section_abbrev(section) {
	var stat = section.find('.omit');
	var m = stat.text().match(/(\d+)\D+(\d+)?/);
	if (!m)
		return false;
	return {stat: stat, omit: parseInt(m[1], 10),
			img: parseInt(m[2] || 0, 10)};
}

function shift_replies(section) {
	if (THREAD)
		return;
	var shown = section.children('article[id]:not(:has(form))');
	var rem = shown.length;
	if (rem < ABBREVIATED_REPLIES)
		return;
	var $stat, omit = 0, img = 0;
	var info = section_abbrev(section);
	if (info) {
		$stat = info.stat;
		omit = info.omit;
		img = info.img;
	}
	else {
		$stat = $('<span class="omit"></span>');
		section.children('blockquote,form').last().after($stat);
	}
	var omitsBefore = omit;
	for (var i = 0; i < shown.length; i++) {
		var cull = $(shown[i]);
		if (rem-- < ABBREVIATED_REPLIES)
			break;
		if (cull.has('figure').length)
			img++;
		omit++;
		cull.remove();
	}
	$stat.text(abbrev_msg(omit, img));
	if (omitsBefore <= THREAD_LAST_N && omit > THREAD_LAST_N) {
		var $expand = section.find('header .act');
		if ($expand.length == 1) {
			var num = extract_num(section);
			var $lastN = $(oneeSama.last_n_html(num));
			$expand.after(' ', $lastN);
		}
	}
}

function spill_page() {
	if (THREAD)
		return;
	/* Ugh, this could be smarter. */
	var ss = $('body > section[id]:visible');
	for (var i = THREADS_PER_PAGE; i < ss.length; i++)
		$(ss[i]).prev('hr').andSelf().hide();

}

var dispatcher = {};

dispatcher[PING] = function (msg) {};

dispatcher[INSERT_POST] = function (msg) {
	var orig_focus = get_focus();
	var num = msg[0];
	msg = msg[1];
	var isThread = !msg.op;
	if (isThread)
		syncs[num] = 1;
	msg.editing = true;
	msg.num = num;

	// did I create this post?
	var el;
	var nonce = msg.nonce;
	delete msg.nonce;
	var myNonce = get_nonces()[nonce];
	var bump = BUMP;
	var myTab = myNonce && myNonce.tab == TAB_ID;
	if (myTab) {
		// posted in this tab; transform placeholder
		ownPosts[num] = true;
		oneeSama.trigger('insertOwnPost', msg);
		postSM.feed('alloc', msg);
		bump = false;
		// delete only after a delay so all tabs notice that it's ours
		setTimeout(destroy_nonce.bind(null, nonce), 10*1000);
		// if we've already made a placeholder for this post, use it
		if (postForm && postForm.el)
			el = postForm.el;
	}

	/* This conflict is really dumb. */
	var links = oneeSama.links = msg.links;
	delete msg.links;

	// create model or fill existing shallow model
	var model;
	if (!isThread) {
		model = UnknownThread.get('replies').get(num);
		if (model) {
			UnknownThread.get('replies').remove(num);
			model.unset('shallow');
			model.set(msg);
		}
		else
			model = new Post(msg);
	}
	else {
		model = new Thread(msg);
	}

	if (myNonce) {
		model.set('mine', true);
		Mine.write(num, Mine.now());
	}

	// insert it into the DOM
	var $section, $hr;
	if (!isThread) {
		var article = new Article({model: model, id: num, el: el});
		if (!el)
			el = article.render().el;

		var thread = Threads.lookup(msg.op, msg.op);
		thread.get('replies').add(model);
		add_post_links(model, links, msg.op);

		$section = $('#' + msg.op);
		shift_replies($section);
		$section.children('blockquote,.omit,form,article[id]:last'
				).last().after(el);
		if (is_sage(msg.email)) {
			bump = false;
		}
		if (postForm) {
			// don't bump due to replies while posting (!)
			bump = false;
		}
		if (bump) {
			$hr = $section.next();
			$section.detach();
			$hr.detach();
		}
	}
	else {
		Threads.add(model);
	}

	// only add new threads on /live
	if (isThread && BUMP) {
		if (!el) {
			$section = $($.parseHTML(oneeSama.monomono(msg
					).join('')));
			$section = $section.filter('section');
			el = $section[0];
		}
		else {
			$section = $(el);
		}
		var section = new Section({model: model, id: num, el: el});
		$hr = $('<hr/>');
		if (!postForm)
			$section.append(make_reply_box());
	}

	Backbone.trigger('afterInsert', model, $(el));
	if (bump) {
		var fencepost = $('body > aside');
		$section.insertAfter(fencepost.length ? fencepost : $ceiling);
		if ($hr)
			$section.after($hr);
		spill_page();
	}
	if (orig_focus)
		orig_focus.focus();
};

dispatcher[MOVE_THREAD] = function (msg, op) {
	msg = msg[0];
	msg.num = op;
	var orig_focus = get_focus();

	var model = new Thread(msg);
	Threads.add(model);

	oneeSama.links = msg.links;
	var $el = $($.parseHTML(oneeSama.monomono(msg).join('')));
	var el = $el.filter('section')[0];

	var section = new Section({model: model, id: op, el: el});
	var $hr = $('<hr/>');
	// No make_reply_box since this is archive-only for now
	if (!BUMP) {
		$el.hide();
		$hr.hide();
	}
	if (msg.replyctr > 0) {
		var omitMsg = abbrev_msg(msg.replyctr, msg.imgctr - 1);
		$('<span class="omit"/>').text(omitMsg).appendTo($el);
	}

	Backbone.trigger('afterInsert', model, $el);
	if (BUMP) {
		var fencepost = $('body > aside');
		$el.insertAfter(fencepost.length ? fencepost : $ceiling
				).after($hr);
		spill_page();
	}
	if (orig_focus)
		orig_focus.focus();
};

dispatcher[INSERT_IMAGE] = function (msg, op) {
	var focus = get_focus();
	var num = msg[0];
	var post = Threads.lookup(num, op);

	if (saku && saku.get('num') == num) {
		if (post)
			post.set('image', msg[1], {silent: true}); // TEMP
		postForm.insert_uploaded(msg[1]);
	}
	else if (post)
		post.set('image', msg[1]);

	if (num == MILLION) {
		var $el = $('#' + num);
		$el.css('background-image', oneeSama.gravitas_style(msg[1]));
		var bg = $el.css('background-color');
		$el.css('background-color', 'black');
		setTimeout(function () { $el.css('background-color', bg); }, 500);
	}

	if (focus)
		focus.focus();
};

dispatcher[UPDATE_POST] = function (msg, op) {
	var num = msg[0], links = msg[4], extra = msg[5];
	var state = [msg[2] || 0, msg[3] || 0];
	var post = Threads.lookup(num, op);
	if (post) {
		add_post_links(post, links, op);
		var body = post.get('body') || '';
		post.set({body: body + msg[1], state: state});
	}

	if (num in ownPosts) {
		if (extra)
			extra.links = links;
		else
			extra = {links: links};
		oneeSama.trigger('insertOwnPost', extra);
		return;
	}
	var bq = $('#' + num + ' > blockquote');
	if (bq.length) {
		oneeSama.dice = extra && extra.dice;
		oneeSama.links = links || {};
		oneeSama.callback = inject;
		oneeSama.buffer = bq;
		oneeSama.state = state;
		oneeSama.fragment(msg[1]);
	}
};

dispatcher[FINISH_POST] = function (msg, op) {
	var num = msg[0];
	delete ownPosts[num];
	var thread = Threads.get(op);
	var post;
	if (op == num) {
		if (!thread)
			return;
		post = thread;
	}
	else {
		if (!thread)
			thread = UnknownThread;
		post = thread.get('replies').get(num);
	}

	if (post)
		post.set('editing', false);
};

dispatcher[DELETE_POSTS] = function (msg, op) {
	var replies = Threads.lookup(op, op).get('replies');
	var $section = $('#' + op);
	var ownNum = saku && saku.get('num');
	msg.forEach(function (num) {
		var postVisible = $('#' + num).is('article');
		delete ownPosts[num];
		var post = replies.get(num);
		clear_post_links(post, replies);
		if (num === ownNum)
			return postSM.feed('done');
		if (num == lockTarget)
			set_lock_target(null);
		if (post)
			replies.remove(post);

		if (!THREAD && !postVisible) {
			/* post not visible; decrease omit count */
			var info = section_abbrev($section);
			if (info && info.omit > 0) {
				/* No way to know if there was an image. Doh */
				var omit = info.omit - 1;
				if (omit > 0)
					info.stat.text(abbrev_msg(omit,
							info.img));
				else
					info.stat.remove();
			}
		}

	});
};

dispatcher[DELETE_THREAD] = function (msg, op) {
	delete syncs[op];
	delete ownPosts[op];
	if (saku) {
		var num = saku.get('num');
		if ((saku.get('op') || num) == op)
			postSM.feed('done');
		if (num == op)
			return;
	}
	var thread = Threads.get(op);
	if (thread)
		thread.trigger('destroy', thread, thread.collection);
};

dispatcher[LOCK_THREAD] = function (msg, op) {
	var thread = Threads.get(op);
	if (thread)
		thread.set('locked', true);
};

dispatcher[UNLOCK_THREAD] = function (msg, op) {
	var thread = Threads.get(op);
	if (thread)
		thread.set('locked', false);
};

dispatcher[DELETE_IMAGES] = function (msg, op) {
	var replies = Threads.lookup(op, op).get('replies');
	msg.forEach(function (num) {
		var post = replies.get(num);
		if (post)
			post.unset('image');
	});
};

dispatcher[SPOILER_IMAGES] = function (msg, op) {
	var thread = Threads.get(op);
	var replies = thread.get('replies');
	msg.forEach(function (info) {
		var num = info[0];
		var post = (num == op) ? thread : replies.get(num);
		if (post)
			post.set('spoiler', info[1]);
	});
};

function insert_image(info, header, toppu) {
	var html = flatten(oneeSama.gazou(info, toppu)).join('');
	// HACK ought to add `class="new"` in common?
	html = html.replace(/<img src/, '<img class="new" src');
	var fig = $(html);
	if (toppu)
		header.before(fig);
	else
		header.after(fig);

	// fade in
	setTimeout(function () { fig.find('img').removeClass('new'); }, 10);
}

function set_highlighted_post(num) {
	$('.highlight').removeClass('highlight');
	$('article#' + num).addClass('highlight');
}

var samePage = new RegExp('^(?:' + THREAD + ')?#(\\d+)$');
$DOC.on('click', 'a', function (event) {
	var target = $(this);
	var href = target.attr('href');
	if (href && (THREAD || postForm)) {
		var q = href.match(/#q(\d+)/);
		if (q) {
			event.preventDefault();
			var id = parseInt(q[1], 10);
			set_highlighted_post(id);
			with_dom(function () {
				open_post_box(id);
				postForm.add_ref(id);
			});
		}
		else if (THREAD) {
			q = href.match(samePage);
			if (q)
				set_highlighted_post(q[1]);
		}
	}
});

$DOC.on('click', 'del', function (event) {
	if (!event.spoilt) {
		event.spoilt = true;
		$(event.target).toggleClass('reveal');
	}
});

$DOC.on('click', '.pagination input', function (event) {
	location.href = $('link[rel=next]').prop('href');
});

dispatcher[SYNCHRONIZE] = connSM.feeder('sync');
dispatcher[INVALID] = connSM.feeder('invalid');

function lookup_model_path(path) {
	var o = window;
	if (!Array.isArray(path))
		return o[path];
	o = o[path[0]];
	if (o) {
		for (var i = 1; i < path.length; i++) {
			o = o.get(path[i]);
			if (!o)
				break;
		}
	}
	return o;
}

dispatcher[MODEL_SET] = function (msg, op) {
	var target = lookup_model_path(msg[0]);
	if (target && target.set)
		target.set(msg[1]);
};

dispatcher[COLLECTION_RESET] = function (msg, op) {
	var target = lookup_model_path(msg[0]);
	if (target && target.reset)
		target.reset(msg[1]);
};

dispatcher[COLLECTION_ADD] = function (msg, op) {
	var target = lookup_model_path(msg[0]);
	if (target && target.add)
		target.add(msg[1], {merge: true});
};

(function () {
	var m = window.location.hash.match(/^#q?(\d+)$/);
	if (m)
		set_highlighted_post(m[1]);

	$('section').each(function () {
		var s = $(this);
		syncs[s.attr('id')] = parseInt(s.attr('data-sync'));

		/* Insert image omission count (kinda dumb) */
		if (!THREAD) {
			var img = parseInt(s.attr('data-imgs')) -
					s.find('img').length;
			if (img > 0) {
				var stat = s.find('.omit');
				var o = stat.text().match(/(\d*)/)[0];
				stat.text(abbrev_msg(parseInt(o), img));
			}
		}
	});

	$('del').attr('onclick', 'void(0)');

	// Android browsers have no easy way to return to the top, so link it
	var android = /Android/.test(navigator.userAgent);
	if (android) {
		var t = $.parseHTML(action_link_html('#', 'Top'))[0];
		$('#bottom').css('min-width', 'inherit').after('&nbsp;', t);
	}
})();
