// jaxon.js -- Jaxon, AJAX and JSON, wrapper component and friends
// Written by Oo+
// require MooTools 1.2+
// an integral part of TRE Web 2.0 Components

//////////////////////////////////////////////////////////////////////////
//
// Jaxon class does the following things:
// - create Request object --or--
// - hook with HTML form to make it submit via AJAX
// - streamline event handling in various stage of AJAX processing
// - provide common implementation for waiting and error handling
// - parse HTTP response and package them so client code is easilier written
//
//////////////////////////////////////////////////////////////////////////
//
// How to use:
//
// This will create standalone Jaxon object:
//   new Jaxon({url: 'ajax.php', onSuccess: function(text, tree, message, data) { ... }});
//
// This will create Jaxon object that hook with form:
//   new Jaxon({form: 'form1', onSuccess: function(text, tree, message, data) { ... }});
//
// Other Request options can be passed via options parameter.
//
//////////////////////////////////////////////////////////////////////////

/// Encapsulate AJAX processing pipeline and response parser in single component.
var Jaxon = new Class({
	Implements: [Events, Options],

	/// Construct the object.
	initialize: function(options) {
		this.setOptions(options);
		this.waitAnim = null;
		this.dialogBox = new Jaxon.DialogBox(options);
		this.requestHook = {
			onRequest: this.handleRequest.bind(this),
			onCancel: this.handleCancel.bind(this),
			onFailure: this.handleFailure.bind(this),
			onSuccess: this.handleSuccess.bind(this)
		};
		if (this.options.form) {
			this.form = $(this.options.form);
			this.form.set('send', this.requestHook);
			this.form.addEvent('submit', function() { this.send(); return false; }.bind(this));
			this.request = this.form.get('send');
		}
		else
			this.request = new Request($merge(options, this.requestHook));
		return this;
	},

	/// Send request.  Do not use this with form.
	send: function(data) {
		if (this.form)
			this.form.send.delay(0, this.form);
		else
			this.request.send(data);
		return this;
	},

	/// Cancel request.
	cancel: function() {
		this.request.cancel();
		return this;
	},

	//////////////////////////////////////////////////////////////////////
	// These members are for internal use only.
	//////////////////////////////////////////////////////////////////////

	handleRequest: function() {
		if (this.form)
			this.form.getElements('button').each(function(e) { e.stackedDisable(); });
		if (this.options.wait == 'popup')
			this.dialogBox.waiting(this.cancel.bind(this));
		else if (this.options.wait == 'inplace') {
			if (!this.waitAnim) {
				this.waitAnim = new Element('div', {'class': this.options.waitAnimClass});
				this.waitAnim.setStyle('opacity', this.options.waitAnimOpacity);
				this.waitAnim.addEvent('click', this.cancel.bind(this));
			}
			var target = $(this.options.waitAnimTarget);
			if (Browser.Engine.trident)
				target.getElements('select').each(function(e) { e.stackedHide(); });
			this.waitAnim.sizeToCover(target);
		}
		this.fireEvent('request');
		return this;
	},

	handleCancel: function() {
		if (this.options.cancel == 'error')
			this.handleError('cancel', this.options.cancelMessage);
		else
			this.fireEvent('cancel').finished('cancel');
		return this;
	},

	handleFailure: function(xhr) {
		this.handleError('failure', this.options.failureMessage);
		return this;
	},

	handleSuccess: function(text) {
		if (this.options.protocol == 'default') {
			var tree = new Element('div').set('html', text).getElement('form');
			if (tree) {
				var message = tree.getElement('.message');
				message = message ? message.get('html') : "";
				if (tree.getProperty('result') == 'OK') {
					// result from server look good, go on
					var data = tree.getElement('.json');
					try {
						data = data ? JSON.decode(data.get('text')) : null;
					}
					catch (exception) {
						data = null;
					}
					var callback = function() {
						this.fireEvent('success', [text, tree, message, data]).finished('success');
					}.bind(this);
					if (message)
						this.dialogBox.ok(message, callback);
					else
						callback.delay(0);
				}
				else {
					// well-formed result, but error occurs
					this.handleError('warning', message)
				}
			}
			else {
				// server-side script error
				this.handleError('error', text);
			}
		}
		else
			this.fireEvent('success', [text]).finished('success');
		return this;
	},

	handleError: function(type, message) {
		var callback = function() {
			this.fireEvent('error', [type, message]).finished('error');
		}.bind(this);
		if (this.options.error == 'popup')
			this.dialogBox.error(type, message, callback);
		else
			callback.delay(0);
		return this;
	},

	finished: function(result) {
		this.dialogBox.close();
		this.fireEvent('finished', [result]);
		if (this.options.wait == 'inplace') {
			if (this.waitAnim) {
				var target = $(this.options.waitAnimTarget);
				if (Browser.Engine.trident)
					target.getElements('select').each(function(e) { e.stackedShow(); });
				this.waitAnim.dispose();
			}
		}
		if (this.form)
			this.form.getElements('button').each(function(e) { e.stackedEnable(); });
		return this;
	},

	options: {
		/*
		onSuccess: function(text, tree, message, data) {},  // get called when everything is fine,
		onRequest: function() {},                           // get called when request is being sent
		onCancel: function() {},                            // get called when request is cancelled
		onError: function(type, message) {},                // get called when error occured; type can be 'cancel', 'failure', 'error', or 'warning'
		onFinished: function(result) {},                    // get called when everything is finished; result can be 'cancel', 'error', or 'success'
		*/
		form: null,                // form element to hook with, if not have, will create Request object
		wait: 'popup',             // 'popup': display popup; 'inplace': place waiting animation over target; otherwise: silent
		waitAnimClass: 'loading',  // CSS class to use when create waiting animation element
		waitAnimOpacity: 1,        // waiting animation panel's opacity
		waitAnimTarget: null,      // target element to place waiting animation
		cancel: 'silent',          // 'error': treat cancel as error; otherwise: silent
		error: 'popup',            // 'popup': display popup for error; otherwise: silent
		protocol: 'default',       // 'custom': call onSuccess() with raw response; otherwise: default response parsing logic
		cancelMessage: "การทำงานถูกยกเลิก",
		failureMessage: "ไม่สามารถติดต่อ Server ได้"
	}
});

