namespace Mapbox.Unity.MeshGeneration.Modifiers
{
	using System.Collections.Generic;
	using UnityEngine;
	using Mapbox.Unity.MeshGeneration.Data;
	using Mapbox.Unity.Map;
	using System;

	[CreateAssetMenu(menuName = "Mapbox/Modifiers/Textured Side Wall Modifier")]
	public class TextureSideWallModifier : MeshModifier
	{
		#region ModifierOptions

		private float _scaledFirstFloorHeight = 0;

		private float _scaledTopFloorHeight = 0;

		private float _scaledPreferredWallLength;
		[SerializeField] private bool _centerSegments = true;
		[SerializeField] private bool _separateSubmesh = true;

		#endregion

		float currentWallLength = 0;
		Vector3 start = Constants.Math.Vector3Zero;
		Vector3 wallDirection = Constants.Math.Vector3Zero;

		Vector3 wallSegmentFirstVertex;
		Vector3 wallSegmentSecondVertex;
		Vector3 wallSegmentDirection;
		float wallSegmentLength;

		private AtlasEntity _currentFacade;
		private Rect _currentTextureRect;

		private float finalFirstHeight;
		private float finalTopHeight;
		private float finalMidHeight;
		private float finalLeftOverRowHeight;
		private float _scaledFloorHeight;
		private int triIndex;
		private Vector3 wallNormal;
		private List<int> wallTriangles;
		private float columnScaleRatio;
		private float rightOfEdgeUv;

		private float currentY1;
		private float currentY2;
		private float _wallSizeEpsilon = 0.99f;
		private float _narrowWallWidthDelta = 0.01f;
		private float _shortRowHeightDelta = 0.015f;

		GeometryExtrusionWithAtlasOptions _options;
		private int _counter = 0;
		private float height = 0.0f;
		private float _scale = 1f;
		private float _minWallLength;
		private float _singleFloorHeight;
		private float _currentMidHeight;
		private float _midUvInCurrentStep;
		private float _singleColumnLength;
		private float _leftOverColumnLength;

		public override void SetProperties(ModifierProperties properties)
		{
			if (properties is GeometryExtrusionWithAtlasOptions)
			{
				_options = (GeometryExtrusionWithAtlasOptions)properties;
			}
			else if (properties is GeometryExtrusionOptions)
			{
				_options = ((GeometryExtrusionOptions)properties).ToGeometryExtrusionWithAtlasOptions();
			}
			else if (properties is UVModifierOptions)
			{
				_options = ((UVModifierOptions)properties).ToGeometryExtrusionWithAtlasOptions();
			}
		}

		public override void UnbindProperties()
		{
			_options.PropertyHasChanged -= UpdateModifier;
		}

		public override void Initialize()
		{
			base.Initialize();
			foreach (var atlasEntity in _options.atlasInfo.Textures)
			{
				atlasEntity.CalculateParameters();
			}
		}

		public override void UpdateModifier(object sender, System.EventArgs layerArgs)
		{
			SetProperties((ModifierProperties)sender);
			NotifyUpdateModifier(new VectorLayerUpdateArgs { property = sender as MapboxDataProperty, modifier = this });
		}

