var config = require('./config');
var imagerConfig = require('./imager/config');
var DEFINES = exports;
DEFINES.INVALID = 0;

DEFINES.INSERT_POST = 2;
DEFINES.UPDATE_POST = 3;
DEFINES.FINISH_POST = 4;
DEFINES.CATCH_UP = 5;
DEFINES.INSERT_IMAGE = 6;
DEFINES.SPOILER_IMAGES = 7;
DEFINES.DELETE_IMAGES = 8;
DEFINES.DELETE_POSTS = 9;
DEFINES.DELETE_THREAD = 10;
DEFINES.LOCK_THREAD = 11;
DEFINES.UNLOCK_THREAD = 12;
DEFINES.REPORT_POST = 13;

DEFINES.PING = 30;
DEFINES.IMAGE_STATUS = 31;
DEFINES.SYNCHRONIZE = 32;
DEFINES.EXECUTE_JS = 33;
DEFINES.MOVE_THREAD = 34;
DEFINES.UPDATE_BANNER = 35;
DEFINES.TEARDOWN = 36;

DEFINES.MODEL_SET = 50;
DEFINES.COLLECTION_RESET = 55;
DEFINES.COLLECTION_ADD = 56;
DEFINES.SUBSCRIBE = 60;
DEFINES.UNSUBSCRIBE = 61;

DEFINES.ANON = 'Anonymous';
DEFINES.INPUT_ROOM = 20;
DEFINES.MAX_POST_LINES = 30;
DEFINES.MAX_POST_CHARS = 2000;
DEFINES.WORD_LENGTH_LIMIT = 120;

/// OneeSama.state[0] flags
DEFINES.S_BOL = 1;
DEFINES.S_QUOTE = 2;
DEFINES.S_BIG = 4;

function initial_state() {
    // state[0] = output mode
    // state[1] = number of spoiler tags we're inside
    return [DEFINES.S_BOL, 0];
}
exports.initial_state = initial_state;

if (typeof mediaURL == 'undefined' || !mediaURL)
    mediaURL = imagerConfig.MEDIA_URL;

function is_pubsub(t) {
    return t > 0 && t < 30;
}
exports.is_pubsub = is_pubsub;

function FSM(start) {
    this.state = start;
    this.spec = {acts: {}, ons: {}, wilds: {}, preflights: {}};
}
exports.FSM = FSM;

FSM.prototype.clone = function () {
    var second = new FSM(this.state);
    second.spec = this.spec;
    return second;
};

// Handlers on arriving to a new state
FSM.prototype.on = function (key, f) {
    var ons = this.spec.ons[key];
    if (ons)
	ons.push(f);
    else
	this.spec.ons[key] = [f];
    return this;
};

// Sanity checks before attempting a transition
FSM.prototype.preflight = function (key, f) {
    var pres = this.spec.preflights[key];
    if (pres)
	pres.push(f);
    else
	this.spec.preflights[key] = [f];
};

// Specify transitions and an optional handler function
FSM.prototype.act = function (trans_spec, on_func) {
    var halves = trans_spec.split('->');
    if (halves.length != 2)
	throw new Error("Bad FSM spec: " + trans_spec);
    var parts = halves[0].split(',');
    var dest = halves[1].match(/^\s*(\w+)\s*$/)[1];
    var tok;
    for (var i = parts.length-1; i >= 0; i--) {
	var part = parts[i];
	var m = part.match(/^\s*(\*|\w+)\s*(?:\+\s*(\w+)\s*)?$/);
	if (!m)
	    throw new Error("Bad FSM spec portion: " + part);
	if (m[2])
	    tok = m[2];
	if (!tok)
	    throw new Error("Tokenless FSM action: " + part);
	var src = m[1];
	if (src == '*')
	    this.spec.wilds[tok] = dest;
	else {
	    var acts = this.spec.acts[src];
	    if (!acts)
		this.spec.acts[src] = acts = {};
	    acts[tok] = dest;
	}
    }
    if (on_func)
	this.on(dest, on_func);
    return this;
};

FSM.prototype.feed = function (ev, param) {
    var spec = this.spec;
    var from = this.state, acts = spec.acts[from];
    var to = (acts && acts[ev]) || spec.wilds[ev];
    if (to && from != to) {
	var ps = spec.preflights[to];
	for (var i = 0; ps && i < ps.length; i++)
	    if (!ps[i].call(this, param))
		return false;
	this.state = to;
	var fs = spec.ons[to];
	for (var i = 0; fs && i < fs.length; i++)
	    fs[i].call(this, param);
    }
    return true;
};

FSM.prototype.feeder = function (ev) {
    var self = this;
    return function (param) {
	self.feed(ev, param);
    };
};