/////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////
//
// Concepts
//
//     This class just decoupled UI logics from Jaxon class, to allow
// better UI customization.  All you have to do to customize the look
// of dialog box in Jaxon process is to override member preparePage()
// to match your need.
//
//////////////////////////////////////////////////////////////////////////

Jaxon.DialogBox = new Class({
	Implements: [Events, Options],

	initialize: function(options) {
		this.setOptions(options);
		this.mode = '';
		this.dialogBox = new DialogBox(null, null, options);
		return this;
	},

	waiting: function(callback) {
		var caption = this.options.dialogCaption ? this.options.dialogCaption : this.options.waitingCaption;
		var content = this.preparePage('waiting', "", this.options.waitingButtonText);
		content.getElements('a').removeEvents('click').addEvent('click', function() {
			callback.delay(0);
		});
		this.dialogBox.update(content, caption);
		return this;
	},

	error: function(type, message, callback) {
		var caption, content;
		if (type == 'warning') {
			caption = this.options.dialogCaption ? this.options.dialogCaption : this.options.warningCaption;
			content = this.preparePage('warning', message, this.options.warningButtonText);
		}
		else {
			caption = this.options.dialogCaption ? this.options.dialogCaption : this.options.failureCaption;
			content = this.preparePage('failure', message, this.options.failureButtonText);
		}
		content.getElements('a').removeEvents('click').addEvent('click', function() {
			callback.delay(0);
		});
		this.dialogBox.update(content, caption);
		return this;
	},

	ok: function(message, callback) {
		var caption = this.options.dialogCaption ? this.options.dialogCaption : this.options.successCaption;
		var content = this.preparePage('success', message, this.options.successButtonText);
		content.getElements('a').removeEvents('click').addEvent('click', function() {
			callback.delay(0);
		});
		this.dialogBox.update(content, caption);
		return this;
	},

	close: function() {
		this.dialogBox.close();
		return this;
	},

	preparePage: function(className, message, buttonText) {
		var page = new Element('div', {'class': className});
		page.set('html', "<div class=\"icon\"></div><div class=\"text\">" + message + "</div>" +
			"<div class=\"button\"><a class=\"focus\" href=\"javascript:;\"><span>" + buttonText + "</span></a></div>");
		return page;
	},

	options: {
		dialogCaption: "",             // set this option to force all dialog page use same caption
		waitingCaption: "Loading...",  // caption when waiting page is shown
		warningCaption: "Warning",     // caption when warning page is shown
		failureCaption: "Error",       // caption when failure page is shown
		successCaption: "Success",     // caption when success page is shown
		waitingButtonText: "Stop",     // button text for waiting page
		warningButtonText: "Close",    // button text for warning page
		failureButtonText: "Close",    // button text for failure page
		successButtonText: "OK"        // button text for success page
	}
});

