package com.brashmonkey.spriter;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;

import com.brashmonkey.spriter.Entity.*;
import com.brashmonkey.spriter.Mainline.Key.*;
import com.brashmonkey.spriter.XmlReader.*;

/**
 * This class parses a SCML file and creates a {@link Data} instance.
 * If you want to keep track of what is going on during the build process of the objects parsed from the SCML file,
 * you could extend this class and override the load*() methods for pre or post processing.
 * This could be e.g. useful for a loading screen which responds to the current building or parsing state.
 * @author Trixt0r
 */
public class SCMLReader {
	
	protected Data data;
	
	/**
	 * Creates a new SCML reader and will parse all objects in the given stream.
	 * @param stream the stream
	 */
	public SCMLReader(InputStream stream){
		this.data = this.load(stream);
	}
	
	/**
	 * Creates a new SCML reader and will parse the given xml string.
	 * @param xml the xml string
	 */
	public SCMLReader(String xml){
		this.data = this.load(xml);
	}
	
	/**
	 * Parses the SCML object save in the given xml string and returns the build data object.
	 * @param xml the xml string
	 * @return the built data
	 */
	protected Data load(String xml){
		XmlReader reader = new XmlReader();
		Element root = reader.parse(xml);
		ArrayList<Element> folders = root.getChildrenByName("folder");
		ArrayList<Element> entities = root.getChildrenByName("entity");
		data = new Data(root.get("scml_version"), root.get("generator"),root.get("generator_version"),
				folders.size(),	entities.size());
		loadFolders(folders);
		loadEntities(entities);
		return data;
	}
	
