/*******************************************************************************
 * Contains all view classes
 ******************************************************************************/

function FacilityView(tree, options) {
	if (!tree) {
		return null;
	}
	if (!options) {
		this.options = null;
	} else {
		this.options = options;
	}
	if (!this.options.embeddedViews)
		this.options.embeddedViews = new Array('tree', 'devicelist',
				'meterlist');
	if (typeof (this.options.header) == 'undefined')
		this.options.header = true;
	
	if (typeof(this.options.initialSelect) == 'undefined')
		this.options.initialSelect = true;

	/***************************************************************************
	 * inheritance
	 **************************************************************************/
	this.inheritFrom = Observable;
	this.inheritFrom();

	this.superAddObserver = this.addObserver;
	this.addObserver = function(o) {
		this.superAddObserver(o);
		if (this.selectedModel && this.options.initialSelect) {
			o.update('select', this.selectedModel);
		}
	};

	this.inheritFrom = Observer;
	this.inheritFrom();

	// init with the model
	this.model = tree;
	this.model.addObserver(this);

	// there definitly is always only one facility visible.
	this.currentFacility = null;
	// this holds the current selection.
	this.selectedModel = null;

	// this view contains exactly 3 other views:
	this.roomView = null;
	this.deviceView = null;
	this.meterView = null;

	this.container = null;
	// the basic html
	this.basicView = $((this.options.header == true ? '<div class="ui-widget ui-accordion">'
			+ '  <h3 class="facilityItem ui-accordion-header ui-helper-reset ui-corner-all ui-state-default ui-state-active hoverable" href="#">'
			+ '    <span class="align-left icon-loading"/>'
			+ '    <a href="#" tabindex="-1" class="fac-name">Alle Räume</a>'
			+ '  </h3>' + '</div>'
			: '')
			+ '<div class="facility-view">'
			+ '	<div class="children">'
			+ (this.options.embeddedViews.contains('tree') ? '<h3><a href="#">' + __('kg.tree.dev-tree') + '</a></h3>' + '		<div class="rooms"></div>'
					: '')
			+ (this.options.embeddedViews.contains('devicelist') ? '		<h3><a href="#">' + __('kg.tree.dev-list') + '</a></h3>' + '		<div class="devices"></div>'
					: '')
			+ (this.options.embeddedViews.contains('meterlist') ? '		<h3><a href="#">' + __('kg.tree.meterer') + '</a></h3>' + '		<div class="meters"></div>'
					: '') + '	</div>' + '</div>');
	initHoverables(this.basicView);
	this.facilityItem = this.basicView.find('.facilityItem');
	/***************************************************************************
	 * Interfaces (observer, view)
	 **************************************************************************/
	this.update = function(action, object) {
		// we're listing on a facility-holder
		switch (action) {
		case 'add':
			// don't allow facility changes
			if (this.currentFacility != null)
				return;

			// we've got a new item, hopefully a facility :)
			if (!object || !object.viewType || object.viewType != 'FAC')
				return;
			this.currentFacility = object;
			this.selectedModel = object;
			// hide loader icon
			this.facilityItem.find('.icon-loading').hide();
			// set header
			this.basicView.find('.fac-name').html(this.currentFacility.name);
			// intstall fac name listener
			this.currentFacility.addObserver( {
				object : this,
				update : function(action, object) {
					if (action == 'name')
						this.object.basicView.find('.fac-name').html(
								this.object.currentFacility.name)
				}
			});

			// create the room view for this facility.
			this.createRoomView(this.currentFacility);
			this.createDeviceView(this.currentFacility);
			this.createMeterView(this.currentFacility);

			// fire a facility selection event if not requested otherwise
			if (this.options.initialSelect) this.notify('select', this.selectedModel);
			
			break;
		case 'remove':
			// we need to remove our rooms... :( this should not happen actually.
			this.roomView.dispose();
			this.roomView = null;
			break;
		case 'select':
			this.basicView
					.find(
							'.item-info.ui-state-active, .facilityItem.ui-state-active')
					.removeClass('ui-state-active');
			// spill over to default for notifying our observers
		default:
			//console.info('notifying ',action,'with ',object);
			this.notify(action, object);
			break;
		}
	};
	this.initHTML = function(container) {
		this.basicView.appendTo(container);
		// make sure we're initialized with correct heights.
		this.basicView.bind('onshow', this, function(e) {
			if (e.data.heightInitialized)
				return;
			// this is needed only once
				e.data.heightInitialized = true;
				// check the height of our contents.
				if (e.data.basicView.find('.ui-accordion-content')
						.css('height') !== 0)
					return;
				// hasn't been shown before, so rebuild it to initialze content
				// heights.
				e.data.basicView.children('.children').accordion('destroy');
				e.data.basicView.children('.children').accordion( {
					fillSpace : true
				});
			});
		// this.basicView.children('.children').accordion('destroy');
		this.basicView.children('.children').accordion( {
			fillSpace : true
		});
	};
	/***************************************************************************
	 * additional functions
	 **************************************************************************/
	// can create a fresh room view from scratch.
	this.createRoomView = function(facility) {
		// find container
		var container = this.basicView.children('.children').find('.rooms');
		// if not there, do not create
		if (container.size() == 0)
			return;

		// replace existing view.
		if (this.roomView != null) {
			this.roomView.dispose();
		}

		// this is the treeview for all rooms, it gets initialized as pure treeview (without a itemview) on the facility
		this.roomView = new TreeView(facility, this.options);

		// need event notification of this child.
		this.roomView.addObserver(this);

		// direct child children, and there we search for rooms.
		this.roomView.initHTML(container);
		return this.roomView;
	};

	this.createDeviceView = function(parent) {

		// find container
		var container = this.basicView.children('.children').find('.devices');
		// if not there, do not create
		if (container.size() == 0)
			return;

		if (this.deviceView != null) {
			this.deviceView.dispose();
		}

		this.deviceView = new DeviceListView(parent, this.options, {viewType : 'DEV'}, {});

		// need event notification of this child.
		this.deviceView.addObserver(this);

		this.deviceView.initHTML(container);
		return this.deviceView;
	};

	this.createMeterView = function(parent) {

		// find container
		var container = this.basicView.children('.children').find('.meters');
		// if not there, do not create
		if (container.size() == 0)
			return;

		if (this.meterView != null) {
			this.meterView.dispose();
		}

		this.meterView = new DeviceListView(parent, this.options, {
			globality : 'FAC'
		}, {}); // globality is a device-specific field.
		// need event notification of this child.
		this.meterView.addObserver(this);
		this.meterView.initHTML(container);
		return this.meterView;
	};

	/***************************************************************************
	 * initialization
	 **************************************************************************/

	this.facilityItem.find('a').data('view', this).click(
			function() {
				$(this).data('view').update('select',
						$(this).data('view').currentFacility);
				$(this).parent().addClass('ui-state-active');
				return false;
			});
	// init if model isn't empty, but use only the first item (must be an
	// facility).
	if (this.model.children.length > 0) {
		if (this.model.children[0].viewType == 'FAC') {
			this.update('add', this.model.children[0]);
		}
	}
}