/////////////////////////////////////////////////////////////////////////////

Jaxon.DynaForm = new Class({
	Implements: [Events, Options],

	initialize: function(options) {
		this.setOptions(options);
		this.form = $(this.options.form);
		this.jaxon = new Jaxon($merge(options, {
			onRequest: function() { this.fireEvent('request', []); }.bind(this),
			onCancel: function() {
				this.fireEvent('cancel', []);
				if (this.options.closeOnCancel)
					this.close.bind(this).delay(1);
			}.bind(this),
			onError: function(type, message) {
				this.fireEvent('error', [type, message]);
				if (this.options.closeOnError)
					this.close.bind(this).delay(1);
			}.bind(this),
			onSuccess: function(text, tree, message, data) {
				this.fireEvent('success', [text, tree, message, data]);
				if (this.options.closeOnSuccess)
					this.close.bind(this).delay(1);
			}.bind(this),
			onFinished: function(result) { this.fireEvent('finished', [result]); }.bind(this)
		}));
		this.prepare();
		if (this.options.trigger)
			$(this.options.trigger).addEvent('click', this.open.bind(this));
		var cancelElement = this.form.getElement('.cancel');
		if (cancelElement)
			cancelElement.addEvent('click', this.close.bind(this));
		return this;
	},

	prepare: function() {
		switch (this.options.style) {
		case 'popup':
			this.dialogBox = new DialogBox(this.options.handle ? $(this.options.handle) : this.form, this.options.caption, this.options);
			break;
		case 'expand':
			this.target = $(this.options.target);
			this.form.addClass('collapsed');
			break;
		case 'unfold':
			this.target = $(this.options.target);
			this.form.addClass('hidden');
			break;
		default:
			break;
		}
		return this;
	},

	open: function(parameter) {
		switch (this.options.style) {
		case 'popup':
			this.dialogBox.open();
			break;
		case 'expand':
			this.target.addClass('collapsed');
			this.form.removeClass('collapsed');
			break;
		case 'unfold':
			this.target.addClass('hidden');
			this.form.removeClass('hidden');
			break;
		default:
			break;
		}
		this.fireEvent('open');
		this.fill(parameter);
		return this;
	},

	close: function() {
		switch (this.options.style) {
		case 'popup':
			this.dialogBox.close();
			break;
		case 'expand':
			this.target.removeClass('collapsed');
			this.form.addClass('collapsed');
			break;
		case 'unfold':
			this.target.removeClass('hidden');
			this.form.addClass('hidden');
			break;
		default:
			break;
		}
		this.fireEvent('close');
		return this;
	},

	fill: function(parameter) {
		if (this.options.fillTemplate)
			this.autoFill(this.form, this.options.fillTemplate);
		if (this.options.fill == 'auto' || this.options.fill == 'semi-auto') {
			new Jaxon({
				url: this.options.fillUrl,
				method: this.options.fillMethod,
				wait: 'inplace',
				waitAnimClass: this.options.fillAnimClass,
				waitAnimTarget: this.form,
				cancel: 'silent',
				error: 'popup',
				onCancel: function() { this.close(); }.bind(this),
				onError: function(type, message) { this.close(); }.bind(this),
				onSuccess: function(text, tree, message, data) {
					if (this.options.fill == 'auto')
						this.autoFill(this.form, data);
					this.fireEvent('fill', [this.form, data]);
				}.bind(this)
			}).send(parameter);
		}
		else if (this.options.fill == 'manual')
			this.fireEvent('fill', [this.form, null]);
	},

	autoFill: function(form, data) {
		var keys = $defined(data.__keys__) ? data.__keys__ : null;
		if (keys) for (var i = 0; i < keys.length; ++i) {
			var key = keys[i];
			var value = data[key];
			var field = form[key];
			if ($defined(field)) {
				if ($type(field) == 'collection') for (var j = 0; j < field.length; ++j)
					this.fillField($(field[j]), value);
				else
					this.fillField($(field), value);
			}
		}
	},

	fillField: function(field, value) {
		var tag = field.get('tag');
		if (tag == 'input') {
			var type = field.get('type');
			if (type == 'hidden' || type == 'text')
				field.set('value', value);
			else if (type == 'checkbox')
				field.set('checked', value ? true : false);
			else if (type == 'radio')
				field.set('checked', field.get('value') == value);
		}
		else if (tag == 'textarea')
			field.set('value', value);
		else if (tag == 'select') {
			field.getElements('option').each(function(option) {
				option.set('selected', option.get('value') == value);
			});
		}
	},

	options: {
		form: null,                // form element, this must be valid
		style: 'popup',            // form opening/closing style, possible options: 'popup', 'expand', 'unfold', otherwise: do no things
		target: null,              // target element, usage depend of style
		trigger: null,             // element to trigger open() automatically
		caption: "",               // for style == 'popup', this will be dialog caption
		handle: null,              // for style == 'popup', grab handle instead of form
		fill: 'manual',            // how to fill form fields; can be 'manual', 'auto', 'semi-auto', or otherwise
		fillTemplate: null,        // if valid, must be object, use this template to fill form before other logics
		fillUrl: "",               // URL of server-side script to request form data to fill
		fillMethod: 'post',        // HTTP method to use
		fillAnimClass: 'loading',  // CSS class use to create waiting animation for form filling process
		closeOnCancel: false,      // auto call close() when user cancelled AJAX operation
		closeOnError: false,       // auto call close() when AJAX operation result in error
		closeOnSuccess: true       // auto call close() after onSuccess event
	}
});

