1package com.jme3.scene.plugins.blender.objects;
2
3import com.jme3.export.*;
4import com.jme3.scene.plugins.blender.BlenderContext;
5import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
6import com.jme3.scene.plugins.blender.file.BlenderInputStream;
7import com.jme3.scene.plugins.blender.file.FileBlockHeader;
8import com.jme3.scene.plugins.blender.file.Pointer;
9import com.jme3.scene.plugins.blender.file.Structure;
10import java.io.IOException;
11import java.util.ArrayList;
12import java.util.HashMap;
13import java.util.List;
14import java.util.Map;
15import java.util.logging.Logger;
16
17/**
18 * The blender object's custom properties.
19 * This class is valid for all versions of blender.
20 * @author Marcin Roguski (Kaelthas)
21 */
22public class Properties implements Cloneable, Savable {
23	private static final Logger		LOGGER				= Logger.getLogger(Properties.class.getName());
24
25	// property type
26	public static final int			IDP_STRING			= 0;
27	public static final int			IDP_INT				= 1;
28	public static final int			IDP_FLOAT			= 2;
29	public static final int			IDP_ARRAY			= 5;
30	public static final int			IDP_GROUP			= 6;
31	// public static final int IDP_ID = 7;//this is not implemented in blender (yet)
32	public static final int			IDP_DOUBLE			= 8;
33	// the following are valid for blender 2.5x+
34	public static final int			IDP_IDPARRAY		= 9;
35	public static final int			IDP_NUMTYPES		= 10;
36
37	protected static final String	RNA_PROPERTY_NAME	= "_RNA_UI";
38	/** Default name of the property (used if the name is not specified in blender file). */
39	protected static final String	DEFAULT_NAME		= "Unnamed property";
40
41	/** The name of the property. */
42	private String					name;
43	/** The type of the property. */
44	private int						type;
45	/** The subtype of the property. Defines the type of array's elements. */
46	private int						subType;
47	/** The value of the property. */
48	private Object					value;
49	/** The description of the property. */
50	private String					description;
51
52	/**
53	 * This method loads the property from the belnder file.
54	 * @param idPropertyStructure
55	 *        the ID structure constining the property
56	 * @param blenderContext
57	 *        the blender context
58	 * @throws BlenderFileException
59	 *         an exception is thrown when the belnder file is somehow invalid
60	 */
61	public void load(Structure idPropertyStructure, BlenderContext blenderContext) throws BlenderFileException {
62		name = idPropertyStructure.getFieldValue("name").toString();
63		if (name == null || name.length() == 0) {
64			name = DEFAULT_NAME;
65		}
66		subType = ((Number) idPropertyStructure.getFieldValue("subtype")).intValue();
67		type = ((Number) idPropertyStructure.getFieldValue("type")).intValue();
68
69		// reading the data
70		Structure data = (Structure) idPropertyStructure.getFieldValue("data");
71		int len = ((Number) idPropertyStructure.getFieldValue("len")).intValue();
72		switch (type) {
73			case IDP_STRING: {
74				Pointer pointer = (Pointer) data.getFieldValue("pointer");
75				BlenderInputStream bis = blenderContext.getInputStream();
76				FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress());
77				bis.setPosition(dataFileBlock.getBlockPosition());
78				value = bis.readString();
79				break;
80			}
81			case IDP_INT:
82				int intValue = ((Number) data.getFieldValue("val")).intValue();
83				value = Integer.valueOf(intValue);
84				break;
85			case IDP_FLOAT:
86				int floatValue = ((Number) data.getFieldValue("val")).intValue();
87				value = Float.valueOf(Float.intBitsToFloat(floatValue));
88				break;
89			case IDP_ARRAY: {
90				Pointer pointer = (Pointer) data.getFieldValue("pointer");
91				BlenderInputStream bis = blenderContext.getInputStream();
92				FileBlockHeader dataFileBlock = blenderContext.getFileBlock(pointer.getOldMemoryAddress());
93				bis.setPosition(dataFileBlock.getBlockPosition());
94				int elementAmount = dataFileBlock.getSize();
95				switch (subType) {
96					case IDP_INT:
97						elementAmount /= 4;
98						int[] intList = new int[elementAmount];
99						for (int i = 0; i < elementAmount; ++i) {
100							intList[i] = bis.readInt();
101						}
102						value = intList;
103						break;
104					case IDP_FLOAT:
105						elementAmount /= 4;
106						float[] floatList = new float[elementAmount];
107						for (int i = 0; i < elementAmount; ++i) {
108							floatList[i] = bis.readFloat();
109						}
110						value = floatList;
111						break;
112					case IDP_DOUBLE:
113						elementAmount /= 8;
114						double[] doubleList = new double[elementAmount];
115						for (int i = 0; i < elementAmount; ++i) {
116							doubleList[i] = bis.readDouble();
117						}
118						value = doubleList;
119						break;
120					default:
121						throw new IllegalStateException("Invalid array subtype: " + subType);
122				}
123			}
124			case IDP_GROUP:
125				Structure group = (Structure) data.getFieldValue("group");
126				List<Structure> dataList = group.evaluateListBase(blenderContext);
127				List<Properties> subProperties = new ArrayList<Properties>(len);
128				for (Structure d : dataList) {
129					Properties properties = new Properties();
130					properties.load(d, blenderContext);
131					subProperties.add(properties);
132				}
133				value = subProperties;
134				break;
135			case IDP_DOUBLE:
136				int doublePart1 = ((Number) data.getFieldValue("val")).intValue();
137				int doublePart2 = ((Number) data.getFieldValue("val2")).intValue();
138				long doubleVal = (long) doublePart2 << 32 | doublePart1;
139				value = Double.valueOf(Double.longBitsToDouble(doubleVal));
140				break;
141			case IDP_IDPARRAY: {
142				Pointer pointer = (Pointer) data.getFieldValue("pointer");
143				List<Structure> arrays = pointer.fetchData(blenderContext.getInputStream());
144				List<Object> result = new ArrayList<Object>(arrays.size());
145				Properties temp = new Properties();
146				for (Structure array : arrays) {
147					temp.load(array, blenderContext);
148					result.add(temp.value);
149				}
150				this.value = result;
151				break;
152			}
153			case IDP_NUMTYPES:
154				throw new UnsupportedOperationException();
155				// case IDP_ID://not yet implemented in blender
156				// return null;
157			default:
158				throw new IllegalStateException("Unknown custom property type: " + type);
159		}
160		this.completeLoading();
161	}
162
163	/**
164	 * This method returns the name of the property.
165	 * @return the name of the property
166	 */
167	public String getName() {
168		return name;
169	}
170
171	/**
172	 * This method returns the description of the property.
173	 * @return the description of the property
174	 */
175	public String getDescription() {
176		return description;
177	}
178
179	/**
180	 * This method returns the type of the property.
181	 * @return the type of the property
182	 */
183	public int getType() {
184		return type;
185	}
186
187	/**
188	 * This method returns the value of the property.
189	 * The type of the value depends on the type of the property.
190	 * @return the value of the property
191	 */
192	public Object getValue() {
193		return value;
194	}
195
196	/**
197	 * This method returns the same as getValue if the current property is of
198	 * other type than IDP_GROUP and its name matches 'propertyName' param. If
199	 * this property is a group property the method tries to find subproperty
200	 * value of the given name. The first found value is returnes os <b>use this
201	 * method wisely</b>. If no property of a given name is foung - <b>null</b>
202	 * is returned.
203	 *
204	 * @param propertyName
205	 *            the name of the property
206	 * @return found property value or <b>null</b>
207	 */
208	@SuppressWarnings("unchecked")
209	public Object findValue(String propertyName) {
210		if (name.equals(propertyName)) {
211			return value;
212		} else {
213			if (type == IDP_GROUP) {
214				List<Properties> props = (List<Properties>) value;
215				for (Properties p : props) {
216					Object v = p.findValue(propertyName);
217					if (v != null) {
218						return v;
219					}
220				}
221			}
222		}
223		return null;
224	}
225
226	@Override
227	public String toString() {
228		StringBuilder sb = new StringBuilder();
229		this.append(sb, new StringBuilder());
230		return sb.toString();
231	}
232
233	/**
234	 * This method appends the data of the property to the given string buffer.
235	 * @param sb
236	 *        string buffer
237	 * @param indent
238	 *        indent buffer
239	 */
240	@SuppressWarnings("unchecked")
241	private void append(StringBuilder sb, StringBuilder indent) {
242		sb.append(indent).append("name: ").append(name).append("\n\r");
243		sb.append(indent).append("type: ").append(type).append("\n\r");
244		sb.append(indent).append("subType: ").append(subType).append("\n\r");
245		sb.append(indent).append("description: ").append(description).append("\n\r");
246		indent.append('\t');
247		sb.append(indent).append("value: ");
248		if (value instanceof Properties) {
249			((Properties) value).append(sb, indent);
250		} else if (value instanceof List) {
251			for (Object v : (List<Object>) value) {
252				if (v instanceof Properties) {
253					sb.append(indent).append("{\n\r");
254					indent.append('\t');
255					((Properties) v).append(sb, indent);
256					indent.deleteCharAt(indent.length() - 1);
257					sb.append(indent).append("}\n\r");
258				} else {
259					sb.append(v);
260				}
261			}
262		} else {
263			sb.append(value);
264		}
265		sb.append("\n\r");
266		indent.deleteCharAt(indent.length() - 1);
267	}
268
269	/**
270	 * This method should be called after the properties loading.
271	 * It loads the properties from the _RNA_UI property and removes this property from the
272	 * result list.
273	 */
274	@SuppressWarnings("unchecked")
275	protected void completeLoading() {
276		if (this.type == IDP_GROUP) {
277			List<Properties> groupProperties = (List<Properties>) this.value;
278			Properties rnaUI = null;
279			for (Properties properties : groupProperties) {
280				if (properties.name.equals(RNA_PROPERTY_NAME) && properties.type == IDP_GROUP) {
281					rnaUI = properties;
282					break;
283				}
284			}
285			if (rnaUI != null) {
286				// removing the RNA from the result list
287				groupProperties.remove(rnaUI);
288
289				// loading the descriptions
290				Map<String, String> descriptions = new HashMap<String, String>(groupProperties.size());
291				List<Properties> propertiesRNA = (List<Properties>) rnaUI.value;
292				for (Properties properties : propertiesRNA) {
293					String name = properties.name;
294					String description = null;
295					List<Properties> rnaData = (List<Properties>) properties.value;
296					for (Properties rna : rnaData) {
297						if ("description".equalsIgnoreCase(rna.name)) {
298							description = (String) rna.value;
299							break;
300						}
301					}
302					descriptions.put(name, description);
303				}
304
305				// applying the descriptions
306				for (Properties properties : groupProperties) {
307					properties.description = descriptions.get(properties.name);
308				}
309			}
310		}
311	}
312
313	@Override
314	@SuppressWarnings({ "rawtypes", "unchecked" })
315	public void write(JmeExporter ex) throws IOException {
316		OutputCapsule oc = ex.getCapsule(this);
317		oc.write(name, "name", DEFAULT_NAME);
318		oc.write(type, "type", 0);
319		oc.write(subType, "subtype", 0);
320		oc.write(description, "description", null);
321		switch (type) {
322			case IDP_STRING:
323				oc.write((String) value, "value", null);
324				break;
325			case IDP_INT:
326				oc.write((Integer) value, "value", 0);
327				break;
328			case IDP_FLOAT:
329				oc.write((Float) value, "value", 0);
330				break;
331			case IDP_ARRAY:
332				switch (subType) {
333					case IDP_INT:
334						oc.write((int[]) value, "value", null);
335						break;
336					case IDP_FLOAT:
337						oc.write((float[]) value, "value", null);
338						break;
339					case IDP_DOUBLE:
340						oc.write((double[]) value, "value", null);
341						break;
342					default:
343						LOGGER.warning("Cannot save the property's value! Invalid array subtype! Property: name: " + name + "; subtype: " + subType);
344				}
345			case IDP_GROUP:
346				oc.writeSavableArrayList((ArrayList<Properties>) value, "value", null);
347				break;
348			case IDP_DOUBLE:
349				oc.write((Double) value, "value", 0);
350				break;
351			case IDP_IDPARRAY:
352				oc.writeSavableArrayList((ArrayList) value, "value", null);
353				break;
354			case IDP_NUMTYPES:
355				LOGGER.warning("Numtypes value not supported! Cannot write it!");
356				break;
357			// case IDP_ID://not yet implemented in blender
358			// break;
359			default:
360				LOGGER.warning("Cannot save the property's value! Invalid type! Property: name: " + name + "; type: " + type);
361		}
362	}
363
364	@Override
365	public void read(JmeImporter im) throws IOException {
366		InputCapsule ic = im.getCapsule(this);
367		name = ic.readString("name", DEFAULT_NAME);
368		type = ic.readInt("type", 0);
369		subType = ic.readInt("subtype", 0);
370		description = ic.readString("description", null);
371		switch (type) {
372			case IDP_STRING:
373				value = ic.readString("value", null);
374				break;
375			case IDP_INT:
376				value = Integer.valueOf(ic.readInt("value", 0));
377				break;
378			case IDP_FLOAT:
379				value = Float.valueOf(ic.readFloat("value", 0.0f));
380				break;
381			case IDP_ARRAY:
382				switch (subType) {
383					case IDP_INT:
384						value = ic.readIntArray("value", null);
385						break;
386					case IDP_FLOAT:
387						value = ic.readFloatArray("value", null);
388						break;
389					case IDP_DOUBLE:
390						value = ic.readDoubleArray("value", null);
391						break;
392					default:
393						LOGGER.warning("Cannot read the property's value! Invalid array subtype! Property: name: " + name + "; subtype: " + subType);
394				}
395			case IDP_GROUP:
396				value = ic.readSavable("value", null);
397				break;
398			case IDP_DOUBLE:
399				value = Double.valueOf(ic.readDouble("value", 0.0));
400				break;
401			case IDP_IDPARRAY:
402				value = ic.readSavableArrayList("value", null);
403				break;
404			case IDP_NUMTYPES:
405				LOGGER.warning("Numtypes value not supported! Cannot read it!");
406				break;
407			// case IDP_ID://not yet implemented in blender
408			// break;
409			default:
410				LOGGER.warning("Cannot read the property's value! Invalid type! Property: name: " + name + "; type: " + type);
411		}
412	}
413
414	@Override
415	public int hashCode() {
416		final int prime = 31;
417		int result = 1;
418		result = prime * result + (description == null ? 0 : description.hashCode());
419		result = prime * result + (name == null ? 0 : name.hashCode());
420		result = prime * result + subType;
421		result = prime * result + type;
422		result = prime * result + (value == null ? 0 : value.hashCode());
423		return result;
424	}
425
426	@Override
427	public boolean equals(Object obj) {
428		if (this == obj) {
429			return true;
430		}
431		if (obj == null) {
432			return false;
433		}
434		if (this.getClass() != obj.getClass()) {
435			return false;
436		}
437		Properties other = (Properties) obj;
438		if (description == null) {
439			if (other.description != null) {
440				return false;
441			}
442		} else if (!description.equals(other.description)) {
443			return false;
444		}
445		if (name == null) {
446			if (other.name != null) {
447				return false;
448			}
449		} else if (!name.equals(other.name)) {
450			return false;
451		}
452		if (subType != other.subType) {
453			return false;
454		}
455		if (type != other.type) {
456			return false;
457		}
458		if (value == null) {
459			if (other.value != null) {
460				return false;
461			}
462		} else if (!value.equals(other.value)) {
463			return false;
464		}
465		return true;
466	}
467}
468