// creates a treeview, creates the item-views itself
function TreeView(tree, options) {
	if (!tree) {
		return null;
	}
	// intialize options
	if (!options)
		this.options = {};
	else
		this.options = options;
	// possible actions: [rename, remove, add, manual-input]
	if (!this.options.actions)
		this.options.actions = new Array(); // default: no action enabled.
	if (!this.options.expandToLevel)
		this.options.expandToLevel = 3; // default: you can see the 3rd level.

	/***************************************************************************
	 * inheritance
	 **************************************************************************/
	this.inheritFrom = Observable;
	this.inheritFrom();

	this.inheritFrom = Observer;
	this.inheritFrom();

	//
	this.model = tree;
	this.model.addObserver(this);
	this.childViews = new Array();

	/***************************************************************************
	 * private members
	 **************************************************************************/
	var that = this;

	// factory method for item views
	function createItemView(itemModel) {
		switch (itemModel.viewType) {
		case 'DEV':
			return new DeviceItemView(itemModel, that.options);
			break;
		default:
			return new TreeItemView(itemModel, that.options);
			break;
		}
	}
	function applyActionFilter(item) {
		for ( var i = 0; i < that.options.actions.length; i++) {
			item.basicView.find(
					'.actions:first .' + that.options.actions[i] + '-item')
					.show();
		}
	}

	function addItem(item)
	{
		var newItemView = createItemView(item);
		applyActionFilter(newItemView);

		// we want to get notified on view events
		newItemView.addObserver(that);

		// save it for reference
		that.childViews.push(newItemView);
		// create container html
		var newChildContainer = $('<li class="item ' + item.viewType + '"><span class="item-content"></span></li>')
			.appendTo(that.basicView)
			.hide()
		;

		// let the child view initialize its html under a given element
		newItemView.initHTML($('.item-content', newChildContainer));
		// show it
		newChildContainer.show('normal');
		// make it draggable
		newChildContainer.draggable({
			appendTo: 'body',
			cursor: 'move',
			distance: 7,
			revert: true,
			revertDuration: 0,
			opacity: .75,
			stack: {group: 'devices', min: 150}
		});
		return newItemView;
	}
	/**********************************
	 * public members, initializations
	 **********************************/

	// basic html code and back-reference for event handlers
	this.basicView = $('<ul class="tree"></ul>').data('view', this);

	// create all item views.
	for ( var i = 0; i < this.model.children.length; i++) {
		addItem(this.model.children[i]);
	}

	// enable drag'n'drop for moving devices between sections
	function initDragNDrop() {
		that.basicView.find('li.item.DEV')
	}
	
	// appends its html to a given node, needs to be called once
	this.initHTML = function(parent) {
		this.basicView.appendTo(parent);
		// make devices draggable
		initDragNDrop();
		var dropper = this.basicView.parents('.tree-item');
		if (dropper.size() == 0) dropper = this.basicView.parent();
		// accept devices to be dropped.
		dropper.droppable({
			accept: 'li.item.DEV',
			hoverClass: 'ui-state-highlight',
			greedy: true,
			drop: function(event, ui) {
				// we need the device model
				var model = ui.draggable.find('.tree-item').data('view').model;
				that.model.moveChild(model);
			}
		});
	};
	// is called on model canges and child-view events
	this.update = function(action, object) {
		//console.info('tree updating: "', action, '" with ', object);
		switch (action) {
		case 'remove':
			// an item has been deleted
			for (i = 0; i < this.childViews.length; i++) {
				if (this.childViews[i].model == object) {
					// find child-container and hide it, then remove it
					this.childViews[i].basicView
						.parents('.item:first')
						.hide('normal', function() {$(this).remove();})
					;
					// notify observer about removal of view
					this.notify('removeView', this.childViews[i]);
					// dispose corresponding childview
					this.childViews.splice(i, 1);
					break;
				}
			}
			break;
		case 'add':
			// an item has been added, corresponding view must be created and notify observer about addition of childView
			this.notify('addView', addItem(object));
			break;
		case 'select':
			//this one comes from a child view
			this.basicView.find('.ui-state-active').removeClass(
					'ui-state-active');
			// spill over to the default action as we want to notify
			// nevertheless.
		default:
			this.notify(action, object);
		}
	};

	this.dispose = function() {
		// delete it from the DOM
		this.basicView.remove();
	};
}