		public override void Run(VectorFeatureUnity feature, MeshData md, UnityTile tile = null)
		{
			if (md.Vertices.Count == 0 || feature == null || feature.Points.Count < 1)
				return;

			if (tile != null)
				_scale = tile.TileScale;

			//facade texture to decorate this building
			_currentFacade =
				_options.atlasInfo.Textures[UnityEngine.Random.Range(0, _options.atlasInfo.Textures.Count)];
			//rect is a struct so we're caching this
			_currentTextureRect = _currentFacade.TextureRect;

			//this can be moved to initialize or in an if clause if you're sure all your tiles will be same level/scale
			_singleFloorHeight = (tile.TileScale * _currentFacade.FloorHeight) / _currentFacade.MidFloorCount;
			_scaledFirstFloorHeight = tile.TileScale * _currentFacade.FirstFloorHeight;
			_scaledTopFloorHeight = tile.TileScale * _currentFacade.TopFloorHeight;
			_scaledPreferredWallLength = tile.TileScale * _currentFacade.PreferredEdgeSectionLength;
			_scaledFloorHeight = _scaledPreferredWallLength * _currentFacade.WallToFloorRatio;
			_singleColumnLength = _scaledPreferredWallLength / _currentFacade.ColumnCount;

			//read or force height
			float maxHeight = 1, minHeight = 0;

			//query height and push polygon up to create roof
			//can we do this vice versa and create roof at last?
			QueryHeight(feature, md, tile, out maxHeight, out minHeight);
			maxHeight = maxHeight * _options.extrusionScaleFactor * _scale;
			minHeight = minHeight * _options.extrusionScaleFactor * _scale;
			height = (maxHeight - minHeight);

			//we cann GenerateRoofMesh even if extrusion type is sidewall-only
			//it pushes the vertices to building height, then we clear top polygon triangles
			//to remove roof.
			GenerateRoofMesh(md, minHeight, maxHeight);
			if (_options.extrusionGeometryType == ExtrusionGeometryType.SideOnly)
			{
				md.Triangles[0].Clear();
			}

			if (_options.extrusionGeometryType != ExtrusionGeometryType.RoofOnly)
			{
				//limiting section heights, first floor gets priority, then we draw top floor, then mid if we still have space
				finalFirstHeight = Mathf.Min(height, _scaledFirstFloorHeight);
				finalTopHeight = (height - finalFirstHeight) < _scaledTopFloorHeight ? 0 : _scaledTopFloorHeight;
				finalMidHeight = Mathf.Max(0, height - (finalFirstHeight + finalTopHeight));
				wallTriangles = new List<int>();

				//cuts long edges into smaller ones using PreferredEdgeSectionLength
				currentWallLength = 0;
				start = Constants.Math.Vector3Zero;
				wallSegmentDirection = Constants.Math.Vector3Zero;

				finalLeftOverRowHeight = 0f;
				if (finalMidHeight > 0)
				{
					finalLeftOverRowHeight = finalMidHeight;
					finalLeftOverRowHeight = finalLeftOverRowHeight % _singleFloorHeight;
					finalMidHeight -= finalLeftOverRowHeight;
				}
				else
				{
					finalLeftOverRowHeight = finalTopHeight;
				}

				for (int i = 0; i < md.Edges.Count; i += 2)
				{
					var v1 = md.Vertices[md.Edges[i]];
					var v2 = md.Vertices[md.Edges[i + 1]];

					wallDirection = v2 - v1;

					currentWallLength = Vector3.Distance(v1, v2);
					_leftOverColumnLength = currentWallLength % _singleColumnLength;
					start = v1;
					wallSegmentDirection = (v2 - v1).normalized;

					//half of leftover column (if _centerSegments ofc) at the begining
					if (_centerSegments && currentWallLength > _singleColumnLength)
					{
						//save left,right vertices and wall length
						wallSegmentFirstVertex = start;
						wallSegmentLength = (_leftOverColumnLength / 2);
						start += wallSegmentDirection * wallSegmentLength;
						wallSegmentSecondVertex = start;

						_leftOverColumnLength = _leftOverColumnLength / 2;
						CreateWall(md);
					}

					while (currentWallLength > _singleColumnLength)
					{
						wallSegmentFirstVertex = start;
						//columns fitting wall / max column we have in texture
						var stepRatio =
							(float)Math.Min(_currentFacade.ColumnCount,
								Math.Floor(currentWallLength / _singleColumnLength)) / _currentFacade.ColumnCount;
						wallSegmentLength = stepRatio * _scaledPreferredWallLength;
						start += wallSegmentDirection * wallSegmentLength;
						wallSegmentSecondVertex = start;

						currentWallLength -= (stepRatio * _scaledPreferredWallLength);
						CreateWall(md);
					}

					//left over column at the end
					if (_leftOverColumnLength > 0)
					{
						wallSegmentFirstVertex = start;
						wallSegmentSecondVertex = v2;
						wallSegmentLength = _leftOverColumnLength;
						CreateWall(md);
					}
				}

				//this first loop is for columns
				if (_separateSubmesh)
				{
					md.Triangles.Add(wallTriangles);
				}
				else
				{
					md.Triangles.Capacity = md.Triangles.Count + wallTriangles.Count;
					md.Triangles[0].AddRange(wallTriangles);
				}
			}
		}

