package alternativa.engine3d.core {
	import alternativa.engine3d.*;
	import alternativa.engine3d.errors.FaceExistsError;
	import alternativa.engine3d.errors.FaceNotFoundError;
	import alternativa.engine3d.errors.InvalidIDError;
	import alternativa.engine3d.materials.SurfaceMaterial;
	import alternativa.types.Set;
	
	use namespace alternativa3d;
	
	/**
	 * Поверхность — набор граней, объединённых в группу. Поверхности используются для установки материалов,
	 * визуализирующих грани объекта.
	 */
	public class Surface {
		// Операции
		/**
		 * @private
		 * Изменение набора граней
		 */		
		alternativa3d var changeFacesOperation:Operation = new Operation("changeFaces", this);
		/**
		 * @private
		 * Изменение материала
		 */		
		alternativa3d var changeMaterialOperation:Operation = new Operation("changeMaterial", this);
		/**
		 * @private
		 * Меш
		 */
		alternativa3d var _mesh:Mesh;
		/**
		 * @private
		 * Материал
		 */
		alternativa3d var _material:SurfaceMaterial;
		/**
		 * @private
		 * Грани
		 */
		alternativa3d var _faces:Set = new Set();
		
		/**
		 * Создание экземпляра поверхности.
		 */		
		public function Surface() {}
		
		/**
		 * Добавление грани в поверхность.
		 *  
		 * @param face экземпляр класса alternativa.engine3d.core.Face или идентификатор грани полигонального объекта
		 * 
		 * @throws alternativa.engine3d.errors.FaceNotFoundError грань не найдена в полигональном объекте содержащем поверхность
		 * @throws alternativa.engine3d.errors.InvalidIDError указано недопустимое значение идентификатора
		 * @throws alternativa.engine3d.errors.FaceExistsError поверхность уже содержит указанную грань
		 * 
		 * @see Face 
		 */
		public function addFace(face:Object):void {
			var byLink:Boolean = face is Face;
			
			// Проверяем на нахождение поверхности в меше
			if (_mesh == null) {
				throw new FaceNotFoundError(face, this);
			}
			
			// Проверяем на null
			if (face == null) {
				throw new FaceNotFoundError(null, this);
			}
			
			// Проверяем наличие грани в меше
			if (byLink) {
				// Если удаляем по ссылке
				if (Face(face)._mesh != _mesh) {
					// Если грань не в меше
					throw new FaceNotFoundError(face, this);
				}
			} else {
				// Если удаляем по ID
				if (_mesh._faces[face] == undefined) {
					// Если нет грани с таким ID
					throw new FaceNotFoundError(face, this);
				} else { 
					if (!(_mesh._faces[face] is Face)) {
						throw new InvalidIDError(face, this);
					}
				}
			}
			
			// Находим грань
			var f:Face = byLink ? Face(face) : _mesh._faces[face];
			
			// Проверяем наличие грани в поверхности
			if (_faces.has(f)) {
				// Если грань уже в поверхности
				throw new FaceExistsError(f, this);
			}
			
			// Проверяем грань на нахождение в другой поверхности
			if (f._surface != null) {
				// Удаляем её из той поверхности
				f._surface._faces.remove(f);
				f.removeFromSurface(f._surface);
			}
			
			// Добавляем грань в поверхность
			_faces.add(f);
			f.addToSurface(this);
			
			// Отправляем операцию изменения набора граней
			_mesh.addOperationToScene(changeFacesOperation);
		}
		/**
		 * Удаление грани из поверхности.
		 *  
		 * @param face экземпляр класса alternativa.engine3d.core.Face или идентификатор грани полигонального объекта
		 * 
		 * @throws alternativa.engine3d.errors.FaceNotFoundError поверхность не содержит указанную грань
		 * @throws alternativa.engine3d.errors.InvalidIDError указано недопустимое значение идентификатора
		 * 
		 * @see Face
		 */
		public function removeFace(face:Object):void {
			var byLink:Boolean = face is Face;
			
			// Проверяем на нахождение поверхности в меше
			if (_mesh == null) {
				throw new FaceNotFoundError(face, this);
			}
			
			// Проверяем на null
			if (face == null) {
				throw new FaceNotFoundError(null, this);
			}
			
			// Проверяем наличие грани в меше
			if (byLink) {
				// Если удаляем по ссылке
				if (Face(face)._mesh != _mesh) {
					// Если грань не в меше
					throw new FaceNotFoundError(face, this);
				}
			} else {
				// Если удаляем по ID
				if (_mesh._faces[face] == undefined) {
					// Если нет грани с таким ID
					throw new FaceNotFoundError(face, this);
				} else {
					if (!(_mesh._faces[face] is Face)) {
						throw new InvalidIDError(face, this);
					}
				}
				
			}
			
			// Находим грань
			var f:Face = byLink ? Face(face) : _mesh._faces[face];
			
			// Проверяем наличие грани в поверхности
			if (!_faces.has(f)) {
				// Если грань не в поверхности
				throw new FaceNotFoundError(f, this);
			}
			
			// Удаляем грань из поверхности
			_faces.remove(f);
			f.removeFromSurface(this);
			
			// Отправляем операцию изменения набора граней
			_mesh.addOperationToScene(changeFacesOperation);
		}
		
		/**
		 * Материал поверхности. При установке нового значения, устанавливаемый материал будет удалён из старой поверхности.
		 */
		public function get material():SurfaceMaterial {
			return _material;
		}
		/**
		 * @private
		 */
		public function set material(value:SurfaceMaterial):void {
			if (_material != value) {
				// Если был материал
				if (_material != null) {
					// Удалить материал из поверхности
					_material.removeFromSurface(this);
					// Удалить материал из меша
					if (_mesh != null) {
						_material.removeFromMesh(_mesh);
						// Удалить материал из сцены
						if (_mesh._scene != null) {
							_material.removeFromScene(_mesh._scene);
						}
					}
				}
				// Если новый материал
				if (value != null) {
					// Если материал был в другой поверхности
					if (value._surface != null) {
						// Удалить его оттуда
						value._surface.material = null;
					}
					// Добавить материал в поверхность
					value.addToSurface(this);
					// Добавить материал в меш
					if (_mesh != null) {
						value.addToMesh(_mesh);
						// Добавить материал в сцену
						if (_mesh._scene != null) {
							value.addToScene(_mesh._scene);
						}
					}
				}
				// Сохраняем материал
				_material = value;
				// Отправляем операцию изменения материала
				addMaterialChangedOperationToScene();
			}
		}
		
		/**
		 * Набор граней поверхности.
		 */
		public function get faces():Set {
			return _faces.clone();
		}
		
		/**
		 * Полигональный объект, которому принадлежит поверхность.
		 */		
		public function get mesh():Mesh {
			return _mesh;
		}
		
		/**
		 * Идентификатор поверхности в полигональном объекте. Если поверхность не принадлежит ни одному объекту,
		 * значение идентификатора равно null.
		 */
		public function get id():Object {
			return (_mesh != null) ? _mesh.getSurfaceId(this) : null;
		}
		
		/**
		 * @private
		 * Добавление в сцену.
		 * 
		 * @param scene
		 */
		alternativa3d function addToScene(scene:Scene3D):void {
			// Добавляем на сцену материал
			if (_material != null) {
				_material.addToScene(scene);
			}
		}
		
		/**
		 * @private
		 * Удаление из сцены.
		 * 
		 * @param scene
		 */
		alternativa3d function removeFromScene(scene:Scene3D):void {
			// Удаляем все операции из очереди
			scene.removeOperation(changeFacesOperation);
			scene.removeOperation(changeMaterialOperation);
			// Удаляем из сцены материал
			if (_material != null) {
				_material.removeFromScene(scene);
			}
		}
		
		/**
		 * @private
		 * Добавление к мешу
		 * @param mesh
		 */		
		alternativa3d function addToMesh(mesh:Mesh):void {
			// Подписка на операции меша
			
			// Добавляем в меш материал
			if (_material != null) {
				_material.addToMesh(mesh);
			}
			// Сохранить меш
			_mesh = mesh;
		}
		
		/**
		 * @private
		 * Удаление из меша
		 * 
		 * @param mesh
		 */
		alternativa3d function removeFromMesh(mesh:Mesh):void {
			// Отписка от операций меша
			
			// Удаляем из меша материал
			if (_material != null) {
				_material.removeFromMesh(mesh);
			}
			// Удалить ссылку на меш
			_mesh = null;
		}
		
		/**
		 * @private
		 * Удаление граней
		 */
		alternativa3d function removeFaces():void {
			for (var key:* in _faces) {
				var face:Face = key;
				_faces.remove(face);
				face.removeFromSurface(this);
			}
		}
		/**
		 * @private
		 * Изменение материала
		 */
		alternativa3d function addMaterialChangedOperationToScene():void {
			if (_mesh != null) {
				_mesh.addOperationToScene(changeMaterialOperation);
			}
		}
		/**
		 * Строковое представление объекта.
		 * 
		 * @return строковое представление объекта
		 */
		public function toString():String {
			var length:uint = _faces.length;
			var res:String = "[Surface ID:" + id + ((length > 0) ? " faces:" : "");
			var i:uint = 0;
			for (var key:* in _faces) {
				var face:Face = key;
				res += face.id + ((i < length - 1) ? ", " : "");
				i++;
			}
			res += "]";
			return res;
		}		
	}
}