/*
 * this view class does not inherit from a treeView! its independent.
 * Nevertheless it embeds an additional treeView for the subtree. This
 * gives more control about nesting.
 */
function TreeItemView(treeItemModel, options) {
	//inherit from observable
	this.inheritFrom = Observable;
	this.inheritFrom();

	// init options, we COPY the given options with the help the JSON parser.
	if (!options)
		this.options = {};
	else
		this.options = JSON.parse(JSON.stringify(options));
	if (!this.options.actions)
		this.options.actions = new Array('expand');
	else if (!this.options.actions.contains('expand'))
		this.options.actions.push('expand');
	if (this.options.expandToLevel === undefined
			|| this.options.expandToLevel === null)
		this.options.expandToLevel = 2; // default: you can see the 3rd level.
	else
		this.options.expandToLevel--; // we do count down here.

	// skip zero is it gets interpreted as null
	if (this.options.expandToLevel == 0)
		this.options.expandToLevel = -1;

	/***************************************************************************
	 * public members
	 **************************************************************************/
	this.actionItems = {
		expand : $('<a href="#" class="expander '
				+ (this.options.expandToLevel > 0 ? 'expanded' : 'closed')
				+ ' ui-corner-all ui-state-default"><span class="ui-icon '
				+ (this.options.expandToLevel > 0 ? 'ui-icon-triangle-1-s'
						: 'ui-icon-triangle-1-e') + '"></span></a>')
	}

	this.model = treeItemModel;
	this.model.addObserver(this);
	// the actual item to be inserted
	this.basicView = $('<div class="tree-item">' + '<div class="item-info">'
			+ '<span class="item-name">' + this.model.name + '</span>'
			+ '<span class="actions"></span>' + '</div>'
			+ '<div class="sub-content ui-dialog-content '
			+ (this.options.expandToLevel > 0 ? '' : 'hidden') + '"></div>'
			+ '</div>').data('view', this);

	// inject action Items
	this.basicView.find('.item-name').before(this.actionItems.expand);

	this.refreshActions = function() {
		if (this.model.children.length > 0
				&& this.options.actions.contains('expand')) {
			this.actionItems.expand.show('fast');
		} else {
			this.actionItems.expand.hide();
		}
	}
	/****************************
	 * interfaces: observer, view
	 ***************************/

	this.initHTML = function(parent) {
		this.refreshActions();
		this.basicView = this.basicView.appendTo(parent);
		initIconTooltips(this.basicView);
	};

	this.update = function(action, object) {
		this.refreshActions();
		switch (action) {
		case 'name':
			// care about the interesting events
			$('.item-name:first', this.basicView).html(this.model.name);
			break;
		case 'add':
			// expand tree if a subitem was added.
			if (this.actionItems.expand.is('.closed'))
				this.actionItems.expand.click();
			break;
		case 'remove':
			break;
		default:
			// don't care about unknown events, maybe another observer knows how to handle.
			this.notify(action, object);
			break;
		}
	};

	this.dispose = function() {
		// delete it from the DOM
		this.basicView.remove();
	};

	/***************************************************************************
	 * initialization
	 **************************************************************************/
	// devices should not be able to contain other devices - model is a bit to
	// powerful here, but the view hides this
	if (this.model.viewType != 'DEV') {
		// create embedded tree
		this.treeView = new TreeView(this.model, this.options);
		this.treeView.addObserver(this);
		this.treeView.initHTML($('.sub-content', this.basicView));
	}

	// hover states
	this.basicView
		.children('.item-info,.item-info .actions a,.expander')
		.hover( 
			function() { $(this).addClass('ui-state-hover'); }, 
			function() { $(this).removeClass('ui-state-hover');	}
		)
	;

	// collapse, expand
	this.actionItems.expand.data('view', this)
			.click( function(e) {
				if ($(this).hasClass('closed')) {
					// open
					$(this).removeClass('closed').addClass('expanded')
							.children().removeClass('ui-icon-triangle-1-e')
							.addClass('ui-icon-triangle-1-s').end()
							.data('view').basicView.children('.sub-content')
							.show('blind', {}, 'fast');
				} else if ($(this).hasClass('expanded')) {
					// close.
					$(this).removeClass('expanded').addClass('closed')
							.children().removeClass('ui-icon-triangle-1-s')
							.addClass('ui-icon-triangle-1-e').end()
							.data('view').basicView.children('.sub-content')
							.hide('blind', {}, 'fast');
				}
				return false;
			});

	// registering event handler and installing a back-reference therefore.
	this.basicView
		.children('.item-info')
		.data('view', this)
		.click(function(e) {
			$(this).data('view').notify('select',$(this).data('view').model);
			$(this).addClass('ui-state-active');
		})
	;
}