		private void CreateWall(MeshData md)
		{
			//need to keep track of this for triangulation indices
			triIndex = md.Vertices.Count;

			//this part minimizes stretching for narrow columns
			//if texture has 3 columns, 33% (of preferred edge length) wide walls will get 1 window.
			//0-33% gets 1 window, 33-66 gets 2, 66-100 gets all three
			//we're not wrapping/repeating texture as it won't work with atlases
			columnScaleRatio = Math.Min(1, wallSegmentLength / _scaledPreferredWallLength);
			rightOfEdgeUv =
				_currentTextureRect.xMin +
				_currentTextureRect.size.x *
				columnScaleRatio; // Math.Min(1, ((float)(Math.Floor(columnScaleRatio * _currentFacade.ColumnCount) + 1) / _currentFacade.ColumnCount));

			_minWallLength = (_scaledPreferredWallLength / _currentFacade.ColumnCount) * _wallSizeEpsilon;
			//common for all top/mid/bottom segments
			wallNormal = new Vector3(-(wallSegmentFirstVertex.z - wallSegmentSecondVertex.z), 0,
				(wallSegmentFirstVertex.x - wallSegmentSecondVertex.x)).normalized;
			//height of the left/right edges
			currentY1 = wallSegmentFirstVertex.y;
			currentY2 = wallSegmentSecondVertex.y;

			//moving leftover row to top
			LeftOverRow(md, finalLeftOverRowHeight);

			FirstFloor(md, height);
			TopFloor(md, finalLeftOverRowHeight);
			MidFloors(md);
		}

