/// <summary>
 /// Assigns the colour value based on the heatMap data to a range of pixels.
 /// </summary>
 /// <param name="heatmap"></param>
 /// <param name="startX"></param>
 /// <param name="startY"></param>
 /// <param name="width"></param>
 /// <param name="height"></param>
 void AssignVisualisationTextureColorValue(ref Heatmap heatmap, int startX, int startY, int width, int height)
 {
     // Assign the correct pixel value to each requested pixel.
     for (int x = startX; x < (startX + width); x++)
     {
         for (int y = startY; y < (startY + height); y++)
         {
             AssignHeatmapColorValue(ref heatmap, x, y);
         }
     }
 }
        /// <summary>
        /// Flip the heatmap constraints.
        /// </summary>
        /// <param name="heatmapIndex"></param>
        /// <param name="heatmaps"></param>
        /// <returns></returns>
        Heatmap FlipHeatmapConstraints(int heatmapIndex, ref List <Heatmap> heatmaps)
        {
            Heatmap heatmap = heatmaps[heatmapIndex];

            float temp = heatmap.higherValueLimit;

            heatmap.higherValueLimit = heatmap.lowerValueLimit;
            heatmap.lowerValueLimit  = temp;

            return(heatmap);
        }
        /// <summary>
        /// Genereates the correct type of Splatprototypes for the heatmap
        /// </summary>
        /// <param name="heatmap"></param>
        /// <returns></returns>
        SplatPrototype[] GenerateSplatPrototypes(Heatmap heatmap)
        {
            switch (heatmap.texSource)
            {
            case (TextureSource.Custom):
                return(GenerateSplatPrototypesCustom(heatmap));

            default:
                return(GenerateSplatPrototypesDefault());
            }
        }
        /// <summary>
        /// No Interpolation, just extract the mapData we have (When  DataPoint resolution matches the alphamapResolution this is the default).
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        public float[,] InterpolationMethodNone(ref Heatmap heatmap, int x, int y, int width, int height)
        {
            float[,] heatmapValues = new float[width, height];

            for (int i = x; i < x + width; i++)
            {
                for (int j = y; j < y + height; j++)
                {
                    heatmapValues[i, j] = heatmap.heatmapDataPoints[i, j].value;
                }
            }

            return(heatmapValues);
        }
        /// <summary>
        /// Chooses the correct method to assign the values to the Heatmap's data points.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="terrainObjectSize"></param>
        /// <param name="heightMap"></param>
        /// <param name="heightMapScale"></param>
        /// <param name="customDataList"></param>
        /// <param name="positionOffset"></param>
        void AssignMapData(Heatmap heatmap, int alphaMapResolution, Vector3 terrainObjectSize, float[,] heightMap, Vector3 heightMapScale, HeatmapNode[] customDataList, Vector3 positionOffset, int terrainHeightmapResolution)
        {
            switch (heatmap.dataType)
            {
            case (HeatmapData.Custom):

                CustomDataToHeatmapData(heatmap, customDataList, false, terrainObjectSize, alphaMapResolution, positionOffset);
                break;

            default:
                TerrainHeightToHeatmapData(heatmap, terrainObjectSize, alphaMapResolution, heightMap, heightMapScale, terrainHeightmapResolution);
                break;
            }
        }
        /// <summary>
        /// Retrieves QPoints for interpolation.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="incrementInterval"></param>
        /// <returns></returns>
        HeatmapDatum[,] GetQPoints(ref Heatmap heatmap, int x, int y, int alphaMapResolution, float incrementInterval)
        {
            HeatmapDatum[,] qPoints = new HeatmapDatum[2, 2];

            int xGridSquare = Mathf.FloorToInt(((float)x / incrementInterval));
            int yGridSquare = Mathf.FloorToInt(((float)y / incrementInterval));


            qPoints[0, 0] = heatmap.heatmapDataPoints[xGridSquare, yGridSquare];
            qPoints[0, 1] = heatmap.heatmapDataPoints[xGridSquare, yGridSquare + 1];
            qPoints[1, 0] = heatmap.heatmapDataPoints[xGridSquare + 1, yGridSquare];
            qPoints[1, 1] = heatmap.heatmapDataPoints[xGridSquare + 1, yGridSquare + 1];

            return(qPoints);
        }
        /// <summary>
        /// Returns the closest data point to texture coordinate.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="incrementInterval"></param>
        /// <returns></returns>
        HeatmapDatum GetClosestDataPoint(ref Heatmap heatmap, int x, int y, int alphaMapResolution, float incrementInterval)
        {
            HeatmapDatum closetDataPoint;

            int xGridSquare = Mathf.RoundToInt(x / incrementInterval);
            int yGridSquare = Mathf.RoundToInt(y / incrementInterval);

            try
            {
                closetDataPoint = heatmap.heatmapDataPoints[xGridSquare, yGridSquare];
            }
            catch (System.IndexOutOfRangeException)
            {
                return(null);
            }

            return(closetDataPoint);
        }
        /// <summary>
        /// Chooses the correct interpolation method.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        float[,] InterpolateColorTextureValues(ref Heatmap heatmap, int alphaMapResolution, int x, int y, int width, int height)
        {
            // If the dataTexture Resoloution and the alphaMapResolution match then there is no need to interpolate the data.
            if (heatmap.heatmapResolution == alphaMapResolution)
            {
                return(InterpolationMethodNone(ref heatmap, alphaMapResolution));
            }

            switch (heatmap.interpolationMode)
            {
            case InterpolationMode.NearestNeighbor:
                return(InterpolationMethodNearestNeighbor(ref heatmap, alphaMapResolution, x, y, width, height));

            case InterpolationMode.Bilinear:
                return(InterpolationMethodBilinear(ref heatmap, alphaMapResolution, x, y, width, height));

            default:
                return(InterpolationMethodNearestNeighbor(ref heatmap, alphaMapResolution, x, y, width, height));
            }
        }
        /// <summary>
        /// Finds the highest and lowest values and uses them as the constraints.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="flipConstraints"></param>
        void AutoConstrainValues(ref Heatmap heatmap)
        {
            float upperValue = heatmap.heatmapDataPoints[0, 0].value;
            float lowerValue = heatmap.heatmapDataPoints[0, 0].value;

            foreach (HeatmapDatum datum in heatmap.heatmapDataPoints)
            {
                if (datum.value > upperValue)
                {
                    upperValue = datum.value;
                }
                if (datum.value < lowerValue)
                {
                    lowerValue = datum.value;
                }
            }

            heatmap.higherValueLimit = heatmap.flipAutoConstrain ? lowerValue : upperValue;
            heatmap.lowerValueLimit  = heatmap.flipAutoConstrain ? upperValue : lowerValue;
        }
        /// <summary>
        /// Nearest neighbour interpolation.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        public float[,] InterpolationMethodNearestNeighbor(ref Heatmap heatmap, int alphaMapResolution, int x, int y, int width, int height)
        {
            float[,] heatmapValues = new float[width, height];
            float incrementInterval = (alphaMapResolution / heatmap.heatmapResolution);


            for (int i = x; i < x + width; i++)
            {
                for (int j = y; j < y + height; j++)
                {
                    heatmapValues[i, j] = this.GetClosestDataPoint(ref heatmap, i, j, alphaMapResolution, incrementInterval).value;
                    if (float.IsNaN(heatmapValues[i, j]))
                    {
                        return(null);
                    }
                }
            }



            return(heatmapValues);
        }
        /// <summary>
        /// Generate heatmap data from the Terrain's height map.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="terrainObjectSize"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="heightMap"></param>
        /// <param name="heightMapScale"></param>
        void TerrainHeightToHeatmapData(Heatmap heatmap, Vector3 terrainObjectSize, int alphaMapResolution, float[,] heightMap, Vector3 heightMapScale, int terrainHeightmapResolution)
        {
            HeatmapDatum[,] heatmapValues = new HeatmapDatum[heatmap.heatmapResolution + 1, heatmap.heatmapResolution + 1];

            for (int x = 0; x <= heatmap.heatmapResolution; x++)
            {
                for (int y = 0; y <= heatmap.heatmapResolution; y++)
                {
                    float resScaleFactorX = terrainObjectSize.x / heatmap.heatmapResolution;
                    float resScaleFactorZ = terrainObjectSize.z / heatmap.heatmapResolution;

                    Vector3 nodeLocation = new Vector3((int)(y * resScaleFactorZ / (terrainObjectSize.z / (terrainHeightmapResolution - 1))), 0.0f, (int)(x * resScaleFactorX / (terrainObjectSize.x / (terrainHeightmapResolution - 1))));

                    float nodeValue = heightMap[(int)nodeLocation.z, (int)nodeLocation.x] * heightMapScale.y;


                    heatmapValues[x, y] = new HeatmapDatum();
                    heatmapValues[x, y].Init(nodeLocation, nodeValue, Coordinates.WorldToTerrainCoords(nodeLocation, terrainObjectSize, alphaMapResolution));
                }
            }

            heatmap.heatmapDataPoints = heatmapValues;
        }
 /// <summary>
 /// Perform Bi-Linear interpolation with fewer parameters.
 /// </summary>
 /// <param name="heatmap"></param>
 /// <param name="alphaMapResolution"></param>
 /// <returns></returns>
 public float[,] InterpolationMethodBilinear(ref Heatmap heatmap, int alphaMapResolution)
 {
     return(InterpolationMethodBilinear(ref heatmap, alphaMapResolution, 0, 0, alphaMapResolution, alphaMapResolution));
 }
 /// <summary>
 /// Perform Nearest-Neighbour interpolation with fewer parameters.
 /// </summary>
 /// <param name="heatmap"></param>
 /// <param name="alphaMapResolution"></param>
 /// <returns></returns>
 public float[,] InterpolationMethodNearestNeighbor(ref Heatmap heatmap, int alphaMapResolution)
 {
     return(InterpolationMethodNearestNeighbor(ref heatmap, alphaMapResolution, 0, 0, alphaMapResolution, alphaMapResolution));
 }
        /// <summary>
        /// Assigns the colour value based on the heatMap data to a given terrain Pixel.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        void AssignHeatmapColorValue(ref Heatmap heatmap, int x, int y)
        {
            // Calculate the value as a % of HottestValueThreshold.
            float value = heatmap.heatmapValues[x, y];

            value += CalculateLowerUpperThresholdValueOffset(heatmap.lowerValueLimit);

            // Offset the value to ensure it's positive.
            value += heatmap.higherValueLimit / heatmap.splatPrototypes.Length;

            // Calculate the value as a % of the range of values it could be.
            value = (value / (heatmap.higherValueLimit - heatmap.lowerValueLimit));

            // Normalise the value so if the value is at HottestValueThreshold, that threshold is equal to the last index in heatMapSplatPrototypes.
            value = (value * heatmap.splatPrototypes.Length - 1);

            if (float.IsNaN(value))
            {
                value = 0.0f;
            }

            // If the value is less than zero, then set the bottom layer to an opacity of 100% and all the other layers to 0% opacity.
            if (value < 0)
            {
                // Set the bottom layer to be 100% opaque.
                heatmap.alphaMapData[x, y, 0] = 1.0f;

                // Loop through all other layers setting their values to 0% opacity.
                for (int layerAboveZero = 1; layerAboveZero < heatmap.splatPrototypes.Length; layerAboveZero++)
                {
                    heatmap.alphaMapData[x, y, layerAboveZero] = 0.0f;
                }

                return;
            }
            // If the value is above the index number of the highest layer.
            else if (value >= heatmap.splatPrototypes.Length - 1)
            {
                // Set the value fot the hottest layer to be 100% opaque.
                heatmap.alphaMapData[x, y, heatmap.splatPrototypes.Length - 1] = 1.0f;

                // Loop through all the other layers setting their values to 0% opacity.
                for (int layersBelowTop = (heatmap.splatPrototypes.Length - 2); layersBelowTop >= 0; layersBelowTop--)
                {
                    heatmap.alphaMapData[x, y, layersBelowTop] = 0.0f;
                }
                return;
            }

            // If the value is within the range of the heatMap.
            else
            {
                // Loop through all the layers to find the layers where the blending should occur.
                for (int layernum = heatmap.splatPrototypes.Length - 1; layernum > 0; layernum--)
                {
                    // If this condition is true, we need to blend these layers.
                    if (value < layernum && value >= layernum - 1)
                    {
                        heatmap.alphaMapData[x, y, layernum]     = value - (layernum - 1);
                        heatmap.alphaMapData[x, y, layernum - 1] = 1.0f - (value - (layernum - 1));

                        // Loop through all layers above this layer and set to 0% opacity.
                        for (int num = layernum + 1; num < heatmap.splatPrototypes.Length; num++)
                        {
                            heatmap.alphaMapData[x, y, num] = 0.0f;
                        }

                        // Loop through all layers below this layer and set to 0% opacity.
                        for (int num = layernum - 2; num >= 0; num--)
                        {
                            heatmap.alphaMapData[x, y, num] = 0.0f;
                        }
                        // Return as correct value has been set for all layers.
                        return;
                    }
                }

                Debug.LogError("The color value of " + value + " could not bet set at location [" + x + "," + y + "]");
                return;
            }
        }
        /// <summary>
        /// Generate Heatmap data from custom data provided.
        /// </summary>
        /// <param name="heatmap"></param>
        /// <param name="customData"></param>
        /// <param name="addValuesOfIntersectingPoints"></param>
        /// <param name="terrainSize"></param>
        /// <param name="alphaMapResolution"></param>
        /// <param name="positionOffset"></param>
        void CustomDataToHeatmapData(Heatmap heatmap, HeatmapNode[] customData, bool addValuesOfIntersectingPoints, Vector3 terrainSize, int alphaMapResolution, Vector3 positionOffset)
        {
            HeatmapDatum[,] heatmapDataArray = new HeatmapDatum[heatmap.heatmapResolution + 1, heatmap.heatmapResolution + 1];

            // Assign the resulting value of the alphaMapResolution divided by the dataTextureResolution to a variable, as this is used a lot.
            int alphaMapDividedByHeatmapResolution = (alphaMapResolution / heatmap.heatmapResolution);


            // Populate the newly created returnVal array with default data.
            for (int x = 0; x <= heatmap.heatmapResolution; x++)
            {
                for (int y = 0; y <= heatmap.heatmapResolution; y++)
                {
                    heatmapDataArray[x, y]               = new HeatmapDatum();
                    heatmapDataArray[x, y].value         = heatmap.baseValue;
                    heatmapDataArray[x, y].nodePosition  = new Vector3((int)(x * alphaMapDividedByHeatmapResolution), 0.0f, (int)(y * alphaMapDividedByHeatmapResolution));
                    heatmapDataArray[x, y].terrainCoords = new Pair <int>((int)(x * alphaMapDividedByHeatmapResolution), (int)(y * alphaMapDividedByHeatmapResolution));
                }
            }

            float incrementInterval = (alphaMapResolution / heatmap.heatmapResolution);

            //Assign the values of all the points we know to the correct position in the array.
            foreach (HeatmapNode customDatum in customData)
            {
                int arrayPositionX, arrayPositionY;
                int brushSize = customDatum.brushSize;

                float brushHardness = customDatum.brushHardness;

                float brushOpacity = customDatum.brushOpacity;

                // Ensure that the data is valid.
                brushOpacity  = Mathf.Clamp(brushOpacity, 0.0f, 100.0f) / 100;
                brushHardness = Mathf.Clamp(brushHardness, 0.0f, 100.0f) / 100;

                // Find the correct position in the array based on the object's transform.
                Vector3 closetMapPointPosition = GetClosestDataPointPosition((customDatum.position - positionOffset), terrainSize, heatmap.heatmapResolution, alphaMapResolution, incrementInterval);
                Vector2 textureCoordianates    = Coordinates.WorldToTerrainCoords(closetMapPointPosition, terrainSize, alphaMapResolution);

                // Using the dataTexture Resolution and the Terrain coordiantes, calculate the correct position in the array.
                arrayPositionX = (int)textureCoordianates.x / alphaMapDividedByHeatmapResolution;
                arrayPositionY = (int)textureCoordianates.y / alphaMapDividedByHeatmapResolution;


                // Calculate the correct value when taking the opacity of the brush into account.
                float value = customDatum.value * brushOpacity;

                if (!customDatum.overwriteOtherNodeValues)
                {
                    if (arrayPositionX >= 0 && arrayPositionY >= 0 && arrayPositionX < heatmap.heatmapResolution && arrayPositionY < heatmap.heatmapResolution)
                    {
                        value = customDatum.value * brushOpacity + heatmapDataArray[arrayPositionX, arrayPositionY].value;
                    }
                }



                // Using the terrain Coordinates, create the Datum that we're going to store in the Array.
                HeatmapDatum textureDatum = new HeatmapDatum();
                textureDatum.Init(closetMapPointPosition, value, new Pair <int>((int)textureCoordianates.x, (int)textureCoordianates.y));

                // Assign the VisualisationTextureDatum to the correct Index in the Array.
                if (arrayPositionX < heatmap.heatmapResolution && arrayPositionY < heatmap.heatmapResolution && arrayPositionX > 0 && arrayPositionY > 0)
                {
                    heatmapDataArray[arrayPositionX, arrayPositionY] = textureDatum;
                }



                // If the brushSize is less than zero then we won't attempt to update the values of the surrounding nodes.
                if (brushSize > 0)
                {
                    // Calculate how many steps outwards we need to take (and thus how many other nodes we need to update).
                    int diameter = (int)(brushSize / alphaMapDividedByHeatmapResolution);

                    // We must have at least 2 steps outwards to be able to define a brush.
                    if (diameter > 0)
                    {
                        int radius        = diameter / 2;
                        int radiusSquared = radius * radius;

                        for (int x = -radius; x < +radius; x++)
                        {
                            for (int y = -radius; y < +radius; y++)
                            {
                                int arryPosX = arrayPositionX + x;
                                int arryPosY = arrayPositionY + y;

                                if (arryPosX == arrayPositionX && arryPosY == arrayPositionY)
                                {
                                    continue;
                                }

                                if (arryPosX >= 0 && arryPosY >= 0 && arryPosX <= heatmap.heatmapResolution && arryPosY <= heatmap.heatmapResolution)
                                {
                                    int dx = arryPosX - arrayPositionX;
                                    int dy = arryPosY - arrayPositionY;

                                    float distanceSquared = (dx * dx) + (dy * dy);

                                    if (distanceSquared <= radiusSquared)
                                    {
                                        float brushHardnessModifier = radiusSquared - (distanceSquared * (1.0f - brushHardness));
                                        brushHardnessModifier /= radiusSquared;

                                        if (!customDatum.overwriteOtherNodeValues)
                                        {
                                            heatmapDataArray[arryPosX, arryPosY].value += (brushHardnessModifier * customDatum.value) * brushOpacity;
                                        }
                                        else
                                        {
                                            heatmapDataArray[arryPosX, arryPosY].value = (brushHardnessModifier * customDatum.value) * brushOpacity;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            heatmap.heatmapDataPoints = heatmapDataArray;
        }