/**
 * subclass treeitem for device specific view
 * - click is handled differently
 * - on creation details must be entered wizard-like
 * - additional details may be altered afterwards
 * - if we're a manual meter, provide a manualInput action-item.
 *
 */

function DeviceItemView(treeItemModel, options) {
	/********************
	 * inheritance
	 ********************/
	this.inheritFrom = TreeItemView;
	this.inheritFrom(treeItemModel, options);

	/***************************************************************************
	 * private helpers
	 **************************************************************************/
	var mySelf = this;

	/**********************************
	 * interfaces overrides: observer
	 **********************************/

	this.superUpdate = this.update;
	this.update = function(action, object) {
		// call super
		this.superUpdate(action, object);
		// react only on state changes
		if (action != 'state' || !this.options.actions.contains('manualInput'))
			return;
		// evaluate model state
		var state = this.model.getField('state');
		// object holds the old state. if it's equal do nothing.
		// if (state == object) return;
		// react on state changes. transition is from "object" to "state"
		switch (state) {
		// new means that there isn't a driver for this device
		case 'new':
			this.notify('select', this.model);
		case null:
			break;
		}

	};

	/********************
	 * initialization
	 ********************/
	this.detailView = $('.sub-content', this.basicView).removeClass(
			'sub-content').addClass('device-details').addClass('hidden') // regardless
																			// of
																			// expandToLevel,
																			// this
																			// should
																			// be
																			// hidden.
			.css('border-top-width', '0px')
	;
}