		private void LeftOverRow(MeshData md, float leftOver)
		{
			//leftover. we're moving small leftover row to top of the building
			if (leftOver > 0)
			{
				md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, currentY1, wallSegmentFirstVertex.z));
				md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, currentY2, wallSegmentSecondVertex.z));
				//move offsets bottom
				currentY1 -= leftOver;
				currentY2 -= leftOver;
				//bottom two vertices
				md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, currentY1, wallSegmentFirstVertex.z));
				md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, currentY2, wallSegmentSecondVertex.z));

				if (wallSegmentLength >= _minWallLength)
				{
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMax));
					md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentTextureRect.yMax));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin,
						_currentTextureRect.yMax - _shortRowHeightDelta));
					md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentTextureRect.yMax - _shortRowHeightDelta));
				}
				else
				{
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMax));
					md.UV[0].Add(
						new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta, _currentTextureRect.yMax));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin,
						_currentTextureRect.yMax - _shortRowHeightDelta));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta,
						_currentTextureRect.yMax - _shortRowHeightDelta));
				}

				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);

				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);

				wallTriangles.Add(triIndex);
				wallTriangles.Add(triIndex + 1);
				wallTriangles.Add(triIndex + 2);

				wallTriangles.Add(triIndex + 1);
				wallTriangles.Add(triIndex + 3);
				wallTriangles.Add(triIndex + 2);

				triIndex += 4;
			}
		}

		private void MidFloors(MeshData md)
		{
			_currentMidHeight = finalMidHeight;
			while (_currentMidHeight >= _singleFloorHeight - 0.01f)
			{
				//first part is the number of floors fitting current wall segment. You can fit max of "row count in mid". Or if wall
				//is smaller and it can only fit i.e. 3 floors instead of 5; we use 3/5 of the mid section texture as well.
				_midUvInCurrentStep =
					((float)Math.Min(_currentFacade.MidFloorCount,
						Math.Round(_currentMidHeight / _singleFloorHeight))) / _currentFacade.MidFloorCount;

				//top two vertices
				md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, currentY1, wallSegmentFirstVertex.z));
				md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, currentY2, wallSegmentSecondVertex.z));
				//move offsets bottom
				currentY1 -= (_scaledFloorHeight * _midUvInCurrentStep);
				currentY2 -= (_scaledFloorHeight * _midUvInCurrentStep);
				//bottom two vertices
				md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, currentY1, wallSegmentFirstVertex.z));
				md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, currentY2, wallSegmentSecondVertex.z));

				//we uv narrow walls different so they won't have condensed windows
				if (wallSegmentLength >= _minWallLength)
				{
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.topOfMidUv));
					md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentFacade.topOfMidUv));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin,
						_currentFacade.topOfMidUv - _currentFacade.midUvHeight * _midUvInCurrentStep));
					md.UV[0].Add(new Vector2(rightOfEdgeUv,
						_currentFacade.topOfMidUv - _currentFacade.midUvHeight * _midUvInCurrentStep));
				}
				else
				{
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.topOfMidUv));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta,
						_currentFacade.topOfMidUv));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin,
						_currentFacade.topOfMidUv - _currentFacade.midUvHeight * _midUvInCurrentStep));
					md.UV[0].Add(new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta,
						_currentFacade.topOfMidUv - _currentFacade.midUvHeight * _midUvInCurrentStep));
				}

				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);
				md.Normals.Add(wallNormal);

				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);
				md.Tangents.Add(wallDirection);

				wallTriangles.Add(triIndex);
				wallTriangles.Add(triIndex + 1);
				wallTriangles.Add(triIndex + 2);

				wallTriangles.Add(triIndex + 1);
				wallTriangles.Add(triIndex + 3);
				wallTriangles.Add(triIndex + 2);

				triIndex += 4;
				_currentMidHeight -= Math.Max(0.1f, (_scaledFloorHeight * _midUvInCurrentStep));
			}
		}

		private void TopFloor(MeshData md, float leftOver)
		{
			//top floor start
			currentY1 -= finalTopHeight;
			currentY2 -= finalTopHeight;
			md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, wallSegmentFirstVertex.y - leftOver,
				wallSegmentFirstVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, wallSegmentSecondVertex.y - leftOver,
				wallSegmentSecondVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, wallSegmentFirstVertex.y - leftOver - finalTopHeight,
				wallSegmentFirstVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x,
				wallSegmentSecondVertex.y - leftOver - finalTopHeight, wallSegmentSecondVertex.z));

			if (wallSegmentLength >= _minWallLength)
			{
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMax));
				md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentTextureRect.yMax));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.bottomOfTopUv));
				md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentFacade.bottomOfTopUv));
			}
			else
			{
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMax));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta, _currentTextureRect.yMax));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.bottomOfTopUv));
				md.UV[0].Add(
					new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta, _currentFacade.bottomOfTopUv));
			}

			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);


			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);

			wallTriangles.Add(triIndex);
			wallTriangles.Add(triIndex + 1);
			wallTriangles.Add(triIndex + 2);

			wallTriangles.Add(triIndex + 1);
			wallTriangles.Add(triIndex + 3);
			wallTriangles.Add(triIndex + 2);

			triIndex += 4;
		}

		private void FirstFloor(MeshData md, float hf)
		{
			md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, wallSegmentFirstVertex.y - hf + finalFirstHeight,
				wallSegmentFirstVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, wallSegmentSecondVertex.y - hf + finalFirstHeight,
				wallSegmentSecondVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentFirstVertex.x, wallSegmentFirstVertex.y - hf,
				wallSegmentFirstVertex.z));
			md.Vertices.Add(new Vector3(wallSegmentSecondVertex.x, wallSegmentSecondVertex.y - hf,
				wallSegmentSecondVertex.z));

			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);
			md.Normals.Add(wallNormal);
			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);
			md.Tangents.Add(wallDirection);

			if (wallSegmentLength >= _minWallLength)
			{
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.topOfBottomUv));
				md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentFacade.topOfBottomUv));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMin));
				md.UV[0].Add(new Vector2(rightOfEdgeUv, _currentTextureRect.yMin));
			}
			else
			{
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentFacade.topOfBottomUv));
				md.UV[0].Add(
					new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta, _currentFacade.topOfBottomUv));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin, _currentTextureRect.yMin));
				md.UV[0].Add(new Vector2(_currentTextureRect.xMin + _narrowWallWidthDelta, _currentTextureRect.yMin));
			}

			wallTriangles.Add(triIndex);
			wallTriangles.Add(triIndex + 1);
			wallTriangles.Add(triIndex + 2);

			wallTriangles.Add(triIndex + 1);
			wallTriangles.Add(triIndex + 3);
			wallTriangles.Add(triIndex + 2);

			triIndex += 4;
		}

		private void CalculateEdgeList(MeshData md, UnityTile tile, float preferredEdgeSectionLength)
		{
		}

		private void GenerateRoofMesh(MeshData md, float minHeight, float maxHeight)
		{
			_counter = md.Vertices.Count;
			switch (_options.extrusionType)
			{
				case ExtrusionType.None:
					break;
				case ExtrusionType.PropertyHeight:
					for (int i = 0; i < _counter; i++)
					{
						md.Vertices[i] = new Vector3(md.Vertices[i].x, md.Vertices[i].y + maxHeight,
							md.Vertices[i].z);
					}

					break;
				case ExtrusionType.MinHeight:
					{
						var minmax = MinMaxPair.GetMinMaxHeight(md.Vertices);
						for (int i = 0; i < _counter; i++)
						{
							md.Vertices[i] = new Vector3(md.Vertices[i].x, minmax.min + maxHeight, md.Vertices[i].z);
						}
					}
					break;
				case ExtrusionType.MaxHeight:
					{
						var minmax = MinMaxPair.GetMinMaxHeight(md.Vertices);
						for (int i = 0; i < _counter; i++)
						{
							md.Vertices[i] = new Vector3(md.Vertices[i].x, minmax.max + maxHeight, md.Vertices[i].z);
						}

						height += minmax.max - minmax.min;
					}
					break;
				case ExtrusionType.RangeHeight:
					for (int i = 0; i < _counter; i++)
					{
						md.Vertices[i] = new Vector3(md.Vertices[i].x, md.Vertices[i].y + maxHeight,
							md.Vertices[i].z);
					}

					break;
				case ExtrusionType.AbsoluteHeight:
					for (int i = 0; i < _counter; i++)
					{
						md.Vertices[i] = new Vector3(md.Vertices[i].x, md.Vertices[i].y + maxHeight,
							md.Vertices[i].z);
					}

					break;
				default:
					break;
			}
		}

		private void QueryHeight(VectorFeatureUnity feature, MeshData md, UnityTile tile, out float maxHeight,
			out float minHeight)
		{
			minHeight = 0.0f;
			maxHeight = 0.0f;

			switch (_options.extrusionType)
			{
				case ExtrusionType.None:
					break;
				case ExtrusionType.PropertyHeight:
				case ExtrusionType.MinHeight:
				case ExtrusionType.MaxHeight:
					if (feature.Properties.ContainsKey(_options.propertyName))
					{
						maxHeight = Convert.ToSingle(feature.Properties[_options.propertyName]);
						if (feature.Properties.ContainsKey("min_height"))
						{
							minHeight = Convert.ToSingle(feature.Properties["min_height"]);
						}
					}

					break;
				case ExtrusionType.RangeHeight:
					if (feature.Properties.ContainsKey(_options.propertyName))
					{
						if (_options.minimumHeight > _options.maximumHeight)
						{
							Debug.LogError("Maximum Height less than Minimum Height.Swapping values for extrusion.");
							var temp = _options.minimumHeight;
							_options.minimumHeight = _options.maximumHeight;
							_options.maximumHeight = temp;
						}

						var featureHeight = Convert.ToSingle(feature.Properties[_options.propertyName]);
						maxHeight = Math.Min(Math.Max(_options.minimumHeight, featureHeight), _options.maximumHeight);
						if (feature.Properties.ContainsKey("min_height"))
						{
							var featureMinHeight = Convert.ToSingle(feature.Properties["min_height"]);
							minHeight = Math.Min(featureMinHeight, _options.maximumHeight);
						}
					}

					break;
				case ExtrusionType.AbsoluteHeight:
					maxHeight = _options.maximumHeight;
					break;
				default:
					break;
			}
		}
	}
}