var entities = {'&' : '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'};
function escape_html(html) {
    return html.replace(/[&<>"]/g, function (c) {
	return entities[c];
    });
}
exports.escape_html = escape_html;

function escape_fragment(frag) {
    var t = typeof(frag);
    if (t == 'object' && frag && typeof(frag.safe) == 'string')
	return frag.safe;
    else if (t == 'string')
	return escape_html(frag);
    else if (t == 'number')
	return frag.toString();
    else
	return '???';
}
exports.escape_fragment = escape_fragment;

function flatten(frags) {
    var out = [];
    for (var i = 0; i < frags.length; i++) {
	var frag = frags[i];
	if (Array.isArray(frag))
	    out = out.concat(flatten(frag));
	else
	    out.push(escape_fragment(frag));
    }
    return out;
}
exports.flatten = flatten;

function safe(frag) {
    return {safe: frag};
}
exports.safe = safe;

function is_noko(email) {
    return email && email.indexOf('@') == -1 && /noko/i.test(email);
}
exports.is_noko = is_noko;
function is_sage(email) {
    return config.SAGE_ENABLED && email &&
	email.indexOf('@') == -1 && /sage/i.test(email);
}
exports.is_sage = is_sage;

var OneeSama = function (t) {
    this.tamashii = t;
    this.hooks = {};
};
exports.OneeSama = OneeSama;
var OS = OneeSama.prototype;