function DeviceListView(treeModel, options, includeFilters, excludeFilters) {

	// intialize options
	if (!options)
		this.options = {};
	else
		this.options = options;
	// possible actions: [rename, remove, add, manual-input]
	if (!this.options.actions)
		this.options.actions = new Array(); // default: no action enabled.

	/***************************************************************************
	 * inheritance
	 **************************************************************************/
	this.inheritFrom = Observable;
	this.inheritFrom();

	/***************************************************************************
	 * private members
	 **************************************************************************/
	var that = this;

	// helper to determine the full location breadcrumb
	function createFullLocation(itemModel) {
		var item = itemModel;
		var result = "";
		while (item.parent && item.parent.name && item.parent.viewType != 'FAC') {
			result = item.parent.name + '&nbsp;&gt;&nbsp;' + result;
			item = item.parent;
		}
		result = result.replace(' ', '&nbsp;')
		return result;
	}

	function applyActionFilter(item) {
		for ( var i = 0; i < that.options.actions.length; i++) {
			//			console.info('Action ',that.options.actions[i], ' matched. (',that.options.actions,')');
			item.basicView.find(
					'.actions .' + that.options.actions[i] + '-item').show(1);
		}
	}

	/**
	 * helper to check if the filter matches. includes take precedence over excludes.
	 * that means, if you provide an include filter, the whole exclude filter gets ignored. Also we're
	 * very strict here, include every providedd field MUST be present and MATCH on includes, an exception are objects,
	 * which are only checked for existence. Regarding excludes if only one single field is present AND matches,
	 * the device gets excluded.
	 * @return True if the filters match, false otherwise
	 */

	function matchFilter(device) {
		// only filter devices. rooms and sections are not wanted. 
		if (!device || !device.viewType || device.viewType != 'DEV') return false;
		result = true;
		if (includeFilters) {
			// include if every field matches.
			for (var field in includeFilters) {
				if (!device[field] || (typeof(device[field]) != 'object' && device[field] != includeFilters[field])) {
					// fail. we can halt and return.
					// console.info('Not Included',device.name,':
					// ',device[field],'=!',includeFilters[field],' (',field,')
					// ');
					result = false;
					break;
				}
			}
			// console.info('Included',device.name);
		} else if (excludeFilters) {

			// exclude if one matches
			for (var field in excludeFilters) {
				if (device[field] && device[field] == excludeFilters[field]) {
					// fail. we can halt and return.
					result = false;
					break;
				}
			}
		}
		return result;
	}

	function unRegisterLocationObserver(model, listener) {
		var item = model;
		while (item.parent) {
			item.parent.removeObserver(listener);
			item = item.parent;
		}
	}
	function registerLocationObserver(model, listener) {
		var item = model;
		while (item.parent) {
			item.parent.addObserver(listener);
			item = item.parent;
		}
	}

	function addItem(model) {
		// don't add if filter doesn't match
		if (!matchFilter(model))
			return;
		// console.info('-- added ', model.name);

		// create and init a fresh view
		that.devices[model.viewType + model.id] = new DeviceItemView(model);

		// modify the view a bit
		that.devices[model.viewType + model.id].basicView
				.find('.actions')
				.prepend(
						'<div class="location"><div class="full-location ui-state-default"></div><div class="short-location ui-state-default"></div></div>');

		// enable only chosen actions.
		applyActionFilter(that.devices[model.viewType + model.id]);

		// add a tiny inline observer :)
		that.devices[model.viewType + model.id].locationObserver = {
			subject : that.devices[model.viewType + model.id],
			shortView : that.devices[model.viewType + model.id].basicView
					.find('.short-location'),
			fullView : that.devices[model.viewType + model.id].basicView
					.find('.full-location'),
			update : function(action, object) {
				if (action == 'name') {
					// one of the parents have been updated.
			this.shortView
					.html((this.subject.model.parent.viewType != 'FAC' ? this.subject.model.parent.name
							: ''));
			this.fullView.html(createFullLocation(this.subject.model.parent));
		}
	}
		};
		// add it to all parents
		registerLocationObserver(model,
				that.devices[model.viewType + model.id].locationObserver);

		// call location update once to initialize.
		that.devices[model.viewType + model.id].locationObserver.update('name',
				null);

		// override dispose for removing the wrapping <li>
		that.devices[model.viewType + model.id].dispose = function() {
			that.devices[model.viewType + model.id].basicView.parent().remove();
		}

		// call the modified updater for override the initialization of the item.
		that.devices[model.viewType + model.id].update('parent', model.name);
		// append it.
		that.devices[model.viewType + model.id].initHTML($(
				'<li class="item"></li>').appendTo(that.basicView));

		// install select listener
		that.devices[model.viewType + model.id].addObserver(that);
		// console.info('Listview ', that.basicView, ' observing ',
		// that.devices[model.viewType+model.id].basicView, ' for selects.');
	}

	// simple recurser to add everywhere as view
	function addSubTree(model) {
		// we're checking for adds, removes and selects
		model.addObserver(that);

		// this observer checks whether our filter matches or unmatches on
		// changes
		model.addObserver( {
			view : that,
			model : model,
			update : function(action, object) {
				//console.info('a:',action,' o:',object);
			if (action == 'add' || action == 'remove' || action == 'select')
				return;
			// console.info(' -- v:
			// ',this.view.basicView.parent().prev().find('a').text(),' m:
			// ',this.model.name);
			if (!this.view.devices[this.model.viewType + this.model.id]) {
				// check whether we may match the item now (is checked right before add)
			// console.info('-- filtered');
			addItem(this.model);
		} else {
			// check whether we do not match the item anymore
			for (key in this.view.devices) {
				//console.info(' -- Displayed: ', key);
			}
			if (!matchFilter(this.model)) {
				//console.info(' --  Compare: ',this.model.name, ' ', this.view.devices[this.model].model.name);
				unRegisterLocationObserver(this.model,
						this.view.devices[this.model].locationObserver)
				this.view.devices[this.model].dispose();
				delete this.view.devices[this.model];
				// console.info(' -- deleted.');
			}
		}
	}
		});
		// this checks for filter and creates view if matches.
		addItem(model);

		for ( var i = 0; i < model.children.length; i++) {
			addSubTree(model.children[i]);
		}
	}

	/*****************************
	 * public fields
	 *****************************/
	this.model = treeModel;
	this.devices = new Array();
	this.basicView = $('<ul class="tree"></ul>');

	/***************************************************************************
	 * Initialization
	 **************************************************************************/
	addSubTree(treeModel);

	/***************************************************************************
	 * interfaces: Observer, View
	 **************************************************************************/
	this.update = function(action, object) {
		switch (action) {
		case 'add':
			addSubTree(object);
			break;
		case 'remove':
			if (this.devices[object.viewType + object.id] != null) {
				unRegisterLocationObserver(object, this.devices[object.viewType
						+ object.id].locationObserver)
				this.devices[object.viewType + object.id].dispose();
				delete this.devices[object.viewType + object.id];
			}
			break;
		case 'select': // gets called by child views
			// remove the currently active class
			this.basicView.find('.ui-state-active').removeClass(
					'ui-state-active');
			this.notify('select', object);
			break;
		default:
		}
	}

	this.initHTML = function(container) {
		this.basicView.appendTo(container);
		this.basicView.find('.short-location').hover(
				function() {
					$(this).parent().find('.full-location').stop(false, true)
							.show('slide', {
								direction : "right"
							}, '100').css( {
								display : 'inline-block'
							});
				},
				function() {
					$(this).parent().find('.full-location').stop(false, true)
							.hide('slide', {
								direction : "right"
							}, '100');
				});
	};
	this.dispose = function() {
		this.basicView.remove();
	}
	/*****************************
	 * public methods
	 *****************************/

}

function EditView(urls, model) {
	// the urls for specific item properties to be matched.
	this.urls = urls;
	this.model = model;
	if (this.model)
		this.model.addObserver(this);

	this.basicView = null;

	var that = this;
	function updateForm() {
		if (that.basicView == null)
			return;
		that.basicView.css('visibility', 'hidden');
		that.basicView.parent().addClass('loading');
		// TODO echo that a model should be selected.
		if (!that)
			return;

		var url = '';
		var data = null;

		KEY_LOOP: for (field in that.urls) {
			if (typeof (that.model[field]) != 'undefined') {
				VALUE_LOOP: for (key in that.urls[field]) {
					if (that.model[field] == key) {
						url = that.urls[field][key].url;
						data = {}
						data[that.urls[field][key].idKey] = that.model.id;

						// matched one possibility, so break both loops.
						break KEY_LOOP;
					}
				}
			}
		}

		// if no matching URL was found
		if (url == '') {
			that.basicView.parent().removeClass('loading');
			return;
		}

		that.basicView.load(url, data, function() {
			that.basicView.parent().removeClass('loading');
			that.basicView.css('visibility', 'visible');
			kgCore.ajaxFormInitializer(null, 'success', null, $(this), null);
		});
	}

	function addCursor() {
		var cursor = $('<div class="cloudCursor"></div>');
		that.basicView.append(cursor);
		// determine active element
		// var activeElement = that.masterView.

	}

	this.refresh = function() {
		updateForm()
	};

	this.update = function(action, object) {
		switch (action) {
		case 'select':
			if (!object)
				return;
			// listen to somebody else.
			if (this.model) {
				this.model.removeObserver(this);
			}
			this.model = object;
			this.model.addObserver(this);
			// update the Form
			updateForm();
			break;
		case 'addView':
			// select the freshman!
			if (object && object.basicView)
				object.basicView.children('.item-info').click();
			break;
		case 'removeView':
			if (!object || !object.model)
				break;
			if (object.model == this.model) {
				this.basicView.css('visibility', 'hidden');
				this.basicView.contents().remove();
			}
			break;
		}
	};

	this.setMasterView = function(masterView) {
		this.masterView = masterView;
		this.masterView.addObserver(this);
	}

	this.initHTML = function(container) {
		this.basicView = $(container);
		this.basicView.data('view', this);
	}
}