Jaxon.NullForm = new Class({
	Implements: [Events, Options],
	initialize: function() { return this; },
	open: function() { return this; },
	close: function() { return this; },
	options: null
});

/////////////////////////////////////////////////////////////////////////////

Jaxon.Pagination = new Class({
	Implements: [Events, Options],

	initialize: function(container, options) {
		this.setOptions(options);
		this.container = $(container);
		this.currentPage = this.options.currentPage;
		this.request = new Jaxon($merge(options, {
			wait: 'inplace',
			waitAnimTarget: this.container,
			cancel: 'error',
			error: 'popup',
			onError: this.switchMode.bind(this, this.options.errorClass),
			onSuccess: function(text, tree, message, data) { this.updatePage(tree, data, true); }.bind(this)
		}));
		this.container.getElements(this.options.reloadTrigger).each(function(e) {
			e.addEvent('click', this.load.bind(this, [null]));
		}.bind(this));
		return this;
	},

	load: function(page) {
		if ($defined(page))
			this.currentPage = parseInt(page);
		this.request.send(this.options.dataTemplate.replace(/%p/g, this.currentPage).replace(/%t/, $time()));
	},

	hasData: function(count) {
		if (count > 0)
			this.switchMode(this.options.hasDataClass);
		else
			this.switchMode(this.options.emptyClass);
		this.fixLinks();
	},

	updatePage: function(tree, data) {
		this.fireEvent('update', [this, tree, data]);
		if ($defined(data)) {
			if ($defined(data.page))
				this.currentPage = data.page;
			if ($defined(data.rows))
				this.hasData(data.rows);
		}
		return this;
	},

	switchMode: function(mode) {
		this.container.getElements(this.options.sectionSelector).each(function(e) {
			if (e.hasClass(mode))
				e.removeClass('collapsed');
			else
				e.addClass('collapsed');
		}.bind(this));
		return this;
	},

	fixLinks: function() {
		var self = this;
		var pageRe = new RegExp('#p=([0-9]+)');
		this.container.getElements('a.fixme').each(function(a) {
			var h = a.get('href');
			var m;
			if (m = h.match(pageRe)) {
				// fix link with page changing action
				var page = parseInt(m[1]);
				a.addEvent('click', function() { self.load(page); });
				a.href = "javascript:;";
				a.removeClass('fixme');
			}
		});
		return this;
	},

	options: {
		/*
		onUpdate: function(tree, data) { ... },  // fired when data is ready, extract contents from tree and fix your page
		*/
		currentPage: 0,               // start page index
		url: "",                      // server-side-script use in loading item list
		method: 'post',               // HTTP request method
		dataTemplate: "p=%p&_=%t",    // template for sending via Jaxon.send() when loading items
		waitAnimClass: 'loading',     // CSS class to use when create waiting animation element
		waitAnimOpacity: 1,           // waiting animation panel's opacity
		sectionSelector: '.section',  // CSS selector use to detect which element is section
		hasDataClass: 'has-data',     // section with this CSS class will be shown in normal situation
		emptyClass: 'empty',          // section with this CSS class will be shown in no records situation
		errorClass: 'error',          // section with this CSS class will be shown in load error situation
		reloadTrigger: '.reload-action',  // CSS selector use to find reload link or button
		dummy: null
	}
});