var break_re = new RegExp("(\\S{" + DEFINES.WORD_LENGTH_LIMIT + "})");
/* internal refs, embeds */
var ref_re = />>(\d+|>\/s\/.+|>\/music\/s\/.+|>\/watch\?v=[\w-]{11}(?:#t=[\dhms]{1,9})?|>\/soundcloud\/[\w-]{1,40}\/[\w-]{1,80}|>\/@\w{1,15}\/\d{4,20}(?:\?s=\d+)?|>\/(?:a|sysint|nap)\/\d{0,10})/;

OS.hook = function (name, func) {
    var hs = this.hooks[name];
    if (!hs)
	this.hooks[name] = hs = [func];
    else if (hs.indexOf(func) < 0)
	hs.push(func);
};

OS.trigger = function (name, param) {
    var hs = this.hooks[name];
    if (hs)
	for (var i = 0; i < hs.length; i++)
	    hs[i].call(this, param);
};

function override(obj, orig, upgrade) {
    var origFunc = obj[orig];
    obj[orig] = function () {
	var args = [].slice.apply(arguments);
	args.unshift(origFunc);
	return upgrade.apply(this, args);
    };
}

/// converts one >>ref to html
OS.red_string = function (ref) {
    var prefix = ref.slice(0, 3);
    var prefix2 = ref.slice(0, 5);
    var dest, linkClass;
    if(prefix2 == '>/sys')
    {
	var num = parseInt(ref.slice(4+5), 10);
	dest = '../outbound/sysint/' + (num ? ''+num : '');
    }	
    else if (prefix == '>/w') {
	dest = 'https://www.youtube.com/' + ref.slice(2);
	linkClass = 'embed watch';
    }
    else if (prefix2 == '>/sou') {
	dest = 'https://soundcloud.com/' + ref.slice(13);
	linkClass = 'embed soundcloud';
    }
    else if (prefix == '>/s') {
	dest ="/"+ ref.slice(2);
	linkClass = 'embed'
    }
    else if (prefix == '>/@') {
	var bits = ref.slice(3).split('/');
	dest = 'https://twitter.com/' + bits[0] + '/status/' + bits[1];
	linkClass = 'embed tweet';
    }
    else if (prefix == '>/a') {
	var num = parseInt(ref.slice(4), 10);
	dest = '../outbound/a/' + (num ? ''+num : '');
    }
    else if (prefix == '>/n') {
	var num = parseInt(ref.slice(4+2), 10);
	dest = '../outbound/nap/' + (num ? ''+num : '');
    }
    else {
	this.tamashii(parseInt(ref, 10));
	return;
    }
    this.callback(new_tab_link(encodeURI(dest), '>>' + ref, linkClass));
};

/// 3rd tokenization stage; breaks text into chunks and >>refs
OS.break_heart = function (frag) {
    if (frag.safe)
	return this.callback(frag);
    // break long words
    var bits = frag.split(break_re);
    for (var i = 0; i < bits.length; i++) {
	// anchor >>refs
	var morsels = bits[i].split(ref_re);
	for (var j = 0; j < morsels.length; j++) {
	    var m = morsels[j];
	    if (j % 2)
		this.red_string(m);
	    else if (i % 2) {
		this.geimu(m);
		this.callback(safe('<wbr>'));
	    }
	    else
		this.geimu(m);
	}
    }
};

/// 2nd tokenization stage; as we transition our state[0] flag, emits html tags as necessary
OS.iku = function (token, to) {
    var state = this.state;
    if (state[0] & DEFINES.S_QUOTE && !(to & DEFINES.S_QUOTE))
	this.callback(safe('</em>'));
    if (state[0] & DEFINES.S_BIG && !(to & DEFINES.S_BIG)) {
	if (token && token.safe == '<br>')
	    token = '';
	this.callback(safe('</h4>'));
    }

    if (to & DEFINES.S_BIG && !(state[0] & DEFINES.S_BIG)) {
	this.callback(safe('<h4>'));
	state[0] |= DEFINES.S_BIG;
    }
    if (to & DEFINES.S_QUOTE && !(state[0] & DEFINES.S_QUOTE)) {
	this.callback(safe('<em>'));
	state[0] |= DEFINES.S_QUOTE;
    }

    if (to == 'SPOIL') {
	if (token[1] == '/') {
	    state[1]--;
	    this.callback(safe('</del>'));
	}
	else {
	    var del = {html: '<del>'};
	    this.trigger('spoilerTag', del);
	    this.callback(safe(del.html));
	    state[1]++;
	}
    }
    else {
	this.break_heart(token);
    }

    state[0] = to;
}

/// 1st tokenization stage, breaking up [spoiler]s, >quotes, and line breaks
OS.fragment = function (frag) {
    var chunks = frag.split(/(\[\/?spoiler\])/i);
    var state = this.state;
    for (var i = 0; i < chunks.length; i++) {
	var chunk = chunks[i];
	if (i % 2) {
	    var to = 'SPOIL';
	    if (chunk[1] == '/' && state[1] < 1)
		to = state[0] & DEFINES.S_QUOTE;
	    this.iku(chunk, to);
	    continue;
	}
	lines = chunk.split(/(\n)/);
	for (var l = 0; l < lines.length; l++) {
	    var line = lines[l];
	    var is_bol = state[0] === DEFINES.S_BOL;
	    if (l % 2)
		this.iku(safe('<br>'), DEFINES.S_BOL);
	    else if (is_bol && !state[1] && /^[#＃]{2}[^#＃]/.test(line)) {
		var to = DEFINES.S_BIG;
		if (/[>＞]/.test(line[2]))
		    to |= DEFINES.S_QUOTE;
		this.iku(line.slice(2), to);
	    }
	    else if (is_bol && /^[>＞]/.test(line))
		this.iku(line, DEFINES.S_QUOTE);
	    else if (line)
		this.iku(line, state[0] & (DEFINES.S_QUOTE | DEFINES.S_BIG));
	}
    }
};

OS.close_out = function () {
    if (this.state[0] & DEFINES.S_QUOTE) {
	this.callback(safe('</em>'));
	this.state[0] -= DEFINES.S_QUOTE;
    }

    if (this.state[0] & DEFINES.S_BIG) {
	this.callback(safe('</h4>'));
	this.state[0] -= DEFINES.S_BIG;
    }

    while (this.state[1] > 0) {
	this.callback(safe('</del>'));
	this.state[1]--;
    }
};

/// converts one post body to HTML
OS.karada = function (body) {
    var output = [];
    this.state = initial_state();
    this.callback = function (frag) { output.push(frag); }
    this.fragment(body);
    this.close_out();
    this.callback = null;
    return output;
}

var dice_re = /(#tea|#dame|#tutturu|#muri|#flip|#when|#du|#pyu|#pcount|#test|#sleep|#janken|#8ball|#imfey|#\?(?:\[(?:\w,*)+\])?|#\d{0,2}d\d{1,4}(?:[+-]\d{1,4})?)/i;
exports.dice_re = dice_re;

var WHEN = [
    'Now', null, null, null, null,
    'Later', null, null, null, null,
    'In 5 minutes', null, null, null, null,
    '5 minutes ago', null, null,
    'In exactly 2.54 seconds', null, null,
    'At midnight tonight', null, null,
    'on fridey nite', null,
    'Tomorrow', null, null, null,
    'Next week', null, null,
    'I\'m not at liberty to say', null,
    'Whenever you want', null, null, null,
    'Never', null, null, null,
    'Immediately', null, null,
    'Always', null, 
    'NEVER EVER', null, 
    'A̵҉̞͚̦̖͔̰̣̱t҉̡̤͚ ̮̱͇́͞͝t̺̺̮h̵̜̠̘̩̮̥͓̀e͇̮͎̻ ͔̪̻̙̦̭̼h̸̭̬̭͉̱̝̖̝̰͠͡e̻͍͈̕͝à̢͈͜ṭ̵̳̩̲̩͚-̤̝͓͍͜ͅḓ͍̥͉̝͜͝e͖͎͓a̸̡̘̠̯̻̩͝t̛̻̗̤͘h͍̺ ̶̧͏̗̖̺ơ̹͍f͓͓̘̻̤͉̤̺ ̨̪̗̻̝̹͞t͈̮͜͢h̰̥̞̹ͅȩ̥̹͇̩̼̻ ̸̡̤̙̯͟ͅú̺̬̘̩͞n͖̣͕̲̦̟̼͡͞į̛̻͇̹͚͡v͈̫͚̜͘̕͠é͔̯̲̤͉̱̫͟͝r̷̛͉̮s̷͔͍̰̹e̤̖̙͝',
];

var EIGHT_BALL = [
    'Yes',
    'No',
    'Maybe',
    'Ara ara',
    'Why this',
    'Help',
    'git gud',
    'Okay',
    'My condolences',
    'Praise be',
    'EEEEEEH?!',
    'Try again never',
    '100%',
    'Can you repeat the question',
    'lol',
    '0.1%',
    'agreedo',
    'nope.avi',
    'Would you kindly?',
];

var IMFEY = [
    'wowme2',
    'u do u',
    "go get 'em champ",
    'my child...',
    '✨',
    'Kooloo Limpah!',
    'Invigorating.',
    'sproink',
    'elwind!',
];

function parse_dice(frag) 
{
    if(/^#(dame|muri|tutturu)$/i.test(frag))
	return {n:1, faces:2};
    if(frag == '#loli') {
	return {n:1, faces:3};
    }
    if(frag == '#?')
    {
	return {n:1, faces:2};
    }
    if (/^#\?/.test(frag))
    {
	var  l = frag.slice(2).split(/(\[|\]|,)/);
	var j =0;
	for(var i=0;i<l.length;i++)
	{
	    if(/(\[|\]|,)/.test(l[i])) continue;
	    if(l[i].length>0) j+=1;
	}
	return {n:1, faces: j};
    }
    if (frag == '#pyu' || frag == '#pcount' || frag == '#du')
	return {n: 1, faces: 2};
    if (frag == '#tea')
	return {n: 1, faces: 2};
    if (frag == '#flip')
	return {n: 1, faces: 2};
    if (frag == '#imfey')
	return {n: 1, faces: IMFEY.length};
    if (frag == '#8ball')
	return {n: 1, faces: EIGHT_BALL.length};
    if (frag == '#when')
	return {n: 1, faces: WHEN.length};
    if (frag == '#test')
	return {n: 1, faces: 2};
    if (frag == '#sleep')
	return {n: 1, faces: 2};
    if (frag == '#janken')
	return {n: 1, faces: 3};
    var m = frag.match(/^#(\d*)d(\d+)([+-]\d+)?$/i);
    if (!m)
	return false;
    var n = parseInt(m[1], 10) || 1, faces = parseInt(m[2], 10);
    if (n < 1 || n > 10 || faces < 2 || faces > 100)
	return false;
    var info = {n: n, faces: faces};
    if (m[3])
	info.bias = parseInt(m[3], 10);
    return info;
}
exports.parse_dice = parse_dice;

const audio_root = "/s/audio";

function readable_audio(bit, d) {
    if (bit=='#dame')
	return "dame.wav";
    else if (bit=='#muri')
	return "muri.wav";
    else if (bit=='#tutturu')
	return "tutturu.wav";
}

function readable_dice(bit, d) {
    if(bit == '#loli') {
	return "you should not see this";
    }
    if(bit == '#?')
    {
	if(d[0] === 0) return '#? (I have no input)';
	if(d[0] === -1) return '#? (You have no post)';
	return '#? ('+d[0]+')';
    }
    if (/^#\?/.test(bit))
    {
	var list = [];
	var l = bit.slice(2).split(/(\[|\]|,)/);
	if(!!l) {
	    for(var i=0;i<l.length;i++)
	    {
		if(/(\[|\]|,)/.test(l[i])) continue;
		if(l[i].length>0)
		    list.push(l[i]);
	    }
	    if(d[1]>list.length) return bit + " (error)";
	    else return bit + " (" + list[ d[1]-1] + ")";
	}
    }
    if (bit == '#du')
    {
	if(d[0]>=0)
	    return '#du ('+d[0]+'%)';
	else
	    return '#du (Try asking again)';
    }
    if (bit == '#pyu')
	return '#pyu (' + d[0]+ ')';
    if (bit == '#pcount')
	return '#pcount (' + d[0] + ')';
    if (bit == '#tea')
	return '#tea (' +  ((d[0] === true)?"now!":d[0]+" hours") + ')';
    if (bit == '#flip')
	return '#flip (' + (d[1] == 2) + ')';
    if (bit == '#imfey')
	return '#imfey (' + IMFEY[d[1] - 1] + ')';
    if (bit == '#8ball')
	return '#8ball (' + EIGHT_BALL[d[1] - 1] + ')';
    if (bit == '#when')
    {
	const f = (n) => {
	    if(WHEN[n] === null)
		return f(n-1);
	    else return WHEN[n];
	};
	return '#when (' + f(d[1]-1) + ')';
    }
    if (bit == '#test')
	return '#test (it worked!)';
    if (bit == '#sleep')
	return '#sleep (Yes)';
    if (bit == '#janken')
	return '#janken ('+ ["Rock", "Paper", "Scissors"][d[1]-1]+')';
    var f = d[0], n = d.length, b = 0;
    if (d[n-1] && typeof d[n-1] == 'object') {
	b = d[n-1].bias;
	n--;
    }
    var r = d.slice(1, n);
    n = r.length;
    bit += ' (';
    var eq = n > 1 || b;
    if (eq)
	bit += r.join(', ');
    if (b)
	bit += (b < 0 ? ' - ' + (-b) : ' + ' + b);
    var sum = b;
    for (var j = 0; j < n; j++)
	sum += r[j];
    return bit + (eq ? ' = ' : '') + sum + ')';
}

/// 4th tokenization stage; populates dice rolls
OS.geimu = function (text) {
    if (!this.dice) {
	this.kinpira(text);
	return;
    }

    var bits = text.split(dice_re);
    for (var i = 0; i < bits.length; i++) {
	var bit = bits[i];
	if (!(i % 2) || !parse_dice(bit)) {
	    this.kinpira(bit);
	}
	else if (this.queueRoll) {
	    this.queueRoll(bit);
	}
	else if (!this.dice[0]) {
	    this.kinpira(bit);
	}
	else {
	    var d = this.dice.shift();
	    this.callback(safe('<strong>'));
	    this.strong = true; // for client DOM insertion
	    var audio = readable_audio(bit,d);
	    if(audio)
	    {
		this.callback(safe("<a style='text-decoration:none;' href='javascript:void(0);' onclick='this.nextSibling.play();'>"+bit+"</a>"));
		this.callback(safe("<audio src='"+audio_root+"/"+audio+"'></audio>"));
	    }
	    else if(bit == "#loli") {
		this.callback(safe("<a href='"+d[0]+"' target='_blank' rel='noopener noreferrer'><img src='"+d[1]+"'></img></a>"));
	    }
	    else
		this.callback(readable_dice(bit, d));
	    this.strong = false;
	    this.callback(safe('</strong>'));
	}
    }
};

/// 5th tokenization stage; parses ^s
OS.kinpira = function (text) {
    this.itameshi(text);
    return;

    if (!/[＾^]/.test(text) || /^([＾^]_|:[＾^])/.test(text)) {
	this.itameshi(text);
	return;
    }
    var bits = text.split(/[＾^]/);
    // remove trailing ^s
    while (bits.length && bits[bits.length-1] == '')
	bits.pop();

    var soup = safe('<sup>');
    this.sup_level = 0;
    for (var i = 0; i < bits.length; i++) {
	if (bits[i])
	    this.itameshi(bits[i]);
	if (i + 1 < bits.length && i < 5) {
	    // if there's more text, open a <sup>
	    this.itameshi(soup);
	    this.sup_level++;
	}
    }
    // close all the sups we opened
    var n = this.sup_level;
    this.sup_level = 0;
    soup = safe('</sup>');
    for (var i = 0; i < n; i++)
	this.itameshi(soup);
};

/// 6th tokenization stage; parses individual *italic* *words*
OS.itameshi = function (text) {
    while (true) {
	var m = /(^|[ .,;:?!(-])\*([^ *]+)\*($|[ .,;:?!)-])/.exec(text);
	if (!m)
	    break;
	if (m.index > 0) {
	    var before = text.slice(0, m.index);
	    LINKIFY ? this.linkify(before) : this.callback(before);
	}
	if (m[1])
	    this.callback(m[1]);
	this.callback(safe('<i>' + escape_html(m[2]) + '</i>'));
	text = text.slice(m.index + m[0].length - m[3].length);
    }
    if (text)
	LINKIFY ? this.linkify(text) : this.callback(text);
};

// Convert text URLs to clickable links
// *Not* recommended. Use at your own risk.
var LINKIFY = false;

/// optional 7th tokenization stage
if (LINKIFY) { OS.linkify = function (text) {

    var bits = text.split(/(https?:\/\/[^\s"<>^]*[^\s"<>'.,!?:;^])/);
    for (var i = 0; i < bits.length; i++) {
	if (i % 2) {
	    var e = escape_html(bits[i]);
	    // open in new tab, and disavow target
	    this.callback(safe('<a href="' + e +
			       '" rel="nofollow noopener noreferrer" target="_blank">' +
			       e + '</a>'));
	}
	else
	    this.callback(bits[i]);
    }
}; }

function chibi(imgnm, src) {
    var name = '', ext = '';
    var m = imgnm.match(/^(.*)(\.\w{3,4})$/);
    if (m) {
	name = m[1];
	ext = m[2];
    }
    var bits = [safe('<a href="'), src, safe('" download="'), imgnm];
    if (name.length >= 38) {
	bits.push(safe('" title="'), imgnm);
	imgnm = [name.slice(0, 30), safe('(&hellip;)'), ext];
    }
    bits.push(safe('" rel="nofollow">'), imgnm, safe('</a>'));
    return bits;
}

OS.spoiler_info = function (index, toppu) {
    var large = toppu || this.thumbStyle == 'large';
    var hd = toppu || this.thumbStyle != 'small';
    return {
	thumb: encodeURI(mediaURL + 'kana/spoiler' + (hd ? '' : 's')
			 + index + '.png'),
	dims: large ? imagerConfig.THUMB_DIMENSIONS
	    : imagerConfig.PINKY_DIMENSIONS,
    };
};

var spoilerImages = imagerConfig.SPOILER_IMAGES;

function pick_spoiler(metaIndex) {
    var imgs = spoilerImages;
    var n = imgs.normal.length;
    var count = n + imgs.trans.length;
    var i;
    if (metaIndex < 0)
	i = Math.floor(Math.random() * count);
    else
	i = metaIndex % count;
    var spoiler = i < n ? imgs.normal[i] : imgs.trans[i - n];
    return {index: spoiler, next: (i+1) % count};
}
exports.pick_spoiler = pick_spoiler;

function new_tab_link(srcEncoded, inside, cls) {
    return [safe('<a href="' + srcEncoded + '" target="_blank"' +
		 (cls ? ' class="'+cls+'"' : '') +
		 ' rel="noreferrer nofollow noopener">'), inside, safe('</a>')];
}


OS.image_paths = function () {
    if (!this._imgPaths) {
	this._imgPaths = {
	    src: mediaURL + 'src/',
	    thumb: mediaURL + 'thumb/',
	    mid: mediaURL + 'mid/',
	    vint: mediaURL + 'vint/',
	};
	this.trigger('mediaPaths', this._imgPaths);
    }
    return this._imgPaths;
};

var audioIndicator = "\u266B"; // musical note

OS.gazou = function (info, toppu) {
    var src, name, caption, video;
    if (info.vint) {
	src = encodeURI('../outbound/hash/' + info.MD5);
	var google = encodeURI('../outbound/g/' + info.vint);
	var iqdb = encodeURI('../outbound/iqdb/' + info.vint);
	caption = ['Search ', new_tab_link(google, '[Google]'), ' ',
		   new_tab_link(iqdb, '[iqdb]'), ' ',
		   new_tab_link(src, '[foolz]')];
    }
    else {
	src = encodeURI(this.image_paths().src + info.src);
	video = info.video || (/\.webm$/i.test(src) && 'webm'); // webm check is legacy
	caption = [video ? 'Video ' : 'Image ', new_tab_link(src, info.src)];
    }

    var img = this.gazou_img(info, toppu);
    var dims = info.dims[0] + 'x' + info.dims[1];

    return [safe('<figure data-MD5="'), info.MD5,
	    safe('" data-size="'), info.size,
	    video ? [safe('" data-video="'), video] : '',
	    safe('"><figcaption>'),
	    caption, safe(' <i>('),
	    info.audio ? (audioIndicator + ', ') : '',
	    info.duration ? (info.duration + ', ') : '',
	    readable_filesize(info.size), ', ',
	    dims, (info.apng ? ', APNG' : ''),
	    this.full ? [', ', chibi(info.imgnm, img.src)] : '',
	    safe(')</i></figcaption>'),
	    this.thumbStyle == 'hide' ? '' : img.html,
	    safe('</figure>\n\t')];
};

exports.thumbStyles = ['small', 'sharp', 'large', 'hide'];

OS.gazou_img = function (info, toppu) {
    var src, thumb;
    var imgPaths = this.image_paths();
    if (!info.vint)
	src = thumb = encodeURI(imgPaths.src + info.src);

    var d = info.dims;
    var w = d[0], h = d[1], tw = d[2], th = d[3];
    if (info.spoiler) {
	var sp = this.spoiler_info(info.spoiler, toppu);
	thumb = sp.thumb;
	tw = sp.dims[0];
	th = sp.dims[1];
    }
    else if (info.vint) {
	tw = tw || w;
	th = th || h;
	src = encodeURI('../outbound/hash/' + info.MD5);
	thumb = imgPaths.vint + info.vint;
    }
    else if (this.thumbStyle != 'small' && info.mid) {
	thumb = encodeURI(imgPaths.mid + info.mid);
	if (!toppu && this.thumbStyle == 'large') {
	    tw *= 2;
	    th *= 2;
	}
    }
    else if (info.thumb)
	thumb = encodeURI(imgPaths.thumb + info.thumb);
    else {
	tw = w;
	th = h;
    }

    var img = '<img src="'+thumb+'"';
    if (tw && th)
	img += ' width="' +tw+'" height="'+th+'">';
    else
	img += '>';
    if (imagerConfig.IMAGE_HATS)
	img = '<span class="hat"></span>' + img;
    img = new_tab_link(src, safe(img));
    return {html: img, src: src};
};

function readable_filesize(size) {
    /* Dealt with it. */
    if (size < 1024)
	return size + ' B';
    if (size < 1048576)
	return Math.round(size / 1024) + ' KB';
    size = Math.round(size / 104857.6).toString();
    return size.slice(0, -1) + '.' + size.slice(-1) + ' MB';
}
exports.readable_filesize = readable_filesize;

function pad(n) {
    return (n < 10 ? '0' : '') + n;
}

OS.readable_time = function (time) {
    var h = this.tz_offset;
    var offset;
    if (h || h == 0)
	offset = h * 60 * 60 * 1000;
    else /* would be nice not to construct new Dates all the time */
	offset = new Date().getTimezoneOffset() * -60 * 1000;
    var d = new Date(time + offset);
    var k = "日月火水木金土"[d.getUTCDay()];
    return (d.getUTCFullYear() + '/' + pad(d.getUTCMonth()+1) + '/' +
	    pad(d.getUTCDate()) + '&nbsp;(' + k + ') ' +
	    pad(d.getUTCHours()) + ':' +
	    pad(d.getUTCMinutes()));
};

function datetime(time) {
    var d = new Date(time);
    return (d.getUTCFullYear() + '-' + pad(d.getUTCMonth()+1) + '-' +
	    pad(d.getUTCDate()) + 'T' + pad(d.getUTCHours()) + ':' +
	    pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) + 'Z');
}

OS.post_url = function (num, op, quote) {
    op = op || num;
    return (this.op == op ? '' : op) + (quote ? '#q' : '#') + num;
};

OS.post_ref = function (num, op, desc_html) {
    var ref = '&gt;&gt;' + num;
    if (desc_html)
	ref += ' ' + desc_html;
    else if (this.op && this.op != op)
	ref += ' \u2192';
    else if (num == op && this.op == op)
	ref += ' (OP)';
    return safe('<a href="'+this.post_url(num, op, false)+'">'+ref+'</a>');
};

OS.post_nav = function (post) {
    var n = post.num, o = post.op;
    return safe('<nav><a href="' + this.post_url(n, o, false) +
		'">No.</a><a href="' + this.post_url(n, o, true) +
		'">' + n + '</a></nav>');
};

function action_link_html(href, name, id) {
    var span = '<span ' + (id ? 'id="'+id+'" ' : '') + 'class="act">';
    return span + '<a href="'+href+'">'+name+'</a></span>';
}
exports.action_link_html = action_link_html;

exports.reasonable_last_n = function (n) {
    return n >= 5 && n <= 500;
};

OS.last_n_html = function (num) {
    return action_link_html(num + '?last' + this.lastN,
			    'Last&nbsp;' + this.lastN);
};

OS.expansion_links_html = function (num, omit) {
    var html = ' &nbsp; ' + action_link_html(num, 'Expand');
    if (omit > this.lastN)
	html += ' ' + this.last_n_html(num);
    return html;
};

OS.atama = function (data) {
    var auth = data.auth;
    var header = auth ? [safe('<b class="'),auth.toLowerCase(),safe('">')]
	: [safe('<b>')];
    if (data.subject)
	header.unshift(safe('<h3>「'), data.subject, safe('」</h3> '));
    if (data.name || !data.trip) {
	header.push(data.name || DEFINES.ANON);
	if (data.trip)
	    header.push(' ');
    }
    if (data.trip)
	header.push(safe('<code>' + data.trip + '</code>'));
    if (auth)
	header.push(' ## ' + auth);
    this.trigger('headerName', {header: header, data: data});
    header.push(safe('</b>'));
    if (data.email) {
	header.unshift(safe('<a class="email" href="mailto:'
			    + encodeURI(data.email)
			    + '" ref="noopener noreferrer" target="_blank">'));
	header.push(safe('</a>'));
    }
    header.push(safe(' <time datetime="' + datetime(data.time) +
		     '">' + this.readable_time(data.time) + '</time> '),
		this.post_nav(data));
    if (!this.full && !data.op) {
	var ex = this.expansion_links_html(data.num, data.omit);
	header.push(safe(ex));
    }
    this.trigger('headerFinish', {header: header, data: data});
    header.unshift(safe('<header>'));
    header.push(safe('</header>\n\t'));
    return header;
};

OS.monogatari = function (data, toppu) {
    var tale = {header: this.atama(data)};
    this.dice = data.dice;
    var body = this.karada(data.body);
    tale.body = [safe('<blockquote>'), body, safe('</blockquote>')];
    if (data.num == MILLION) {
	tale.body.splice(1, 0, safe('<script>window.gravitas=true;</script>'));
    }
    if (data.image && !data.hideimg)
	tale.image = this.gazou(data.image, toppu);
    return tale;
};

var MILLION = 1000000;

function gravitas_body() {
    $('body').css({margin: 0});
}

OS.gravitas_style = function (idata, cssy) {
    var src = this.image_paths().src + idata.src;
    src = "url('" + encodeURI(src) + "')";
    return cssy ? ("background-image: " + src + ";") : src;
};

OS.mono = function (data) {
    var info = {
	data: data,
	classes: data.editing ? ['editing'] : [],
	style: ''
    };
    if (data.flavor)
	info.classes.push(data.flavor);
    if (data.num == MILLION) {
	info.classes.push('gravitas');
	if (data.image)
	    info.style = this.gravitas_style(data.image, true);
    }
    this.trigger('openArticle', info);
    var cls = info.classes.length && info.classes.join(' '),
	o = [safe('\t<article id="'+data.num+'"'),
	     (cls ? [safe(' class="'), cls, safe('"')] : ''),
	     safe(info.style ? ' style="'+info.style+'"' : ''),
	     safe('>')],
	c = safe('</article>\n'),
	gen = this.monogatari(data, false);
    return flatten([o, gen.header, gen.image || '', gen.body, c]).join('');
};

OS.monomono = function (data, cls) {
    if (data.flavor)
	cls = cls ? cls+' '+data.flavor : data.flavor;
    if (data.locked)
	cls = cls ? cls+' locked' : 'locked';
    var style;
    if (data.num == MILLION) {
	cls = cls ? cls+' gravitas' : 'gravitas';
	if (data.image)
	    style = this.gravitas_style(data.image, true);
    }
    var o = [safe('<section id="' + data.num),
	     (cls ? [safe('" class="'), cls] : ''),
	     safe(style ? '" style="' + style : ''),
	     safe('" data-sync="' + (data.hctr || 0)),
	     safe(data.full ? '' : '" data-imgs="'+data.imgctr),
	     safe('">')],
	c = safe('</section>\n'),
	gen = this.monogatari(data, true);
    return flatten([o, gen.image || '', gen.header, gen.body, '\n', c]);
};

function pluralize(n, noun) {
    return n + ' ' + noun + (n == 1 ? '' : 's');
}
exports.pluralize = pluralize;

exports.abbrev_msg = function (omit, img_omit) {
    return omit + (omit==1 ? ' reply' : ' replies') + (img_omit
						       ? ' and ' + pluralize(img_omit, 'image')
						       : '') + ' omitted.';
};

exports.parse_name = function (name) {
    var tripcode = '', secure = '';
    var hash = name.indexOf('#');
    if (hash >= 0) {
	tripcode = name.substr(hash+1);
	name = name.substr(0, hash);
	hash = tripcode.indexOf('#');
	if (hash >= 0) {
	    secure = escape_html(tripcode.substr(hash+1));
	    tripcode = tripcode.substr(0, hash);
	}
	tripcode = escape_html(tripcode);
    }
    name = name.trim().replace(config.EXCLUDE_REGEXP, '');
    return [name.substr(0, 100), tripcode.substr(0, 128),
	    secure.substr(0, 128)];
};

function random_id() {
    return Math.floor(Math.random() * 1e16) + 1;
}