function ChartView(model) {
	this.model = model;
	if (this.model)
		this.model.addObserver(this);

	this.type = 'PCH'; // PowerConsumptionHistory
	this.basicView = $('<div><h3 class="kg-header"><span class="align-left icon-loading"/><span class="name">...</span></h3><div class="chartView"></div></div>');
	this.chartView = this.basicView.find('.chartView');
	this.failHTML = '<div class="flash-notice ui-widget"><div class="ui-state-error ui-corner-all error"><span class="ui-icon ui-icon-alert"></span>'
			+ __('kg.flash-required')
			+ '<strong><a href="http://get.adobe.com/flashplayer">'
			+ __('kg.flash-required-link') + '</a></strong></div></div>';

	this.amChart = null;
	// create a unused chart_id, which is used on every chart this view
	// generates.
	if (!kgCore.charts)
		kgCore.charts = new Array();

	this.chartId = 0;
	while (kgCore.charts['cid_' + this.chartId]) {
		this.chartId++;
	}
	this.chartId = 'cid_' + this.chartId;
	registerChart(this.chartId, this);

	/***************************************************************************
	 * private members
	 **************************************************************************/
	var that = this;
	function createChart() {
		var id = that.chartId;
		// embed a fresh chart.
		that.amChart = that.chartView.flashembed( { // embed options
					src : 'charts/amstock.swf',
					version : [ 8, 0 ],
					expressInstall : "flash/expressinstall.swf",
					onFail : function() {
						that.chartView.html(that.failHTML);
					},
					bgcolor : '#FFFFFF',
					allowfullscreen : false,
					wmode : 'transparent'
				}, { // flash params
					path : 'charts',
					settings_file : 'charts/default_settings.xml',
					additional_chart_settings : '',
					preloader_color : '#BBBBBB',
					chart_id : that.chartId
				}).getApi();
	}

	/*****************************
	 * interfaces: observer, view
	 ****************************/
	this.update = function(action, object) {
		switch (action) {
		case 'name':
			// change name
			this.basicView.find('h3 .name').html(this.model.name);
			break;
		case 'data':
			// update/draw chart
			this.updateChart();
			break;
		// chart specific messages
		case 'price':
			// update/draw chart
			this.updateChart();
			break;
		case 'chart-error':
			//			console.info('Chart (',this,') Error: ',object, ' MyModel: ', this.model, ' MyType: ', this.type);
			/*
			 * most probably missing data. since we didn't include custom error
			 * messages in the default configuration, the message "No data"
			 * means we should try to get fresh settings. check wether we've got
			 * a model and a type set
			 */
			if (object == 'No data' && this.model && this.type) {
				// redraw
				// console.info('Calling update...');
				this.updateChart();
			}
			break;
		default:
			//			console.info('Chart (',this.chartId,') update (', action, ', ',object,' )');
			break;
		}
	}

	this.initHTML = function(container) {
		this.basicView.appendTo(container);
		// createChart();
	}

	/*********************
	 * public methods
	 *********************/
	this.updateChart = function() {
		//		console.info('Chart (',this.chartId,') updating chart');
		if (!this.amChart) {
			//			console.info('no chart, creating it now.');
			createChart();
			return; // re-loading settings will occur as soon as chart is loaded
			// and notifies "no data"
		}

		function callback(mySelf, data, model) {
			// only load successful data and current model-data
			mySelf.basicView.find('.icon-loading').hide('normal');
			if (data.status != 'success' || model != mySelf.model)
				return;
			// console.info('Chart (',mySelf.chartId,') Pushing settings...');
			if (typeof (mySelf.amChart.setSettings) == 'function')
				mySelf.amChart.setSettings(data.data, true, true, true);
		}
		//		console.info('Chart (',this.chartId,') Retrieving settings...');
		// show loader
		this.basicView.find('.icon-loading').show('normal');
		// get settings
		this.model.getChartSettings(this.type, callback, this);
	}

	this.setModel = function(model, reload) {
		// assume reload if not specified differently
		if (typeof (reload) == "undefined")
			reload = true;
		if (!model || (model == this.model && reload === false)) {
			return false;
		}
		if (this.model)
			this.model.removeObserver(this);
		this.model = model;
		this.update('name', null)
		this.model.addObserver(this);
		if (reload === true) {
			this.updateChart();
		}
		return true;
	}

	this.setType = function(type, reload) {
		// assume reload if not specified differently
		if (typeof (reload) == "undefined")
			reload = true;

		if (!type || (type == this.type && reload === false)) {
			return false;
		}
		this.type = type;
		if (reload === true) {
			this.updateChart();
		}
		return true;
	}

	this.setModelAndType = function(model, type) {
		// if one actually changed, redraw.
		if (this.setModel(model, false) || this.setType(type, false))
			this.updateChart();
	}
}
/**
 * Allowed options are
 * {
 * 		'modelNameNode'	: selector or jquery object which innerHTML will be set to the model name
 * 		'chartBaseNode'	: selector or jquery object which innerHTML will be set to the chart base (like 'week from 01.01.-07.01.2010)
 * 		'loadingNode'	: selector or jquery object which will be shown and hidden according to loading state. 
 * 		'type'			: which chart type should be initialized with 
 * }
 * 
 */
