module('lively.Scripting').requires('lively.Widgets', 'cop.Layers', 'lively.Connector', 'lively.PartsBinMorphs').toRun(function() {
/*
 *  This module is for textual scripting morphs on a lively page
 */

Object.extend(Layer, {
	allGlobalInstances: function() {
		return Object.values(Global).select(function(ea) {return ea instanceof Layer})
	},
})

cop.create('ScriptingLayer')
.beGlobal()
.refineClass(Morph, {
	layerMenuAddWithLayerItems: function() {
		var self = this;
		var list =  Layer.allGlobalInstances()
			.invoke('getName')
			.sort()
			.collect(function(ea) {return [ea, function() {
				self.world().setStatusMessage(
					"enable withLayer " + ea + " in " + self, Color.blue, 10)
				self.addWithLayer(Global[ea])
			}]});
		if (list.length == 0) 
			return function() {}
		else
			return list	
	},

	layerMenuRemoveWithLayerItems: function() {
		var self = this;
		var list =  this.getWithLayers()
			.invoke('getName')
			.sort()
			.collect(function(ea) {return [ea, 
				function() {
					self.world().setStatusMessage(
							"remove withLayer " + ea + " in " + self, Color.blue, 10);
					self.removeWithLayer(Global[ea])										
				}]});
		if (list.length == 0) 
			return function() {}
		else
			return list
	},

	morphMenu: function(evt) {
		var menu;
		// TOTO remove this workaround ContextJS issue (morphMenu is overriden in TextMorph and called with $super) 
		withoutLayers([ScriptingLayer], function() {
			menu= cop.proceed(evt);
		});
		var items =  [
			["startSteppingScripts", this.startSteppingScripts],		
			["copy to puplic PartsBin", this.copyToPartsBin],		
			["copy to my PartsBin", this.interactiveCopyToMyPartsBin],
			["layers", [
				["addWithLayer", this.layerMenuAddWithLayerItems()],		
				["removeWithLayer", this.layerMenuRemoveWithLayerItems()]]],		
		];

		if (Object.isFunction(this.reset)) items.push(["reset", this.reset])

		menu.addItem(["Scripting", items])
		return menu;
	}
}).refineClass(WorldMorph, {

	debuggingSubMenuItems: function(evt) {
		var items = cop.proceed(evt);
		items.push(["remove broken attribute connections", this.disconnectBrokenAttributeConnections.bind(this)])
		items.push(["display connections", this.displayConnections.bind(this)])
		items.push(["hide connections", this.hideConnections.bind(this)])
		return items;
	},

	disconnectBrokenAttributeConnections: function() {
		this.withAllSubmorphsDo(function() {
			if (this.attributeConnections == undefined) return; 			
			this.attributeConnections.each(function(ea) {
				if (ea.getTargetObj() == null) {
					alert('disconnect ' + ea)
					ea.disconnect();				
				}
			})
		})

	},
	complexMorphsSubMenuItems: function(evt) {
		var items = cop.proceed(evt);
		items.push(["PartsBin", 
			function(){ 
				// var partsBin = new lively.Scripting.PartsBin(URL.codeBase.withFilename("PartsBin/"));
				// partsBin.openInWorld(evt.mousePoint, "partsBin");
				// partsBin.loadAll();

				var partsBin = lively.PartsBin.getPart('PartsBinReloaded', 'PartsBin/Tools')
				partsBin.openInWorld(evt.mousePoint)

			}]);
		return items
	},
	helpSubMenuItems: function(evt) {
		var items = cop.proceed(evt);
		items.push(["LivelyHelp", 
			function(){ 
				var morph = lively.PartsBin.getPart('LivelyHelp', 'PartsBin/Documentation')
				if (!morph) {
					alert("Sorry the LivelyHelp is gone! ")
					return
				}
				morph.openInWorld(evt.mousePoint)

			}]);
		items.push(["Tutorial", 
			function(){ 
				var morph = lively.PartsBin.getPart('ScriptingTutorial', 'PartsBin/Documentation');
				if (!morph) {
					alert("Sorry the Tutorial is gone! ")
					return
				}
				morph.openInWorld(evt.mousePoint)

			}]);
		return items
	},

	displayConnections: function() {
		this.withAllSubmorphsDo(function() { showConnections(this) })
	},
	hideConnections: function() {
		hideAllConnections(WorldMorph.current());
	},

	

})

Morph.addMethods({
	showNameField: function() {
		if (this.isEpimorph) return;

		this.removeNameField();
		var nameField = new TextMorph(new Rectangle(0,-15,100,20), this.name);
		this.addMorph(nameField);
		nameField.beLabel();
		nameField.applyStyle({
			fill: null, borderWidth: 0, strokeOpacity: 0, 
			textColor: Color.gray.darker(), fontSize: 10})
		nameField.isNameField = true;
		connect(this, 'name', nameField, 'setTextString')
	},

	removeNameField: function() {
		this.submorphs.select(function(ea) {return ea.isNameField}).invoke('remove')
	},

	isShowingNameField: function() {
		return this.submorphs.detect(function(ea) {return ea.isNameField}) !== undefined
	},

	showAllNameFields: function() {
		this.removeNameField();
		this.submorphs.invoke('showAllNameFields');
		this.showNameField();
	},

	hideAllNameFields: function() {
		this.removeNameField();
		this.submorphs.invoke('hideAllNameFields');
	},

})


cop.create('DisplayMorphNameLayer').refineClass(Morph, {
	subMenuPropertiesItems: function(evt) {
		var items = cop.proceed(evt);
		if(this.isShowingNameField() ) {
			items.push(["[X] show name field", this.removeNameField])
		} else {
			items.push(["[] show name field", this.showNameField])
		}
		items.push(["show all name fields", this.showAllNameFields])
		items.push(["hide all name fields", this.hideAllNameFields])
		return items
	}
}).beGlobal()

cop.create('CopyCheapListMorphLayer').refineClass(CheapListMorph, {
	morphMenu: function(evt){
		var menu = cop.proceed(evt);
		var self = this;
		menu.addItem(["duplicate as TextMorph", function() {
			evt.hand.addMorph(new TextMorph(new Rectangle(0,0,500,300), self.textString))
		}])

		return menu
	}
})
CopyCheapListMorphLayer.beGlobal()

BoxMorph.subclass("lively.Scripting.DuplicatorPanel", {
	padding: new Rectangle(5, 5, 0, 0),

	initialize: function($super, position, numberOfSlots) {
		numberOfSlots = numberOfSlots || 7;
		var totalWidth = this.slotWidth * numberOfSlots + this.borderSpace;
		$super(new lively.scene.Rectangle(position.extent(pt(totalWidth, 50))));
		this.applyStyle({borderWidth: 2, borderColor: Color.darkGray, fill: Color.white});
		this.shapeRoundEdgesBy(10);
		this.layoutManager = new HorizontalLayout();
		for (var i=0; i < numberOfSlots; i++) {
			this.addSlot(i);
		};
		this.setExtent(this.submorphBounds().extent().addPt(pt(10,10)));
	},

	suppressHandles: true,

	addSlot: function(n) {
		var slot = new lively.Scripting.DuplicatorMorph();
		this.addMorph(slot);
	}
});


/*
 * MorphDuplicatorMorph 
 * - duplicates its submorph and puts it into the hand
 * - can be customized by dropping a morph on it
 */
ClipMorph.subclass('lively.Scripting.DuplicatorMorph', {
	
	defaultExtent: pt(80,80),

	openForDragAndDrop: true,


	initialize: function($super) {
		$super(pt(0,0).extent(this.defaultExtent));
		this.applyStyle({borderWidth: 1, borderColor: Color.gray, fill: Color.lightGray});
		this.shapeRoundEdgesBy(16);
		this.setStrokeOpacity(1);
		// this.beClipMorph();
	},

	suppressHandles: true,

	addMorph: function($super, morph) {
		var oldTarget = this.target();
		if (oldTarget)
		 	oldTarget.remove();
		this.setScale(1);
		var targetWidth = morph.shape.bounds().width
		var scale = targetWidth ? this.bounds().width / targetWidth : 1;


		morph.withAllSubmorphsDo(function() {
			if (this.mouseHandler)
				this.ignoreEvents();
			this.disabledEventsForDuplicating = true
		});

		$super(morph);

		morph.setScale(scale - 0.1);		
		morph.centerAt(this.shape.bounds().center());
		console.log("scale " + scale);

	},

	okToBeGrabbedBy: function() {
		return false		
	},
	
	target: function() {
		return this.submorphs[0]
	},

	handlesMouseDown: Functions.True,

	onMouseDown: function(evt) {
		if (!this.target()) return;
		var duplicate = this.target().duplicate();

		duplicate.withAllSubmorphsDo(function() {
			if (this.disabledEventsForDuplicating)
				this.enableEvents();
			delete this.disabledEventsForDuplicating;
		});
		duplicate.setPosition(pt(0,0));
		duplicate.setScale(1);
		evt.hand.grabMorph(duplicate, evt)	
	},

	onMouseMove: Functions.True,
});

Object.extend(Morph, {
	makeDefaultDuplicatorPanel: function(point) {
		var pane = new lively.Scripting.DuplicatorPanel(point, 10);
		var i = 0;
		var add = function(m) {pane.submorphs[i].addMorph(m); i++};

		add(new TextMorph(pt(0,0).extent(pt(120, 10)), "Text"));
		add(Morph.makeLine([pt(0,0), pt(60, 30)], 2, Color.black));
		add(Morph.makeConnector(pt(0,0), pt(60, 30)));
		add(Morph.makeRectangle(pt(0,0), pt(60, 30)));
		add(Morph.makeCircle(pt(0,0), 25));
		add(Morph.makeStar(pt(0,0)));
		add(new MarkerMorph(pt(0,0).extent(pt(50, 50))));
		
		return pane
	}
});


cop.create('AttributeConnectionMorphLayer')
.refineClass(lively.Connector.ConnectorMorph, {

	setup: function() {
		this.setBorderWidth(2);
		var color = Color.blue;
		this.setBorderColor(color);
		this.arrowHead.head.setFill(color);
		this.arrowHead.head.setBorderColor(color);
		this.isMetaMorph = true;
		this.labelStyle = {fill: Color.white, textColor: Color.blue};
		lively.bindings.connect(this, 'geometryChanged', this, 'updateLabelPositions');
	}, 

	updateLabelPositions: function() {
		if (this.startLabel) this.startLabel.setPosition(this.getStartPos());
		if (this.endLabel) this.endLabel.setPosition(this.getEndPos());
		if (this.middleLabel) this.middleLabel.setPosition(this.getRelativePoint(0.5));
	},

	setupStartLabel: function() {
		var startLabel = new TextMorph(new Rectangle(0, 0, 100, 30), 
			"from: " + this.connection.getSourceAttrName()).beLabel();
		startLabel.applyStyle(this.labelStyle);
		this.addMorph(startLabel);
		this.startLabel = startLabel;
	}, 

	setConverter: function(newSource) {
		var func
		try {
			func = eval("(" + newSource + ")"); // test if correct
			this.connection.converterString = newSource;
			this.connection.converter = null;
		} catch(error) {
			alert("Could not update converter in " + this.connection)
			this.world().logError(error)
		}
	},

	setupMiddleLabel: function() {
		var c = this.connection;
		if (!c) return;
		if (c.converterString) {
			var middleLabel = new TextMorph(new Rectangle(0,0, 300,30), c.converterString);
			middleLabel.setWithoutLayers([ConnectorMorphLayer]); // normal handle behavior!
			middleLabel.setWithLayers([SyntaxHighlightLayer]);
			middleLabel.highlightJavaScriptSyntax();
			this.addMorph(middleLabel);
			middleLabel.applyStyle({fill: Color.white,  fillOpacity: 0.5, borderRadius: 6, fontSize: 18});
			middleLabel.noEval = true;
			middleLabel.addScript(function getDoitContext() { return this.owner.connection} )

			lively.bindings.connect(middleLabel, "savedTextString", this, 'setConverter')
			lively.bindings.connect(middleLabel, "savedTextString", middleLabel, 'highlightJavaScriptSyntax')
			this.middleLabel = middleLabel;
		}
	}, 

	setupEndLabel: function() {
		var endLabel = new TextMorph(new Rectangle(0,0, 100,30), 
			"to: " + this.connection.getTargetMethodName()).beLabel();
		endLabel.applyStyle(this.labelStyle);
		this.addMorph(endLabel);
		this.endLabel = endLabel;
	},

	editConverter: function() {
		if (!this.connection) return;

		// initialize defefault
		if (!this.connection.converterString) this.connection.converterString = "function(v) { return v }";

		this.setupMiddleLabel();
		this.updateLabelPositions();
	},

	morphMenu: function(evt) {
		// var menu = cop.proceed(evt);
		var menu = new MenuMorph([], this)

		menu.addItem(['remove connection', function() {
			if (!this.connection) return;
			alertOK("disconnecting " + this.connection)
			this.remove();
			this.connection.disconnect();
			}], 0)

		menu.addItem(['edit converter', this.editConverter], 1)
		menu.addItem(['hide', this.remove], 2)

		return menu
	}		
}); 


cop.create('ScriptingConnectionsLayer')
.refineClass(TextMorph, {
	getConnectionTargets: function() {
		return this.customConnectionTarget()
			.concat(["setTextString"]);
	},
	getConnectionAttributes: function() {
		var result = ["origin", "textString", "savedTextString"];
		if (this.connectionSources)
			result = result.concat(this.connectionSources)
		return result
	},
	acceptsDropping: function(morph) {
		// for dropping a connection onto a text morph (normally this wouldnt work)
		return morph.isConnectionHandle ? true : cop.proceed(morph);
	},
})
.refineClass(ScriptableButtonMorph, {
	getConnectionAttributes: function() {
		var result = ["doAction"];
		if (this.connectionSources)
			result = result.concat(this.connectionSources)
		return result;
	},
})
.refineClass(Morph, {
	customConnectionTarget: function() {
		var self = this;
		var result = Functions.own(self)
			.select(function(name) { return self[name].hasLivelyClosure });
		if (this.connectionTargets)
			result = result.concat(this.connectionTargets)
		return result.sort()
	},
	
	getConnectionTargets: function() {
		return this.customConnectionTarget()
			.concat(["setPosition", "setCenter", "setScale", "setExtent", "setBounds"]);
	},

	getConnectionAttributes: function() {
		var result = ["origin", "geometryChanged", "fullBounds", "value"];
		if (this.connectionSources)
			result = result.concat(this.connectionSources)
		return result
	},

	connectionAttributeMenuItems: function(evt) {		
		var self = this;
		return this.getConnectionAttributes()
			.collect(function(eaAttribute){ 
				return [eaAttribute, function() {
						var handle = Morph.makeRectangle(0, 0, 15,15);
						handle.openInWorld(evt.hand.getPosition().addPt(pt(-5,-5)))
						handle.suppressHandles = true;
						handle.isConnectionHandle = true;
						handle.addScript(function dropMeOnMorph (receiver) {
							if (this.receiver instanceof WorldMorph) {
								this.connector.remove();
								this.remove();
							}
							this.connector.connectEndMorph(receiver);
							
							var items = [
								["cancel", function() {this.remove();this.connector.remove()}]
							];
							var connector = this.connector;

							if (!receiver.getConnectionTargets) {
								connector.remove(); this.remove();
								return;
							}

							items = items.concat(receiver.getConnectionTargets().collect(
								function(eaTargetSelector) {
									return [eaTargetSelector, 
										function() {
											connector.connection = connect(
												connector.startMorph, connector.sourceAttributeName,  
												connector.endMorph, eaTargetSelector);
											connector.setupStartLabel();
											connector.setupEndLabel();
											connector.updateLabelPositions();
										}]
								}));

							var menu = new MenuMorph(items, this);
							menu.openIn(this.world(), this.connector.getGlobalEndPos())
				
							alertOK("connect to " + receiver)
							this.remove();


						});


						var connector = Morph.makeConnector(pt(100,100),evt.mousePoint.addPt(pt(200,0)));
						handle.connector = connector;
						connector.addWithLayer(AttributeConnectionMorphLayer);
						connector.setup();
						
						connector.openInWorld();
						connector.connectMorphs(self, handle);
						connector.sourceAttributeName = eaAttribute;					

						evt.hand.addMorph(handle)
				}]
			})
	},

	morphMenu: function(evt) {
		var menu = cop.proceed(evt);
		menu.addItem(["connect attribute", this.connectionAttributeMenuItems(evt)], 5)
	
		return menu
	}
});
SliderMorph.addMethods({
	connectionTargets: ['setValue'],
});

}) // end of module