/*
 * Scratch Project Editor and Player
 * Copyright (C) 2014 Massachusetts Institute of Technology
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

// ScratchObj.as
// John Maloney, April 2010
//
// This is the superclass for both ScratchStage and ScratchSprite,
// containing the variables and methods common to both.

package scratch {
	import flash.display.Bitmap;
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.ColorTransform;
	import flash.utils.getTimer;
	
	import blocks.Block;
	import blocks.BlockArg;
	import blocks.BlockIO;
	
	import cc.makeblock.util.BlockUtil;
	
	import filters.FilterPack;
	
	import interpreter.RobotHelper;
	import interpreter.Variable;
	
	import translation.Translator;
	
	import util.JSON;
	
	import watchers.ListWatcher;

public class ScratchObj extends Sprite {

	[Embed(source='../assets/pop.wav', mimeType='application/octet-stream')] protected static var Pop:Class;

	public static const STAGEW:int = 480;
	public static const STAGEH:int = 360;

	public var objName:String = 'no name';
	public var isStage:Boolean = false;
	public var variables:Array = [];
	public var defaultVar:String = "";
	public var lists:Array = [];
	public var scripts:Array = [];
	public var scriptComments:Array = [];
	public var sounds:Array = [];
	public var costumes:Array = [];
	public var currentCostumeIndex:Number;
	public var volume:Number = 100;
	public var instrument:int = 0;
	public var filterPack:FilterPack;
	public var isClone:Boolean;

	public var img:Sprite; // holds a bitmap or svg object, after applying image filters, scale, and rotation
	private var lastCostume:ScratchCostume;

	// Caches used by the interpreter:
	public var listCache:Object = {};
	public var procCache:Object = {};
	public var varCache:Object = {};

	public function clearCaches():void {
		// Clear the list, procedure, and variable caches for this object.
		listCache = {};
		procCache = {};
		varCache = {};
	}

	public function allObjects():Array { return [this] }

	public function deleteCostume(c:ScratchCostume):void {
		if (costumes.length < 2) return; // a sprite must have at least one costume
		var i:int = costumes.indexOf(c);
		if (i < 0) return;
		costumes.splice(i, 1);
		if (currentCostumeIndex >= i) showCostume(currentCostumeIndex - 1);
		if (IQCar.app) IQCar.app.setSaveNeeded();
	}

	public function deleteSound(snd:ScratchSound):void {
		var i:int = sounds.indexOf(snd);
		if (i < 0) return;
		sounds.splice(i, 1);
		if (IQCar.app) IQCar.app.setSaveNeeded();
	}

	public function showCostumeNamed(n:String):void {
		var i:int = indexOfCostumeNamed(n);
		if (i >= 0) showCostume(i);
	}

	public function indexOfCostumeNamed(n:String):int {
		for (var i:int = 0; i < costumes.length; i++) {
			if (ScratchCostume(costumes[i]).costumeName == n) return i;
		}
		return -1;
	}

	public function showCostume(costumeIndex:Number):void {
		if (isNaNOrInfinity(costumeIndex)) costumeIndex = 0;
		currentCostumeIndex = costumeIndex % costumes.length;
		if (currentCostumeIndex < 0) currentCostumeIndex += costumes.length;
		var c:ScratchCostume = currentCostume();
		if (c == lastCostume) return; // optimization: already showing that costume
		lastCostume = c.isBitmap() ? c : null; // cache only bitmap costumes for now

		updateImage();
	}

	public function updateCostume():void { updateImage() }

	public function currentCostume():ScratchCostume {
		return costumes[Math.round(currentCostumeIndex) % costumes.length]
	}

	public function costumeNumber():int {
		// One-based costume number as seen by user (currentCostumeIndex is 0-based)
		return currentCostumeIndex + 1;
	}

	public function isCostumeNameUsed(name:String):Boolean {
		var existingNames:Array = [];
		for each (var c:ScratchCostume in costumes) {
			existingNames.push(c.costumeName.toLowerCase());
		}
		return (existingNames.indexOf(name.toLowerCase()) > -1);
	}

	public function unusedCostumeName(baseName:String = ''):String {
		// Create a unique costume name by appending a number if necessary.
		if (baseName == '') baseName = Translator.map(isStage ? 'backdrop1' : 'costume1');
		var existingNames:Array = [];
		for each (var c:ScratchCostume in costumes) {
			existingNames.push(c.costumeName.toLowerCase());
		}
		var lcBaseName:String = baseName.toLowerCase();
		if (existingNames.indexOf(lcBaseName) < 0) return baseName; // basename is not already used
		lcBaseName = withoutTrailingDigits(lcBaseName);
		var i:int = 2;
		while (existingNames.indexOf(lcBaseName + i) >= 0) { i++ } // find an unused name
		return withoutTrailingDigits(baseName) + i;
	}

	public function unusedSoundName(baseName:String = ''):String {
		// Create a unique sound name by appending a number if necessary.
		if (baseName == '') baseName = 'sound';
		var existingNames:Array = [];
		for each (var snd:ScratchSound in sounds) {
			existingNames.push(snd.soundName.toLowerCase());
		}
		var lcBaseName:String = baseName.toLowerCase();
		if (existingNames.indexOf(lcBaseName) < 0) return baseName; // basename is not already used
		lcBaseName = withoutTrailingDigits(lcBaseName);
		var i:int = 2;
		while (existingNames.indexOf(lcBaseName + i) >= 0) { i++ } // find an unused name
		return withoutTrailingDigits(baseName) + i;
	}

	protected function withoutTrailingDigits(s:String):String {
		var i:int = s.length - 1;
		while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--;
		return s.slice(0, i + 1);
	}

	protected function updateImage():void {
		var currChild:DisplayObject = (img.numChildren == 1 ? img.getChildAt(0) : null);
		var currDispObj:DisplayObject = currentCostume().displayObj();
		var change:Boolean = (currChild != currDispObj);
		if(change) {
			while (img.numChildren > 0) img.removeChildAt(0);
			img.addChild(currDispObj);
		}
		clearCachedBitmap();
		adjustForRotationCenter();
		updateRenderDetails(0);
	}

	protected function updateRenderDetails(reason:uint):void {
		if(((parent && parent is ScratchStage) || this is ScratchStage)) {
			var renderOpts:Object = {};
			var costume:ScratchCostume = currentCostume();

			// 0 - costume change, 1 - rotation style change
			if(reason == 0) {
				if(costume && costume.baseLayerID == ScratchCostume.WasEdited)
					costume.prepareToSave();

				var id:String = (costume ? costume.baseLayerMD5 : null);
				if(!id) id = objName + (costume ? costume.costumeName : '_' + currentCostumeIndex);
				else if(costume && costume.textLayerMD5) id += costume.textLayerMD5;

				renderOpts.bitmap = (costume && costume.bitmap ? costume.bitmap : null);
			}

			// TODO: Clip original bitmap to match visible bounds?
			if(reason == 1)
				renderOpts.costumeFlipped = (this is ScratchSprite ? (this as ScratchSprite).isCostumeFlipped() : false);

			if(reason == 0) {
				if(this is ScratchSprite) {
					renderOpts.bounds = (this as ScratchSprite).getVisibleBounds(this);
					renderOpts.raw_bounds = getBounds(this);
				}
				else
					renderOpts.bounds = getBounds(this);
			}

			if(parent is ScratchStage)
				(parent as ScratchStage).updateRender(this, id, renderOpts);
			else
				(this as ScratchStage).updateRender(img, id, renderOpts);
		}
	}

	protected function adjustForRotationCenter():void {
		// Adjust the offset of img relative to it's parent. If this object is a
		// ScratchSprite, then img is adusted based on the costume's rotation center.
		// If it is a ScratchStage, img is centered on the stage.
		var costumeObj:DisplayObject = img.getChildAt(0);
		if (isStage) {
			if (costumeObj is Bitmap) {
				img.x = (STAGEW - costumeObj.width) / 2;
				img.y = (STAGEH - costumeObj.height) / 2;
			} else {
				// SVG costume; don't center for now
				img.x = img.y = 0;
			}
		} else {
			var c:ScratchCostume = currentCostume();
			costumeObj.scaleX = 1 / c.bitmapResolution; // don't flip
			img.x = -c.rotationCenterX / c.bitmapResolution;
			img.y = -c.rotationCenterY / c.bitmapResolution;
			if ((this as ScratchSprite).isCostumeFlipped()) {
				costumeObj.scaleX = -1 / c.bitmapResolution; // flip
				img.x = -img.x;
			}
		}
	}

	public function clearCachedBitmap():void {
		// Does nothing here, but overridden in ScratchSprite
	}

	static private var cTrans:ColorTransform = new ColorTransform();
	public function applyFilters(forDragging:Boolean = false):void {
		img.filters = filterPack.buildFilters(forDragging);
		clearCachedBitmap();
//		if(!IQCar.app.isIn3D || forDragging) {
			var n:Number = Math.max(0, Math.min(filterPack.getFilterSetting('ghost'), 100));
			cTrans.alphaMultiplier = 1.0 - (n / 100.0);
			n = 255 * Math.max(-100, Math.min(filterPack.getFilterSetting('brightness'), 100)) / 100;
			cTrans.redOffset = cTrans.greenOffset = cTrans.blueOffset = n;
			img.transform.colorTransform = cTrans;
//		}
//		else {
//			updateEffects();
//		}
	}

	protected function updateEffects():void {
		if((parent && parent is ScratchStage) || this is ScratchStage) {
			if(parent is ScratchStage)
				(parent as ScratchStage).updateSpriteEffects(this, filterPack.getAllSettings());
			else {
				(this as ScratchStage).updateSpriteEffects(img, filterPack.getAllSettings());
//				if((this as ScratchStage).videoImage)
//					(this as ScratchStage).updateSpriteEffects((this as ScratchStage).videoImage, filterPack.getAllSettings());
			}
		}
	}

	protected function shapeChangedByFilter():Boolean {
		var filtersObj:Object = filterPack.getAllSettings();
		return (filtersObj['fisheye'] !== 0 || filtersObj['whirl'] !== 0 || filtersObj['mosaic'] !== 0);
	}

	static public const clearColorTrans:ColorTransform = new ColorTransform();
	public function clearFilters():void {
		filterPack.resetAllFilters();
		img.filters = [];
		img.transform.colorTransform = clearColorTrans;
		clearCachedBitmap();

		if(parent && parent is ScratchStage) {
			(parent as ScratchStage).updateSpriteEffects(this, null);
		}
	}

	public function setMedia(media:Array, currentCostume:ScratchCostume):void {
		var newCostumes:Array = [];
		sounds = [];
		for each (var m:* in media) {
			if (m is ScratchSound) sounds.push(m);
			if (m is ScratchCostume) newCostumes.push(m);
		}
		if (newCostumes.length > 0) costumes = newCostumes;
		var i:int = costumes.indexOf(currentCostume);
		currentCostumeIndex = (i < 0) ? 0 : i;
		showCostume(i);
	}

	public function defaultArgsFor(op:String, specDefaults:Array):Array {
		// Return an array of default parameter values for the given operation (primitive name).
		// For most ops, this will simply return the array of default arg values from the command spec.
		var app:IQCar = root as IQCar;
		var sprites:Array;

		if ((['broadcast:', 'doBroadcastAndWait', 'whenIReceive'].indexOf(op)) > -1) {
			var msgs:Array = app.runtime.collectBroadcasts();
			return (msgs.length > 0) ? [msgs[0]] : ['message1'];
		}
		if ((['lookLike:', 'startScene', 'startSceneAndWait', 'whenSceneStarts'].indexOf(op)) > -1) {
			return [costumes[costumes.length - 1].costumeName];
		}
		if ((['playSound:', 'doPlaySoundAndWait'].indexOf(op)) > -1) {
			return (sounds.length > 0) ? [sounds[sounds.length - 1].soundName] : [''];
		}
		if ('createCloneOf' == op) {
			if (!isStage) return ['_myself_'];
			sprites = app.stagePane.sprites();
			return (sprites.length > 0) ? [sprites[sprites.length - 1].objName] : [''];
		}
		if ('getAttribute:of:' == op) {
			sprites = app.stagePane.sprites();
			return (sprites.length > 0) ? ['x position', sprites[sprites.length - 1].objName] : ['volume', '_stage_'];
		}

		if ('setVar:to:' == op) return [defaultVarName(), 0];
		if ('changeVar:by:' == op) return [defaultVarName(), 1];
		if ('showVariable:' == op) return [defaultVarName()];
		if ('hideVariable:' == op) return [defaultVarName()];

		if ('append:toList:' == op) return ['thing', defaultListName()];
		if ('deleteLine:ofList:' == op) return [1, defaultListName()];
		if ('insert:at:ofList:' == op) return ['thing', 1, defaultListName()];
		if ('setLine:ofList:to:' == op) return [1, defaultListName(), 'thing'];
		if ('getLine:ofList:' == op) return [1, defaultListName()];
		if ('lineCountOfList:' == op) return [defaultListName()];
		if ('list:contains:' == op) return [defaultListName(), 'thing'];
		if ('showList:' == op) return [defaultListName()];
		if ('hideList:' == op) return [defaultListName()];

		return specDefaults;
	}

	public function defaultVarName():String {
		//if (defaultVar != "")
		//{
		//  return defaultVar;
		//}
		
		for(var i:int=variables.length-1; i>=0; i--){
			var varName:String = variables[i].name;
			if(varName == null){
				continue;
			}
			if(!RobotHelper.isAutoVarName(varName)){
				return varName;
			}
		}
//		if (variables.length > 0) return variables[variables.length - 1].name; // local var
		return isStage ? '' : IQCar.app.stagePane.defaultVarName(); // global var, if any
	}

	public function defaultListName():String {
		if (lists.length > 0) return lists[lists.length - 1].listName; // local list
		return isStage ? '' : IQCar.app.stagePane.defaultListName(); // global list, if any
	}

	/* Scripts */

	public function allBlocks():Array {
		var result:Array = [];
		for each (var script:Block in scripts) {
			script.allBlocksDo(function(b:Block):void { result.push(b) });
		}
		return result;
	}

	/* Sounds */

	public function findSound(arg:*):ScratchSound {
		// Return a sound describe by arg, which can be a string (sound name),
		// a number (sound index), or a string representing a number (sound index).
		if (sounds.length == 0) return null;
		if (typeof(arg) == 'number') {
			var i:int = Math.round(arg - 1) % sounds.length;
			if (i < 0) i += sounds.length; // ensure positive
			return sounds[i];
		} else if (typeof(arg) == 'string') {
			for each (var snd:ScratchSound in sounds) {
				if (snd.soundName == arg) return snd; // arg matches a sound name
			}
			// try converting string arg to a number
			var n:Number = Number(arg);
			if (isNaN(n)) return null;
			return findSound(n);
		}
		return null;
	}

	public function setVolume(vol:Number):void {
		volume = Math.max(0, Math.min(vol, 100));
	}

	public function setInstrument(instr:Number):void {
		instrument = Math.max(1, Math.min(Math.round(instr), 128));
	}

	/* Procedures */

	public function procedureDefinitions():Array {
		var result:Array = [];
		for (var i:int = 0; i < scripts.length; i++) {
			var b:Block = scripts[i] as Block;
			if (b && (b.op == Specs.PROCEDURE_DEF)) result.push(b);
		}
		return result;
	}

	public function lookupProcedure(procName:String):Block {
		for (var i:int = 0; i < scripts.length; i++) {
			var b:Block = scripts[i] as Block;
			if (b && (b.op == Specs.PROCEDURE_DEF) && (b.spec == procName)) return b;
		}
		return null;
	}

	/* Variables */

	public function varNames():Array {
		var varList:Array = [];
		for each (var v:Variable in variables) {
			if(v.name == null){
				continue;
			}
			varList.push(v.name);
		}
		return varList;
	}

	public function setVarTo(varName:String, value:*):void {
		var v:Variable = lookupOrCreateVar(varName);
		v.value = value;
		IQCar.app.runtime.updateVariable(v);
	}

	public function ownsVar(varName:String):Boolean {
		// Return true if this object owns a variable of the given name.
		for each (var v:Variable in variables) {
			if (v.name != null && v.name == varName) return true;
		}
		return false;
	}

	public function setdefaultArgVar(varName:String):void{
	  defaultVar = varName;
	}
	public function lookupOrCreateVar(varName:String):Variable {
		// Lookup and return a variable. If lookup fails, create the variable in this object.
		var v:Variable = lookupVar(varName);
		if (v == null) { // not found; create it
			v = new Variable(varName, 0);
			variables.push(v);
		}
		return v;
	}

	public function lookupVar(varName:String):Variable {
		// Look for variable first in sprite (local), then stage (global).
		// Return null if not found.
		var v:Variable;
		for each (v in variables) {
			if (v.name == varName) return v;
		}
		for each (v in IQCar.app.stagePane.variables) {
			if (v.name == varName) return v;
		}
		return null;
	}

	public function deleteVar(varToDelete:String):void {
		var newVars:Array = [];
		for each (var v:Variable in variables) {
			if (v.name == varToDelete) {
				if ((v.watcher != null) && (v.watcher.parent != null)) {
					v.watcher.parent.removeChild(v.watcher);
				}
				v.watcher = v.value = null;
			}
			else newVars.push(v);
		}
		variables = newVars;
	}

	/* Lists */

	public function listNames():Array {
		var result:Array = [];
		for each (var list:ListWatcher in lists) result.push(list.listName);
		return result;
	}

	public function ownsList(listName:String):Boolean {
		// Return true if this object owns a list of the given name.
		for each (var w:ListWatcher in lists) {
			if (w.listName == listName) return true;
		}
		return false;
	}

	public function lookupOrCreateList(listName:String):ListWatcher {
		// Look and return a list. If lookup fails, create the list in this object.
		var list:ListWatcher = lookupList(listName);
		if (list == null) { // not found; create it
			list = new ListWatcher(listName, [], this);
			lists.push(list);
		}
		return list;
	}

	public function lookupList(listName:String):ListWatcher {
		// Look for list first in this sprite (local), then stage (global).
		// Return null if not found.
		var list:ListWatcher;
		for each (list in lists) {
			if (list.listName == listName) return list;
		}
		for each (list in IQCar.app.stagePane.lists) {
			if (list.listName == listName) return list;
		}
		return null;
	}

	public function deleteList(listName:String):void {
		var newLists:Array = [];
		for each (var w:ListWatcher in lists) {
			if (w.listName == listName) {
				if (w.parent) w.parent.removeChild(w);
			} else {
				newLists.push(w);
			}
		}
		lists = newLists;
	}

	/* Events */

	private const DOUBLE_CLICK_MSECS:int = 300;
	private var lastClickTime:uint;

	public function click(evt:MouseEvent):void {
		var app:IQCar = root as IQCar;
		if (!app) return;
		var now:uint = getTimer();
		app.runtime.startClickedHats(this);
		if ((now - lastClickTime) < DOUBLE_CLICK_MSECS) {
			if (!isStage && ScratchSprite(this).isClone) return;
			app.selectSprite(this);
			lastClickTime = 0;
		} else {
			lastClickTime = now;
		}
	}
	
	public function onSpriteNameChanged(oldName:String, newName:String):void
	{
		function changeSpriteName(b:Block):void
		{
			switch(b.op){
				case "gotoSpriteOrMouse:":
				case "pointTowards:":
					break;
				default:
					return;
			}
			var blockArg:BlockArg = b.args[0];
			if(blockArg.argValue == oldName){
				blockArg.setArgValue(newName);
			}
		}
		for each (var b:Block in scripts) {
			BlockUtil.ForEach(b, changeSpriteName);
		}
	}
	
	/* Translation */

	public function updateScriptsAfterTranslation():void {
		// Update the scripts of this object after switching languages.
		var newScripts:Array = [];
		for each (var b:Block in scripts) {
			var newStack:Block = BlockIO.arrayToStack(BlockIO.stackToArray(b), isStage);
			newStack.x = b.x;
			newStack.y = b.y;
			newScripts.push(newStack);
			if (b.parent) { // stack in the scripts pane; replace it
				b.parent.addChild(newStack);
				b.parent.removeChild(b);
			}
		}
		scripts = newScripts;
		var blockList:Array = allBlocks();
		for each (var c:ScratchComment in scriptComments) {
			c.updateBlockRef(blockList);
		}
	}

	/* Saving */

	public function writeJSON(json:util.JSON):void {
		var allScripts:Array = [];
		for each (var b:Block in scripts) {
			allScripts.push([b.x, b.y, BlockIO.stackToArray(b)]);
		}
		var allComments:Array = [];
		for each (var c:ScratchComment in scriptComments) {
			allComments.push(c.toArray());
		}
		json.writeKeyValue('objName', objName);
		if (variables.length > 0)	json.writeKeyValue('variables', variables);
		if (lists.length > 0)		json.writeKeyValue('lists', lists);
		if (scripts.length > 0)		json.writeKeyValue('scripts', allScripts);
		if (scriptComments.length > 0) json.writeKeyValue('scriptComments', allComments);
		if (sounds.length > 0)		json.writeKeyValue('sounds', sounds);
		json.writeKeyValue('costumes', costumes);
		json.writeKeyValue('currentCostumeIndex', currentCostumeIndex);
	}

	public function readJSON(jsonObj:Object):void {
		objName = jsonObj.objName;
		variables = (jsonObj.variables == undefined) ? [] : jsonObj.variables;
		for (var i:int = 0; i < variables.length; i++) {
			var varObj:Object = variables[i];
			variables[i] = IQCar.app.runtime.makeVariable(varObj);
		}
		lists = (jsonObj.lists == undefined) ? [] : jsonObj.lists;
		scripts = (jsonObj.scripts == undefined) ? [] : jsonObj.scripts;
		scriptComments = (jsonObj.scriptComments == undefined) ? [] : jsonObj.scriptComments;
		sounds = (jsonObj.sounds == undefined) ? [] : jsonObj.sounds;
		costumes = jsonObj.costumes;
		currentCostumeIndex = jsonObj.currentCostumeIndex;
		if (isNaNOrInfinity(currentCostumeIndex)) currentCostumeIndex = 0;
	}

	private function isNaNOrInfinity(n:Number):Boolean {
		if (n != n) return true; // NaN
		if (n == Number.POSITIVE_INFINITY) return true;
		if (n == Number.NEGATIVE_INFINITY) return true;
		return false;
	}

	public function instantiateFromJSON(newStage:ScratchStage):void {
		var i:int, jsonObj:Object;

		// lists
		for (i = 0; i < lists.length; i++) {
			jsonObj = lists[i];
			var newList:ListWatcher = new ListWatcher();
			newList.readJSON(jsonObj);
			newList.target = this;
			newStage.addChild(newList);
			newList.updateTitleAndContents();
			lists[i] = newList;
		}

		// scripts
		scripts.length = scripts.length > 1 ? 1 : scripts.length;
		for (i = 0; i < scripts.length; i++) {
			// entries are of the form: [x y stack]
			var entry:Array = scripts[i];
			var b:Block = BlockIO.arrayToStack(entry[2], isStage);
			b.x = entry[0];
			b.y = entry[1];
			scripts[i] = b;
		}

		// script comments
		for (i = 0; i < scriptComments.length; i++) {
			scriptComments[i] = ScratchComment.fromArray(scriptComments[i]);
		}

		// sounds
		for (i = 0; i < sounds.length; i++) {
			jsonObj = sounds[i];
			sounds[i] = new ScratchSound('json temp', null);
			sounds[i].readJSON(jsonObj);
		}

		// costumes
		for (i = 0; i < costumes.length; i++) {
			jsonObj = costumes[i];
			costumes[i] = new ScratchCostume('json temp', null);
			costumes[i].readJSON(jsonObj);
		}
	}

}}