function SVGChartView(model, options) {
	this.model = model;

	/***************************************************************************
	 * inheritance
	 **************************************************************************/
	this.inheritFrom = Observable;
	this.inheritFrom();
	
	
	// init object state
	if (this.model) this.model.addObserver(this);
	if (typeof (options) == 'undefined') options = {};
	this.type = (options.type ? options.type : 2);
	this.page = 1;
	this.basicView = $('<div></div>');
	this.nameView = options.modelNameNode ? options.modelNameNode : $(
			'<h3 class="kg-header"><span class="name">...</span></h3>')
			.appendTo(this.basicView).find('.name');
	this.chartView = $('<div class="chartView"></div>')
			.appendTo(this.basicView);
	this.loadingView = options.loadingNode ? $(options.loadingNode) : $(
			'<span class="align-left icon-loading"/>').insertBefore(
			this.nameView);
	this.chartBaseNode = options.chartBaseNode ? $(options.chartBaseNode) : $(
			'<span class="chartBase"></span>').insertAfter(this.nameView);
	this.tooltipNode = $('#chartTooltip');
	if (this.tooltipNode.size() == 0) {
		this.tooltipNode = $(
			'<div id="chartTooltip" class="ui-corner-all ui-widget-content padder" style="position:absolute; left:-9999px; top:0px"></div>')
			.appendTo('body')
		;
	}
	
	this.flot = null;
	this.heartbeatTimer = null;
	this.heartbeatData =  [];

	/***************************************************************************
	 * private members
	 **************************************************************************/
	var that = this;
	function xTickFormatter(value, axis)
	{
		return timeFormatter(value, that.xTickFormat);
	}

	// custom formatter to show local times instead of server times
	function timeFormatter(value, format) {
		// this points to the plot object
		var d = new Date(value);
		var result = format;
		result = result.replace(/%h\+%/g, d.getHours()+1);
		result = result.replace(/%h%/g, d.getHours());
		result = result.replace(/%M%/g, d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes());
		result = result.replace(/%S%/g, d.getSeconds() < 10 ? '0'+d.getSeconds() : d.getSeconds());
		result = result.replace(/%d%/g, d.getDate());
		result = result.replace(/%m%/g, d.getMonth() + 1);
		result = result.replace(/%y%/g, d.getFullYear());
		return result;
	}
	function currencyFormatter(value, axis) {
		return (Math.abs(value) < 1 ? (value * 100).toFixed((Math.abs(axis.max) < 0.01 ? 4 : 1)) + ' ct' : value
				.toFixed(2) + ' &euro;');
	}
	function wattFormatter(value, axis) {
		return (Math.abs(value) < (axis ? 1 : 10) ? value.toFixed(1) + ' W' : value.toFixed(0) + ' W');
	}
	function milliAmpereFormatter(value, axis) {
		return (Math.abs(value) < (axis ? 1 : 10) ? value.toFixed(1) + ' mA' : value.toFixed(0) + ' mA');
	}

	// bind event listeners
	this.chartView.bind('plothover', function(event, pos, item) {
		if (item) {
			var x = timeFormatter(item.datapoint[0], that.xTipFormat);
			var y = that.yTickFormatter(item.datapoint[1], item.series.yaxis);
			
			that.tooltipNode.html(x+'<br>'+y);
			that.tooltipNode.css({
				top: pos.pageY+10,
				left: pos.pageX+10
			});
		} else {
			that.tooltipNode.css({
				left: -9999
			});
		}
	});
	this.chartView.bind('mouseout', function(event) {
		that.tooltipNode.css({left:-9999});
	});
	var redrawTimer = null;
	$(window).bind('resize', function () {
		// clear potential redraw schedules
		window.clearTimeout(redrawTimer);
		redrawTimer = window.setTimeout(function () {
			that.updateChart();
		}, 1000);
	});


	/***************************************************************************
	 * interfaces: observer, view
	 **************************************************************************/
	this.update = function(action, object) {
		switch (action) {
		case 'name':
			// change name
			this.nameView.html(this.model.name);
			break;
		case 'data':
			// update/draw chart
			this.updateChart();
			break;
		// chart specific messages
		case 'price':
			// update/draw chart
			this.updateChart();
			break;
		case 'cache':
			// update/draw chart, but not on live data
			if (this.type != 666) {
				this.updateChart();
			}
			break;
		default:
			break;
		}
	};

	this.initHTML = function(container) {
		this.basicView.appendTo(container);
	};

	/***************************************************************************
	 * public methods
	 **************************************************************************/
	this.heartbeat = function () {
		var refresher = function(data) {
			if (data.status == 'success' && data.data) {
				// append the data on the first series
				for (var i = 0; i < data.data.length; i++) {
					// only add if the timestamp is newer than the last point.
					if (that.heartbeatData[0][that.heartbeatData[0].length-1][0] < data.data[i][0]) {
						that.heartbeatData[0].push(data.data[i]);
					}
				}
				// remove elements so we're at 60 datapoints max
				var remove = that.heartbeatData[0].length - 60;
				if (remove > 0) {
					that.heartbeatData[0] = that.heartbeatData[0].slice(remove);
				}
				that.flot.setData(that.heartbeatData);
				that.flot.setupGrid();
				that.flot.draw();
				that.flot.triggerRedrawOverlay();		
			}
			// if we should stop timer will be null
			if (that.heartbeatTimer != null) that.heartbeatTimer = window.setTimeout(that.heartbeat, 2000);			
		};
		if (that.model.units && that.model.getRemote) {
			var key = null;
			for (var i = 0; i < that.model.units.length; i++) {
				// TODO well... do this better.
				if (that.model.units[i].unit == 'MA') {
					key = that.model.units[i].key;
					break;
				}
			}
			// last datapoints x is:
			var since = that.heartbeatData[0][that.heartbeatData[0].length-1][0];
			that.model.getRemote(key, refresher, since == null ? ((new Date()).getTime()-60000) : since);
		}
	}
	
	this.updateChart = function() {
		// load current state to callback scope
		var type = this.type;
		var page = this.page;
		function callback(mySelf, data, model) {
			// only load if request was successful and view state matches request state. 
			if (data.status != 'success' || model != mySelf.model
					|| type != mySelf.type || page != mySelf.page) {
				return;
			}
			mySelf.loadingView.hide();

			// add time tick formatter.
			data.data.options['xaxis']['tickFormatter'] = xTickFormatter;
			switch (data.data.yTickFormat) {
				case 'W':
					mySelf.yTickFormatter = wattFormatter;
					break;
				case 'MA':
					mySelf.yTickFormatter = milliAmpereFormatter;
					break;
				default:
					mySelf.yTickFormatter = currencyFormatter;
					break;
			}
			data.data.options['yaxis']['tickFormatter'] = mySelf.yTickFormatter;
			

			// show chart base
			mySelf.chartBaseNode.html(data.data.label);
			
			// adjust time format
			mySelf.xTickFormat = data.data.xTickFormat;
			mySelf.xTipFormat = data.data.xTipFormat;

			// recreate the chart
			var plotter = function() {
				mySelf.flot = $.plot(mySelf.chartView, data.data.data, data.data.options);
				if (mySelf.type == 666) {
					// initialize heartbeat
					mySelf.heartbeatData = data.data.data;
					// set new timeout
					window.clearTimeout(this.heartbeatTimer);
					mySelf.heartbeatTimer = window.setTimeout(mySelf.heartbeat, 1000);
				} else {
					// make sure heartbeat is off.
					window.clearTimeout(mySelf.heartbeatTimer);
					mySelf.heartbeatTimer = null; 
				}
			};
 
			if (mySelf.chartView.is(':hidden')) {
				mySelf.chartView.one('onshow', plotter);
			} else {
				plotter();
			}
		}
		// show loader
		this.loadingView.show();
		// clear existing timeout
		window.clearTimeout(this.heartbeatTimer);
		this.heartbeatTimer = null;
		// get settings
		this.model.getSvgChartSettings(callback, this, {
			'chartType' : this.type,
			'page' : this.page
		});
	};

	this.setModel = function(model, reload) {
		// assume reload if not specified differently
		if (typeof (reload) == "undefined")
			reload = true;
		if (!model || (model == this.model && reload === false)) {
			return false;
		}
		if (this.model) this.model.removeObserver(this);
		this.model = model;
		this.update('name', null)
		this.model.addObserver(this);
		this.notify('model', this.model);
		// make sure heartbeat is just for devives...
		if (this.model.viewType != 'DEV' && this.type == 666) this.setType(1, false);
		
		if (reload === true) {
			this.updateChart();
		}
		return true;
	};

	this.setType = function(type, reload) {
		// assume reload if not specified differently
		if (typeof (reload) == "undefined")
			reload = true;

		if (typeof (type) == 'undefined'
				|| (type == this.type && reload === false)) {
			return false;
		}
		this.type = type;
		this.notify('type', this.type);

		if (reload === true) {
			this.updateChart();
		}
		return true;
	};

	this.setModelAndType = function(model, type) {
		// if one actually changed, redraw.
		if (this.setModel(model, false) || this.setType(type, false))
			this.updateChart();
	};

	this.previousPage = function() {
		this.page += 1;
		this.updateChart();
	};

	this.nextPage = function() {
		if (this.page <= 1)
			return;
		this.page -= 1;
		this.updateChart();
	};

	this.gotoPage = function(page) {
		if (page == this.page)
			return;
		this.page = Math.max(page, 1);
		this.updateChart();
	};

	this.getPage = function() {
		return this.page;
	};
}
