/*
 * Copyright (c) 2008-2011 Hasso Plattner Institute
 *
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

module('lively.persistence.Serializer').requires().toRun(function() {

Object.subclass('ObjectGraphLinearizer',
'settings', {
	defaultCopyDepth: 100,
	keepIds: Config.keepSerializerIds || false,
	showLog: true,
},
'initializing', {

	initialize: function() {
		this.idCounter = 0;
		this.registry = {};
		this.plugins = [];
		this.copyDepth = 0;
		this.path = [];
	},

	cleanup: function() {
		// remive ids from all original objects and the original objects as well as any recreated objects
		for (var id in this.registry) {
			var entry = this.registry[id];
			if (!this.keepIds && entry.originalObject)
				delete entry.originalObject[this.idProperty]
			if (!this.keepIds && entry.recreatedObject)
				delete entry.recreatedObject[this.idProperty]
			delete entry.originalObject;
			delete entry.recreatedObject;
		}
	},

},
'testing', {
	isReference: function(obj) { return obj && obj.__isSmartRef__ },
	isValueObject: function(obj) {
		if (obj == null) return true;
		if ((typeof obj !== 'object') && (typeof obj !== 'function')) return true;
		if (this.isReference(obj)) return true;
		return false
	},
},
'accessing', {
	idProperty: '__SmartId__',
	escapedCDATAEnd: '<=CDATAEND=>',
	CDATAEnd: '\]\]\>',

	newId: function() {	return this.idCounter++ },
	getIdFromObject: function(obj) {
		return obj.hasOwnProperty(this.idProperty) ? obj[this.idProperty] : undefined;
	},
	getRegisteredObjectFromId: function(id) {
		return this.registry[id] && this.registry[id].registeredObject
	},
	getRecreatedObjectFromId: function(id) {
		return this.registry[id] && this.registry[id].recreatedObject
	},
	setRecreatedObject: function(object, id) {
		var registryEntry = this.registry[id];
		if (!registryEntry)
			throw new Error('Trying to set recreated object in registry but cannot find registry entry!');
		registryEntry.recreatedObject = object
	},

},
'plugins', {
	addPlugin: function(plugin) {
		this.plugins.push(plugin);
		plugin.setSerializer(this);
	},
	addPlugins: function(plugins) {
		plugins.forEach(function(ea) { this.addPlugin(ea) }, this);
	},

	somePlugin: function(methodName, args) {
		// invoke all plugins with methodName and return the first non-undefined result (or null)
		for (var i = 0; i < this.plugins.length; i++) {
			var plugin = this.plugins[i],
				pluginMethod = plugin[methodName];
			if (!pluginMethod) continue;
			var result = pluginMethod.apply(plugin, args);
			if (result) return result
		}
		return null;
	},
	letAllPlugins: function(methodName, args) {
		// invoke all plugins with methodName and args
		for (var i = 0; i < this.plugins.length; i++) {
			var plugin = this.plugins[i],
				pluginMethod = plugin[methodName];
			if (!pluginMethod) continue;
			pluginMethod.apply(plugin, args);
		}
	},
},
'object registry -- serialization', {
	register: function(obj) {
		if (this.isValueObject(obj))
			return obj;

		if (Object.isArray(obj)) {
			var result = [];
			for (var i = 0; i < obj.length; i++) {
				this.path.push(i) // for debugging
				var item = obj[i];
				if (this.somePlugin('ignoreProp', [obj, i, item])) continue;
				result.push(this.register(item));
				this.path.pop();
			}
			return result;
		}

		var id = this.addIdAndAddToRegistryIfNecessary(obj);
		return this.registry[id].ref;
	},

	addIdAndAddToRegistryIfNecessary: function(obj) {
		var id = this.getIdFromObject(obj);
		if (id === undefined) id = this.addIdToObject(obj);
		if (!this.registry[id]) this.addNewRegistryEntry(id, obj)
		return id
	},

	addNewRegistryEntry: function(id, obj) {
		// copyObjectAndRegisterReferences must be done AFTER setting the registry entry
		// to allow reference cycles  
		var entry = this.createRegistryEntry(obj, null/*set registered obj later*/, id);
		this.registry[id] = entry;
		entry.registeredObject = this.copyObjectAndRegisterReferences(obj)
		return entry
	},

	createRegistryEntry: function(realObject, registeredObject, id) {
		return {
			originalObject: realObject || null,
			registeredObject: registeredObject || null, // copy of original with replaced refs
			recreatedObject: null, // new created object with patched refs
			ref: {__isSmartRef__: true, id: id},
		}
	},

	copyObjectAndRegisterReferences: function(obj) {
		if (this.copyDepth > this.defaultCopyDepth) {
			debugger;
			alert("Error in copyObjectAndRegisterReferences, path: " + this.path);
			throw new Error('Stack overflow while registering objects? ' + obj)
		}
		this.copyDepth++;
		var copy = {};
// LivelyLoader.installWatcher(copy, 'drawSelection', true);
		var source = this.somePlugin('serializeObj', [obj, copy]) || obj;
		for (var key in source) {
			if (!source.hasOwnProperty(key) || (key === this.idProperty && !this.keepIds)) continue;
			this.path.push(key); // for debugging
			var value = source[key];
			if (this.somePlugin('ignoreProp', [source, key, value])) continue;
if (key === 'drawSelection') debugger
			copy[key] = this.register(value);
			this.path.pop();
		}
		this.letAllPlugins('additionallySerialize', [source, copy]);
		this.copyDepth--;
		return copy;
	},

},
'object registry -- deserialization', {

	recreateFromId: function(id) {
		var recreated = this.getRecreatedObjectFromId(id);
		if (recreated) return recreated;

		// take the registered object (which has unresolveed references) and
		// create a new similiar object with patched references		
		var registeredObj = this.getRegisteredObjectFromId(id),
			recreated = this.somePlugin('deserializeObj', [registeredObj]) || {};
		this.setRecreatedObject(recreated, id); // important to set recreated before patching refs!
		for (var key in registeredObj) {
			if (key === this.classNameProperty) continue;
			this.path.push(key) // for debugging
			var value = registeredObj[key];
			recreated[key] = this.patchObj(value);
			this.path.pop();
		};
		this.letAllPlugins('afterDeserializeObj', [recreated]);
		return recreated;
	},

	patchObj: function(obj) {
		if (this.isReference(obj))
			return this.recreateFromId(obj.id)

		if (Object.isArray(obj))
			return obj.collect(function(item, idx) {
				this.path.push(idx) // for debugging
				var result = this.patchObj(item);
				this.path.pop();
				return result;
			}, this)

		return obj;		
	},

},
'serializing', {
	serialize: function(obj) {
		var root = this.serializeToJso(obj);
		return this.stringifyJSO(root);
	},
	serializeToJso: function(obj) {
		try {
			var start = new Date(),
				ref = this.register(obj),
				simplifiedRegistry = this.simplifyRegistry(this.registry),
				root = {id: ref.id, registry: simplifiedRegistry};
			this.log('Serializing done in ' + (new Date() - start) + 'ms');
			return root;
		} catch (e) {
			this.log('Cannot serialize ' + obj + ' because ' + e);
			return null;
		} finally {
			this.cleanup();
		}
	},

	simplifyRegistry: function(registry) {
		var simplified = {isSimplifiedRegistry: true};
		for (var id in registry)
			simplified[id] = this.getRegisteredObjectFromId(id)
		return simplified;
	},

	addIdToObject: function(obj) { return obj[this.idProperty] = this.newId() },
	stringifyJSO: function(jso) {
		var str = JSON.stringify(jso),
			regex = new RegExp(this.CDATAEnd, 'g');
		str = str.replace(regex, this.escapedCDATAEnd);
		return str
	},

},
'deserializing',{
	deserialize: function(json) {
		var jso = this.parseJSON(json);
		return this.deserializeJso(jso);
	},
	deserializeJso: function(jsoObj) {
		var start = new Date(),
			id = jsoObj.id;
		this.registry = this.createRealRegistry(jsoObj.registry);
		var result = this.recreateFromId(id);
		this.letAllPlugins('deserializationDone');
		this.cleanup();
		this.log('Deserializing done in ' + (new Date() - start) + 'ms');
		return result;
	},
	parseJSON: function(json) {
		if (typeof json !== 'string') return json; // already is JSO?
		var regex = new RegExp(this.escapedCDATAEnd, 'g'),
			converted = json.replace(regex, this.CDATAEnd);
		return JSON.parse(converted);
	},
	createRealRegistry: function(registry) {
		if (!registry.isSimplifiedRegistry) return registry;
		var realRegistry = {};
		for (var id in registry)
			realRegistry[id] = this.createRegistryEntry(null, registry[id], id);
		return realRegistry;
	},


},
'copying', {
	copy: function(obj) {
		var rawCopy = this.serializeToJso(obj);
		if (!rawCopy) throw new Error('Cannot copy ' + obj)
		return this.deserializeJso(rawCopy);
	},
},
'debugging', {
	log: function(msg) {
		if (!this.showLog) return;
		Global.WorldMorph && WorldMorph.current() ?
			WorldMorph.current().setStatusMessage(msg, Color.blue, 6) :
			console.log(msg);
	},
	getPath: function() { return '["' + this.path.join('"]["') + '"]' },
});