	/**
	 * Parses the SCML objects saved in the given stream and returns the built data object.
	 * @param stream the stream from the SCML file 
	 * @return the built data
	 */
	protected Data load(InputStream stream){
		XmlReader reader = new XmlReader();
		try {
			Element root = reader.parse(stream);
			ArrayList<Element> folders = root.getChildrenByName("folder");
			ArrayList<Element> entities = root.getChildrenByName("entity");
			data = new Data(root.get("scml_version"), root.get("generator"),root.get("generator_version"),
					folders.size(),	entities.size());
			loadFolders(folders);
			loadEntities(entities);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return data;
	}
	
	/**
	 * Iterates through the given folders and adds them to the current {@link Data} object.
	 * @param folders a list of folders to load
	 */
	protected void loadFolders(ArrayList<Element> folders){
		for(int i = 0; i < folders.size(); i++){
			Element repo = folders.get(i);
			ArrayList<Element> files = repo.getChildrenByName("file");
			Folder folder = new Folder(repo.getInt("id"), repo.get("name", "no_name_"+i), files.size());
			loadFiles(files, folder);
			data.addFolder(folder);
		}
	}
	
	/**
	 * Iterates through the given files and adds them to the given {@link Folder} object.
	 * @param files a list of files to load
	 * @param folder the folder containing the files
	 */
	protected void loadFiles(ArrayList<Element> files, Folder folder){
		for(int j = 0; j < files.size(); j++){
			Element f = files.get(j);
			File file = new File(f.getInt("id"), f.get("name"),
					new Dimension(f.getInt("width", 0), f.getInt("height", 0)),
					new Point(f.getFloat("pivot_x", 0f), f.getFloat("pivot_y", 1f)));
			
			folder.addFile(file);
		}
	}

	/**
	 * Iterates through the given entities and adds them to the current {@link Data} object.
	 * @param entities a list of entities to load
	 */
	protected void loadEntities(ArrayList<Element> entities){
		for(int i = 0; i < entities.size(); i++){
			Element e = entities.get(i);
			ArrayList<Element> infos = e.getChildrenByName("obj_info");
			ArrayList<Element> charMaps = e.getChildrenByName("character_map");
			ArrayList<Element> animations = e.getChildrenByName("animation");
			Entity entity = new Entity(e.getInt("id"), e.get("name"),
					animations.size(), charMaps.size(), infos.size());
			data.addEntity(entity);
			loadObjectInfos(infos, entity);
			loadCharacterMaps(charMaps, entity);
			loadAnimations(animations, entity);
		}
	}
	
	/**
	 * Iterates through the given object infos and adds them to the given {@link Entity} object.
	 * @param infos a list of infos to load
	 * @param entity the entity containing the infos
	 */
	protected void loadObjectInfos(ArrayList<Element> infos, Entity entity){
		for(int i = 0; i< infos.size(); i++){
			Element info = infos.get(i);
			Entity.ObjectInfo objInfo = new Entity.ObjectInfo(info.get("name","info"+i),
									Entity.ObjectType.getObjectInfoFor(info.get("type","")),
									new Dimension(info.getFloat("w", 0), info.getFloat("h", 0)));
			entity.addInfo(objInfo);
			Element frames = info.getChildByName("frames");
			if(frames == null) continue;
			ArrayList<Element> frameIndices = frames.getChildrenByName("i");
			for(Element index: frameIndices){
				int folder = index.getInt("folder", 0);
				int file =  index.getInt("file", 0);
				objInfo.frames.add(new FileReference(folder, file));
			}
		}
	}
	
	/**
	 * Iterates through the given character maps and adds them to the given {@link Entity} object.
	 * @param maps a list of character maps to load
	 * @param entity the entity containing the character maps
	 */
	protected void loadCharacterMaps(ArrayList<Element> maps, Entity entity){
		for(int i = 0; i< maps.size(); i++){
			Element map = maps.get(i);
			Entity.CharacterMap charMap = new Entity.CharacterMap(map.getInt("id"), map.getAttribute("name", "charMap"+i));
			entity.addCharacterMap(charMap);
			ArrayList<Element> mappings = map.getChildrenByName("map");
			for(Element mapping: mappings){
				int folder = mapping.getInt("folder");
				int file =  mapping.getInt("file");
				charMap.put(new FileReference(folder, file),
						new FileReference(mapping.getInt("target_folder", folder), mapping.getInt("target_file", file)));
			}
		}
	}
	
	/**
	 * Iterates through the given animations and adds them to the given {@link Entity} object.
	 * @param animations a list of animations to load
	 * @param entity the entity containing the animations maps
	 */
	protected void loadAnimations(ArrayList<Element> animations, Entity entity){
		for(int i = 0; i < animations.size(); i++){
			Element a = animations.get(i);
			ArrayList<Element> timelines = a.getChildrenByName("timeline");
			Element mainline = a.getChildByName("mainline");
			ArrayList<Element> mainlineKeys = mainline.getChildrenByName("key");
			Animation animation = new Animation(new Mainline(mainlineKeys.size()),
									  a.getInt("id"), a.get("name"), a.getInt("length"), 
									  a.getBoolean("looping", true),timelines.size());
			entity.addAnimation(animation);
			loadMainlineKeys(mainlineKeys, animation.mainline);
			loadTimelines(timelines, animation, entity);
			animation.prepare();
		}
	}
	
	/**
	 * Iterates through the given mainline keys and adds them to the given {@link Mainline} object.
	 * @param keys a list of mainline keys
	 * @param main the mainline
	 */
	protected void loadMainlineKeys(ArrayList<Element> keys, Mainline main){
		for(int i = 0; i < main.keys.length; i++){
			Element k = keys.get(i);
			ArrayList<Element> objectRefs = k.getChildrenByName("object_ref");
			ArrayList<Element> boneRefs = k.getChildrenByName("bone_ref");
			Curve curve = new Curve();
			curve.setType(Curve.getType(k.get("curve_type","linear")));
			curve.constraints.set(k.getFloat("c1", 0f),k.getFloat("c2", 0f),k.getFloat("c3", 0f),k.getFloat("c4", 0f));
			Mainline.Key key = new Mainline.Key(k.getInt("id"), k.getInt("time", 0), curve,
					boneRefs.size(), objectRefs.size());
			main.addKey(key);
			loadRefs(objectRefs, boneRefs, key);
		}
	}
	
	/**
	 * Iterates through the given bone and object references and adds them to the given {@link Mainline.Key} object.
	 * @param objectRefs a list of object references
	 * @param boneRefs a list if bone references
	 * @param key the mainline key
	 */
	protected void loadRefs(ArrayList<Element> objectRefs, ArrayList<Element> boneRefs, Mainline.Key key){
		for(Element e: boneRefs){
			BoneRef boneRef = new BoneRef(e.getInt("id"),e.getInt("timeline"),
							e.getInt("key"), key.getBoneRef(e.getInt("parent", -1)));
			key.addBoneRef(boneRef);
		}

		for(Element o: objectRefs){
			ObjectRef objectRef = new ObjectRef(o.getInt("id"),o.getInt("timeline"),
							o.getInt("key"), key.getBoneRef(o.getInt("parent", -1)), o.getInt("z_index",0));
			key.addObjectRef(objectRef);
		}
		Arrays.sort(key.objectRefs);
	}
	
	/**
	 * Iterates through the given timelines and adds them to the given {@link Animation} object.
	 * @param timelines a list of timelines
	 * @param animation the animation containing the timelines
	 * @param entity entity for assigning the timeline an object info
	 */
	protected void loadTimelines(ArrayList<Element> timelines, Animation animation, Entity entity){
		for(int i = 0; i< timelines.size(); i++){
			Element t = timelines.get(i);
			ArrayList<Element> keys = timelines.get(i).getChildrenByName("key");
			String name = t.get("name");
			ObjectType type = ObjectType.getObjectInfoFor(t.get("object_type", "sprite"));
			ObjectInfo info = entity.getInfo(name);
			if(info == null) info = new ObjectInfo(name, type, new Dimension(0,0));
			Timeline timeline = new Timeline(t.getInt("id"), name, info, keys.size());
			animation.addTimeline(timeline);
			loadTimelineKeys(keys, timeline);
		}
	}
	
	/**
	 * Iterates through the given timeline keys and adds them to the given {@link Timeline} object.
	 * @param keys a list if timeline keys
	 * @param timeline the timeline containing the keys
	 */
	protected void loadTimelineKeys(ArrayList<Element> keys, Timeline timeline){
		for(int i = 0; i< keys.size(); i++){
			Element k = keys.get(i);
			Curve curve = new Curve();
			curve.setType(Curve.getType(k.get("curve_type", "linear")));
			curve.constraints.set(k.getFloat("c1", 0f),k.getFloat("c2", 0f),k.getFloat("c3", 0f),k.getFloat("c4", 0f));
			Timeline.Key key = new Timeline.Key(k.getInt("id"), k.getInt("time", 0), k.getInt("spin", 1), curve);
			Element obj = k.getChildByName("bone");
			if(obj == null) obj = k.getChildByName("object");
			
			Point position = new Point(obj.getFloat("x", 0f), obj.getFloat("y", 0f));
			Point scale = new Point(obj.getFloat("scale_x", 1f), obj.getFloat("scale_y", 1f));
			Point pivot = new Point(obj.getFloat("pivot_x", 0f), obj.getFloat("pivot_y", (timeline.objectInfo.type == ObjectType.Bone)? .5f:1f));
			float angle = obj.getFloat("angle", 0f), alpha = 1f;
			int folder = -1, file = -1;
			if(obj.getName().equals("object")){
				if(timeline.objectInfo.type == ObjectType.Sprite){
					alpha = obj.getFloat("a", 1f);
					folder = obj.getInt("folder", -1);
					file = obj.getInt("file", -1);
					File f = data.getFolder(folder).getFile(file);
					pivot = new Point(obj.getFloat("pivot_x", f.pivot.x), obj.getFloat("pivot_y", f.pivot.y));
					timeline.objectInfo.size.set(f.size);
				}
			}
			Timeline.Key.Object object;
			if(obj.getName().equals("bone")) object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file));
			else object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file));
			key.setObject(object);
			timeline.addKey(key);
		}
	}
	
	/**
	 * Returns the loaded SCML data.
	 * @return the SCML data.
	 */
	public Data getData(){
		return data;
	}
	
}