Object.extend(ObjectGraphLinearizer, {
	forLively: function() {
		var serializer = new ObjectGraphLinearizer();
		serializer.addPlugins([
			new DEPRECATEDScriptFilter(),
			new ClosurePlugin(),
			new RegExpPlugin(),
			new IgnoreFunctionsPlugin(),
			new ClassPlugin(),
			new LivelyWrapperPlugin(),
			new DoNotSerializePlugin(),
			new StoreAndRestorePlugin(),
			new OldModelFilter(),
			new LayerPlugin()
		]);
		return serializer;
	},
	forLivelyCopy: function() {
		var serializer = this.forLively();
		var p = new GenericFilter();
		var world =  Config.isNewMorphic ? lively.morphic.World.current() : WorldMorph.current();
		p.addFilter(function(obj, prop, value) { return value === world })
		serializer.addPlugins([p]);
		return serializer;
	},

	allRegisteredObjectsDo: function(registryObj, func, context) {
		for (var id in registryObj) {
			var registeredObject = registryObj[id];
			if (!registryObj.isSimplifiedRegistry)
				registeredObject = registeredObject.registeredObject;
			func.call(context || Global, id, registeredObject)
		}
	},
});

Object.subclass('ObjectLinearizerPlugin',
'accessing', {
	getSerializer: function() { return this.serializer },
	setSerializer: function(s) { this.serializer = s },
},
'plugin interface', {
	/* interface methods that can be reimplemented by subclasses:
	serializeObj: function(original) {},
	additionallySerialize: function(original, persistentCopy) {},
	deserializeObj: function(persistentCopy) {},
	ignoreProp: function(obj, propName) {},
	afterDeserializeObj: function(obj) {},
	deserializationDone: function() {},
	*/
});
ObjectLinearizerPlugin.subclass('ClassPlugin',
'properties', {
	isInstanceRestorer: true, // for Class.intializer
	classNameProperty: '__LivelyClassName__',
	sourceModuleNameProperty: '__SourceModuleName__',
},
'plugin interface', {
	additionallySerialize: function(original, persistentCopy) {
		this.addClassInfoIfPresent(original, persistentCopy);
	},
	deserializeObj: function(persistentCopy) {
		return this.restoreIfClassInstance(persistentCopy);
	},
	ignoreProp: function(obj, propName) {
		return propName == this.classNameProperty
	},
	afterDeserializeObj: function(obj) {
		this.removeClassInfoIfPresent(obj)
	},


},
'class info persistence', {
	addClassInfoIfPresent: function(original, persistentCopy) {
		// store class into persistentCopy if original is an instance
		if (!original || !original.constructor) return;
		var className = original.constructor.type;
		persistentCopy[this.classNameProperty] = className;
		var srcModule = original.constructor.sourceModule
		if (srcModule)
			persistentCopy[this.sourceModuleNameProperty] = srcModule.namespaceIdentifier;
	},
	restoreIfClassInstance: function(persistentCopy) {
		// if (!persistentCopy.hasOwnProperty[this.classNameProperty]) return;
		var className = persistentCopy[this.classNameProperty];
		if (!className) return;
		var klass = Class.forName(className);
		if (!klass || ! (klass instanceof Function)) {
			var msg = 'ObjectGraphLinearizer is trying to deserialize instance of ' +
				className + ' but this class cannot be found!';
			dbgOn(true);
			if (!Config.ignoreClassNotFound) throw new Error(msg);
			console.error(msg);
			lively.bindings.callWhenNotNull(WorldMorph, 'currentWorld',
				{warn: function(world) { world.alert(msg) }}, 'warn');
			return {isClassPlaceHolder: true, className: className};
		}
		return new klass(this);
	},

	removeClassInfoIfPresent: function(obj) {
		if (obj[this.classNameProperty])
			delete obj[this.classNameProperty];
	},
},
'searching', {
	sourceModulesIn: function(registryObj) {
		var result = [];
		ObjectGraphLinearizer.allRegisteredObjectsDo(registryObj, function(id, value) {
			var sourceModule = value[this.sourceModuleNameProperty];
			if (sourceModule && !sourceModule.startsWith('Global.anonymous_')) {
				sourceModule.include('undefined') ?
					console.error('Found strange SourceModule: ' + sourceModule) :
					result.push(sourceModule);
			}
		}, this)
		return result.uniq();
	},
});
ObjectLinearizerPlugin.subclass('LayerPlugin',
'properties', {
	withLayersPropName: 'withLayers',
	withoutLayersPropName: 'withoutLayers'

},
'plugin interface', {
	additionallySerialize: function(original, persistentCopy) {
		this.serializeLayerArray(original, persistentCopy, this.withLayersPropName)
		this.serializeLayerArray(original, persistentCopy, this.withoutLayersPropName)
	},
	afterDeserializeObj: function(obj) {
		this.deserializeLayerArray(obj, this.withLayersPropName)
		this.deserializeLayerArray(obj, this.withoutLayersPropName)
	},
	ignoreProp: function(obj, propName, value) {
		return propName == this.withLayersPropName || propName == this.withoutLayersPropName;
	},
},
'helper',{
	serializeLayerArray: function(original, persistentCopy, propname) {
		var layers = original[propname]
		if (layers && layers.length > 0)
			persistentCopy[propname] = layers.invoke('getName');
	},
	deserializeLayerArray: function(obj, propname) {
		var layers = obj[propname];
module('cop.Layers').load(true); // FIXME
		if (layers && layers.length > 0) {
			obj[propname] = layers.collect(function(ea) {
				return Object.isString(ea) ? cop.create(ea, true) : ea;
			});
		}
	},
});
ObjectLinearizerPlugin.subclass('StoreAndRestorePlugin',
'initializing', {
	initialize: function($super) {
		$super();
		this.restoreObjects = [];
	},
},
'plugin interface', {
	serializeObj: function(original, persistentCopy) {
		if (typeof original.onstore === 'function')
			original.onstore(persistentCopy);
	},
	afterDeserializeObj: function(obj) {
		if (typeof obj.onrestore === 'function')
			this.restoreObjects.push(obj);
	},
	deserializationDone: function() {
		this.restoreObjects.invoke('onrestore');
	},
});
ObjectLinearizerPlugin.subclass('DoNotSerializePlugin',
'testing', {
	doNotSerialize: function(obj, propName) {
		if (!obj.doNotSerialize) return false;
		var merged = Object.mergePropertyInHierarchy(obj, 'doNotSerialize');
		return merged.include(propName);
	},
},
'plugin interface', {
	ignoreProp: function(obj, propName, value) {
		return this.doNotSerialize(obj, propName);
	},
});
ObjectLinearizerPlugin.subclass('LivelyWrapperPlugin', // for serializing lively.data.Wrappers
'names', {
	rawNodeInfoProperty: '__rawNodeInfo__',
},
'testing', {
	hasRawNode: function(obj) {
		// FIXME how to ensure that it's really a node? instanceof?
		return obj.rawNode && obj.rawNode.nodeType
	},
},
'plugin interface', {
	additionallySerialize: function(original, persistentCopy) {
		if (this.hasRawNode(original))
			this.captureRawNode(original, persistentCopy);
	},
	ignoreProp: function(obj, propName, value) {
		if (!value) return false;
		if (value.nodeType) return true; // FIXME dont serialize nodes
		if (value === Global) return true;
		return false;
	},
	afterDeserializeObj: function(obj) {
		this.restoreRawNode(obj);
	},
},
'rawNode handling', {
	captureRawNode: function(original, copy) {
		var attribs = $A(original.rawNode.attributes).collect(function(attr) {
			return {key: attr.name, value: attr.value, namespaceURI: attr.namespaceURI}
		})
		var rawNodeInfo = {
			tagName: original.rawNode.tagName,
			namespaceURI: original.rawNode.namespaceURI,
			attributes: attribs,
		};
		copy[this.rawNodeInfoProperty] = rawNodeInfo;
	},

	restoreRawNode: function(newObj) {
		var rawNodeInfo = newObj[this.rawNodeInfoProperty];
		if (!rawNodeInfo) return;
		delete newObj[this.rawNodeInfoProperty];
		var rawNode = document.createElementNS(rawNodeInfo.namespaceURI, rawNodeInfo.tagName);
		rawNodeInfo.attributes.forEach(function(attr) {
			rawNode.setAttributeNS(attr.namespaceURI, attr.key, attr.value);
		});
		newObj.rawNode = rawNode;
	},
});
ObjectLinearizerPlugin.subclass('RegExpPlugin',
'accessing', {
	serializedRegExpProperty: '__regExp__',
},
'plugin interface', {
	serializeObj: function(original) {
		if (original instanceof RegExp)
			return this.serializeRegExp(original);
	},
	serializeRegExp: function(regExp) {
		var serialized = {};
		serialized[this.serializedRegExpProperty] = regExp.toString();
		return serialized;
	},

	deserializeObj: function(obj) {
		var serializedRegExp = obj[this.serializedRegExpProperty];
		if (!serializedRegExp) return null;
		delete obj[this.serializedRegExpProperty];
		try {
			return eval(serializedRegExp);
		} catch(e) {
			console.error('Cannot deserialize RegExp ' + e + '\n' + e.stack);
		}
	},
});
ObjectLinearizerPlugin.subclass('OldModelFilter',
'initializing', {
	initialize: function($super) {
		$super();
		this.relays = [];
	},
},
'plugin interface', {
	ignoreProp: function(source, propName, value) {
		// if (propName === 'formalModel') return true;
		// if (value && value.constructor && value.constructor.name.startsWith('anonymous_')) return true;
		return false;
	},
	additionallySerialize: function(original, persistentCopy) {
		var klass = original.constructor;
		// FIX for IE9+ which does not implement Function.name
		if (!klass.name) {
			var n = klass.toString().match('^function\s*([^(]*)\\(');
			klass.name = (n ? n[1].strip() : '');
		}
		if (!klass || !klass.name.startsWith('anonymous_')) return;
		ClassPlugin.prototype.removeClassInfoIfPresent(persistentCopy);
		var def = JSON.stringify(original.definition);
		def = def.replace(/[\\]/g, '')
		def = def.replace(/"+\{/g, '{')
		def = def.replace(/\}"+/g, '}')
// if (def.startsWith('{"0":')) debugger
		persistentCopy.definition = def;
		persistentCopy.isInstanceOfAnonymousClass = true;
		if (klass.superclass == Relay) {
			persistentCopy.isRelay = true;
		} else if (klass.superclass == PlainRecord) {
			persistentCopy.isPlainRecord = true;
		} else {
			alert('Cannot serialize model stuff of type ' + klass.superclass.type)
		}
	},
	afterDeserializeObj: function(obj) {
		// if (obj.isRelay) this.relays.push(obj);
	},
	deserializationDone: function() {
		// this.relays.forEach(function(relay) {
			// var def = JSON.parse(relay.definition);
		// })
	},
	deserializeObj: function(persistentCopy) {
		if (!persistentCopy.isInstanceOfAnonymousClass) return null;
		var instance;
		function createInstance(ctor, ctorMethodName, argIfAny) {
			var string = persistentCopy.definition, def;
			string = string.replace(/[\\]/g, '')
			string = string.replace(/"+\{/g, '{')
			string = string.replace(/\}"+/g, '}')
			try {
				def = JSON.parse(string);
			} catch(e) {
				console.error('Cannot correctly deserialize ' + ctor + '>>' + ctorMethodName + '\n' + e);
				def = {};
			}
			return ctor[ctorMethodName](def, argIfAny)
		}

		if (persistentCopy.isRelay) {
			var delegate = this.getSerializer().patchObj(persistentCopy.delegate);
			instance = createInstance(Relay, 'newInstance', delegate);
		}

		if (persistentCopy.isPlainRecord) {
			// debugger
			instance = createInstance(Record, 'newPlainInstance');
		}

		if (!instance) alert('Cannot serialize old model object: ' + JSON.stringify(persistentCopy))
		return instance;
	},

});


ObjectLinearizerPlugin.subclass('DEPRECATEDScriptFilter',
'accessing', {
	serializedScriptsProperty: '__serializedScripts__',
	getSerializedScriptsFrom: function(obj) {
		if (!obj.hasOwnProperty(this.serializedScriptsProperty)) return null;
		return obj[this.serializedScriptsProperty]
	},
},
'plugin interface', {
	additionallySerialize: function(original, persistentCopy) {
		var scripts = {}, found = false;
		Functions.own(original).forEach(function(funcName) {
			var func = original[funcName];
			if (!func.isSerializable) return;
			found = true;
			scripts[funcName] = func.toString();
		});
		if (!found) return;
		persistentCopy[this.serializedScriptsProperty] = scripts;
	},
	afterDeserializeObj: function(obj) {
		var scripts = this.getSerializedScriptsFrom(obj);
		if (!scripts) return;
		Properties.forEachOwn(scripts, function(scriptName, scriptSource) {
			Function.fromString(scriptSource).asScriptOf(obj, scriptName);
		})
		delete obj[this.serializedScriptsProperty];
	},
});
ObjectLinearizerPlugin.subclass('ClosurePlugin',
'initializing', {
	initialize: function($super) {
		$super();
		this.objectsMethodNamesAndClosures = [];
	},
},
'accessing', {
	serializedClosuresProperty: '__serializedLivelyClosures__',
	getSerializedClosuresFrom: function(obj) {
		if (!obj.hasOwnProperty(this.serializedClosuresProperty)) return null;
		return obj[this.serializedClosuresProperty]
	},
},
'plugin interface', {
	serializeObj: function(closure) { // for serializing lively.Closures
		if (!closure || !closure.isLivelyClosure) return;
		if (closure.originalFunc)
			closure.setFuncSource(closure.originalFunc.toString());
		return closure;
	},
	additionallySerialize: function(original, persistentCopy) { // for serializing objects having lively.Closures
		var closures = {}, found = false;
		Functions.own(original).forEach(function(funcName) {
			var func = original[funcName];
			if (!func || !func.hasLivelyClosure) return;
			found = true;
			closures[funcName] = func.livelyClosure;
		});
		if (!found) return;
		persistentCopy[this.serializedClosuresProperty] = this.getSerializer().register(closures);
	},
	afterDeserializeObj: function(obj) {
		var closures = this.getSerializedClosuresFrom(obj);
		if (!closures) return;
		Properties.forEachOwn(closures, function(name, closure) {
			// we defer the receration of the actual function so that all of the function's properties
			// are already deserialized
			if (closure instanceof lively.Closure) {
				// obj[name] = closure.recreateFunc();
				obj.__defineSetter__(name, function(v) { delete obj[name]; obj[name] = v });
				obj.__defineGetter__(name, function() {
					alert('early closure recreation ' + name)
					return obj[name] = closure.recreateFunc();
				})
				this.objectsMethodNamesAndClosures.push({obj: obj, name: name, closure: closure})
			}
		}, this)
		delete obj[this.serializedClosuresProperty];
	},
	deserializationDone: function() {
		this.objectsMethodNamesAndClosures.forEach(function(ea) {
			ea.obj[ea.name] = ea.closure.recreateFunc();
		})
	},

});
ObjectLinearizerPlugin.subclass('IgnoreFunctionsPlugin',
'interface', {
	ignoreProp: function(obj, propName, value) {
		return value && typeof value === 'function' && !value.isLivelyClosure && !(value instanceof RegExp);
	},
});
ObjectLinearizerPlugin.subclass('GenericFilter',
// example
// f = new GenericFilter()
// f.addPropertyToIgnore('owner')
// 
'initializing', {
	initialize: function($super) {
		$super();
		this.ignoredClasses = [];
		this.ignoredProperties = [];
		this.filterFunctions = [];
	},
},
'plugin interface', {
	addClassToIgnore: function(klass) {
		this.ignoredClasses.push(klass.type);
	},
	addPropertyToIgnore: function(name) {
		this.ignoredProperties.push(name);
	},

	addFilter: function(filterFunction) {
		this.filterFunctions.push(filterFunction);
	},
	ignoreProp: function(obj, propName, value) {
		return this.ignoredProperties.include(propName) || 
			(value && this.ignoredClasses.include(value.constructor.type)) ||
			this.filterFunctions.any(function(func) { return func(obj, propName, value) });
	},
});
ObjectLinearizerPlugin.subclass('AttributeConnectionPlugin',
'plugin interface', {
	deserializeObj: function(persistentCopy) {
		var className = persistentCopy[ClassPlugin.prototype.classNameProperty];
		if (!className || className != 'AttributeConnection') return;
		debugger;
	},
});
Object.extend(lively.persistence.Serializer, {
	jsonWorldId: 'LivelyJSONWorld',
	changeSetElementId: 'WorldChangeSet',
	serialize: function(obj, optPlugins, optSerializer) {
		var serializer = optSerializer || ObjectGraphLinearizer.forLively();
		if (optPlugins) optPlugins.forEach(function(plugin) { serializer.addPlugin(plugin) });
		var json = serializer.serialize(obj);
		return json;
	},

	serializeWorld: function(world) {
		var doc = new Importer().getBaseDocument(); // FIXME
		return this.serializeWorldToDocument(world, doc);
	},

	serializeWorldToDocument: function(world, doc) {
		return this.serializeWorldToDocumentWithSerializer(world, doc, ObjectGraphLinearizer.forLively());
	},
	serializeWorldToDocumentWithSerializer: function(world, doc, serializer) {
		// this helper object was introduced to make the code that is browser dependent
		// (currently IE9 vs the rest) easier to read. It sould be moved to dome general DOM abstraction layer
		var domAccess = {
			getSystemDictNode: function(doc) {
				return (doc.getElementById ?
					doc.getElementById('SystemDictionary') :
					doc.selectSingleNode('//*[@id="SystemDictionary"]'));
			},
			createMetaNode: function(doc) {
				return UserAgent.isIE ? doc.createNode(1, 'meta', Namespace.XHTML) : XHTMLNS.create('meta')
			},
			getCSNode: function(doc, changeSet) {
				var changeSetNode;
				if (!changeSet) {
					alert('Found no ChangeSet while serializing ' + world + '! Adding an empty CS.');
					changeSetNode = LivelyNS.create('code');
				} else {
					changeSetNode = cs.getXMLElement();
				}
				if (!UserAgent.isIE) return doc.importNode(changeSetNode, true);
				// mr: this is a real IE hack!
				var helperDoc = new ActiveXObject('MSXML2.DOMDocument.6.0');
				helperDoc.loadXML(new XMLSerializer().serializeToString(changeSetNode));
				return doc.importNode(helperDoc.firstChild, true);
			},
			getHeadNode: function(doc) {
				return doc.getElementsByTagName('head')[0] || doc.selectSingleNode('//*["head"=name()]');
			},
		}
		// FIXME remove previous meta elements - is this really necessary?
		var metaElement;
		while (metaElement = doc.getElementsByTagName('meta')[0])
			metaElement.parentNode.removeChild(metaElement)

		// FIXME remove system dictionary
		var sysDict = domAccess.getSystemDictNode(doc);
		if (sysDict) sysDict.parentNode.removeChild(sysDict);

		// serialize changeset
		var cs = world.getChangeSet(),
			csElement = domAccess.getCSNode(doc, cs),
			metaCSNode = domAccess.createMetaNode(doc);
		metaCSNode.setAttribute('id', this.changeSetElementId);
		metaCSNode.appendChild(csElement);

		// serialize world
		var json = this.serialize(world, null, serializer),
			metaWorldNode = domAccess.createMetaNode(doc);
		if (!json) throw new Error('Cannot serialize world -- serialize returned no JSON!');
		metaWorldNode.setAttribute('id', this.jsonWorldId)
		metaWorldNode.appendChild(doc.createCDATASection(json))

		var head = domAccess.getHeadNode(doc);
		head.appendChild(metaCSNode);
		head.appendChild(metaWorldNode);

		return doc;	
	},
	deserialize: function(json, optDeserializer) {
		var deserializer = optDeserializer || ObjectGraphLinearizer.forLively();
		var obj = deserializer.deserialize(json);
		return obj;
	},

	deserializeWorldFromDocument: function(doc) {
		var worldMetaElement = doc.getElementById(this.jsonWorldId);
		if (!worldMetaElement)
			throw new Error('Cannot find JSONified world when deserializing');
		var serializer = ObjectGraphLinearizer.forLively(),
			json = worldMetaElement.textContent,
			world = serializer.deserialize(json);
		return world;
	},

	deserializeWorldFromJso: function(jso) {
		var serializer = ObjectGraphLinearizer.forLively(),
			world = serializer.deserializeJso(jso);
		return world;
	},

	deserializeChangeSetFromDocument: function(doc) {
		var csMetaElement = doc.getElementById(this.changeSetElementId);
		if (!csMetaElement)
			throw new Error('Cannot find ChangeSet meta element when deserializing');
		return ChangeSet.fromNode(csMetaElement);
	},

	sourceModulesIn: function(jso) {
		return new ClassPlugin().sourceModulesIn(jso.registry);
	},

	parseJSON: function(json) {
		return new ObjectGraphLinearizer().parseJSON(json);
	},
	copyWithoutWorld: function(obj) {
		var serializer = ObjectGraphLinearizer.forLivelyCopy(),
			dontCopyWorldPlugin = new GenericFilter();
		dontCopyWorldPlugin.addFilter(function(obj, propName, value) { return value === WorldMorph.current() })
		serializer.addPlugin(dontCopyWorldPlugin);
		var copy = serializer.copy(obj);
		return copy;
	},


});

}) // end of module