using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using UnityEditor;
using UnityEngine;
namespace Framework.Terrain {
  /// Helper methods for dealing with terrains
  public class TerrainHelper {
    #region struct HeightRange
    /// Container that stores a minimum and a maximum height
    private struct HeightRange {
      /// Default range initialized to 0
      public static readonly HeightRange Default = new HeightRange(0.0f, 0.0f);
      /// Initializes a new height range
      /// Minimum height that will be assigned to the height range
      /// Maximum height that will be assigned to the height range
      public HeightRange(float min, float max) {
        this.Min = min;
        this.Max = max;
      }
      /// Lower height in the height range
      public float Min;
      /// Upper height in the height range
      public float Max;
    }
    #endregion // struct HeightRange
    #region class LayerWeightCalculator
    /// Calculates the layer weights when imported from texture colors
    private class LayerWeightCalculator {
      /// Maximum number of layers this helper class can deal with
      const int MaximumLayerCount = 8;
      /// Initializes a new layer weight calculator
      /// Textures the layer weights will be imported from
      /// Assignments of color channels to splat layers
      public LayerWeightCalculator(Texture2D[] textures, int[] colorIndices) {
        if(textures != null) {
          if(textures.Length >= 2) {
            this.firstTexture = textures[0];
            this.secondTexture = textures[1];
          } else if (textures.Length >= 1) {
            this.firstTexture = textures[0];
          }
        }
        this.colorIndices = colorIndices;
        if(colorIndices != null) {
          this.colorIndexCount = colorIndices.Length;
        }
        this.weights = new float[MaximumLayerCount];
      }
      /// Calculates the normalized layer weights from the colors
      /// X coordinate on the splat weight textures
      /// Y coordinate on the splat weight textures
      /// An array containing the normalized layer weights
      public float[] CalculateWeights(float u, float v) {
        clearWeightBuffer();
        // Add all weights involving the first texture if a first texture is present
        if(this.firstTexture != null) {
          Color firstColor = this.firstTexture.GetPixelBilinear(u, v);
          for(int index = 0; index < this.colorIndexCount; ++index) {
            switch(this.colorIndices[index]) {
              case 1: { this.weights[index] = firstColor.r; break; }
              case 2: { this.weights[index] = firstColor.g; break; }
              case 3: { this.weights[index] = firstColor.b; break; }
              case 4: { this.weights[index] = firstColor.a; break; }
            }
          }
        }
        // Add all weights involving the second texture if a second texture is present
        if(this.secondTexture != null) {
          Color secondColor = this.secondTexture.GetPixelBilinear(u, v);
          for(int index = 0; index < this.colorIndexCount; ++index) {
            switch(this.colorIndices[index]) {
              case 5: { this.weights[index] = secondColor.r; break; }
              case 6: { this.weights[index] = secondColor.g; break; }
              case 7: { this.weights[index] = secondColor.b; break; }
              case 8: { this.weights[index] = secondColor.a; break; }
            }
          }
        }
        // Calculate the sum of all weights
        float sum = 0.0f;
        for(int index = 0; index < MaximumLayerCount; ++index) {
          sum += this.weights[index];
        }
        // Unless there are no weights (Div-by-0 danger), normalize all weights
        if(sum > 0.0f) {
          for(int index = 0; index < MaximumLayerCount; ++index) {
            this.weights[index] /= sum;
          }
        }
        return this.weights;
      }
      /// Resets all weights to zero
      private void clearWeightBuffer() {
        //Array.Clear(this.weights, 0, MaximumLayerCount);
        for(int index = 0; index < MaximumLayerCount; ++index) {
          this.weights[index] = 0.0f;
        }
      }
      /// First texture from which to take color values
      private Texture2D firstTexture;
      /// Second texture from which to take color values
      private Texture2D secondTexture;
      /// Which color channels are mapped to which splat layers
      private int[] colorIndices;
      /// Number of color indices present
      private int colorIndexCount;
      /// Temporary buffer to hold retrieved weights for normalization
      /// 
      ///   Reusing this buffer avoids feeding the garbage collector each pixel
      /// 
      private float[] weights;
    }
    #endregion // class LayerWeightCalculator
    /// Imports terrain heights from a texture
    /// Terrain data the heights will be assigned to
    /// Texture the heights will be taken from
    public static void ImportHeightsFromTexture(
      TerrainData terrainData, Texture2D texture
    ) {
      int terrainWidthAndHeight = terrainData.heightmapResolution;
      float[,] heights = terrainData.GetHeights(
        0, 0, terrainWidthAndHeight, terrainWidthAndHeight
      );
      for(int y = 0; y < terrainWidthAndHeight; ++y) {
        float v = (float)(y + 0.5f) / (float)(terrainWidthAndHeight);
        for(int x = 0; x < terrainWidthAndHeight; ++x) {
          float u = (float)(x + 0.5f) / (float)(terrainWidthAndHeight);
          Color color = texture.GetPixelBilinear(u, v);
          heights[y, x] = color.grayscale;
        }
      }
      terrainData.SetHeights(0, 0, heights);
    }
    /// Returns the terrain heights as a texture
    /// Terrain data the heights will be taken from
    /// A texture containing the terrain heights
    /// Heights are normalized across the actually used height range
    public static Texture2D GetHeightsAsTexture(TerrainData terrainData) {
      int terrainWidthAndHeight = terrainData.heightmapResolution;
      var texture = new Texture2D(
        terrainWidthAndHeight, terrainWidthAndHeight, TextureFormat.RGBA32, false, true
      );
      // Take the pixel color values from Unity...
      float[,] heights = terrainData.GetHeights(
        0, 0, terrainWidthAndHeight, terrainWidthAndHeight
      );
      HeightRange heightExtremes = getHeightExtremes(heights);
      heightExtremes.Max -= heightExtremes.Min;
      // ...and store them in our texture as gray values
      var line = new Color32[terrainWidthAndHeight];
      for(int v = 0; v < terrainWidthAndHeight; ++v) {
        for(int u = 0; u < terrainWidthAndHeight; ++u) {
          float height = (heights[v, u] - heightExtremes.Min) * 255.0f / heightExtremes.Max;
          // We could just set all 3 channels to the closest brightness,
          // but we can do better: if we choose the r,g,b channels with the assumption
          // that their average becomes the grey value, we have 768 gray levels
          // (all without impacting compatibility).
          //
          //          0.0   0.1   0.2   0.3   0.4   0.5   0.6   0.7   0.8   0.9   1.0
          //
          // red       0     0     0     0     0     |     1     1     1     1     1
          // green     0     0     0     0     0     0     0  |  1     1     1     1
          // blue      0     0     0     0  |  1     1     1     1     1     1     1
          //
          //          0.2:   254+254+254 = 254.0   -    0+0+0 = 0.0
          //          0.4:   254+254+255 = 254.33   -   0+0+1 = 0.33
          //          0.6:   255+254+255 = 254.67  -    1+0+1 = 0.67
          //          0.8:   255+255+255 = 255.0    -   1+1+1 = 1.0
          //
          line[u].r = (byte)(height + 0.5f);
          line[u].g = (byte)(height + (1.0f / 3.0f));
          line[u].b = (byte)(height + (2.0f / 3.0f));
          line[u].a = 255;
        }
        texture.SetPixels32(0, v, terrainWidthAndHeight, 1, line);
      }
      return texture;
    }
    /// Imports the splat layer weights from one or more textures
    /// Terrain data to which the layer weights will be applied
    /// Textures the layer weights will be taken from
    /// Colors to map to each of the texture layers
    public static void ImportLayerWeightsFromTextures(
      TerrainData terrainData, Texture2D[] textures, int[] colorIndices
    ) {
      int terrainWidthAndHeight = terrainData.alphamapResolution;
      int layerCount = colorIndices.Length;
      if(layerCount > terrainData.alphamapLayers) {
        throw new IndexOutOfRangeException(
          "Layer count is higher than the number of layers present in terrain"
        );
      }
      float[,,] layerWeights = terrainData.GetAlphamaps(
        0, 0, terrainWidthAndHeight, terrainWidthAndHeight
      );
      var layerWeightCalculator = new LayerWeightCalculator(textures, colorIndices);
      for(int y = 0; y < terrainWidthAndHeight; ++y) {
        float v = (float)(y + 0.5f) / (float)(terrainWidthAndHeight);
        for(int x = 0; x < terrainWidthAndHeight; ++x) {
          float u = (float)(x + 0.5f) / (float)(terrainWidthAndHeight);
          float[] weights = layerWeightCalculator.CalculateWeights(u, v);
          for(int index = 0; index < layerCount; ++index) {
            layerWeights[y, x, index] = weights[index];
          }
        }
      }
      terrainData.SetAlphamaps(0, 0, layerWeights);
    }
    /// Returns the terrain layer weights as a texture
    /// Terrain data the layer weights will be taken from
    /// Layer indices that will be stored in the color channels
    /// A texture containing the terrain layer weights
    public static Texture2D GetLayerWeightsAsTexture(TerrainData terrainData, int[] layerIndices) {
      int terrainWidthAndHeight = terrainData.alphamapResolution;
      var texture = new Texture2D(
        terrainWidthAndHeight, terrainWidthAndHeight, TextureFormat.RGBA32, false, true
      );
      // Ensure no more than 4 layers have been specified
      int layerCount = getArrayLength(layerIndices);
      if(layerCount > 4) {
        throw new ArgumentException("Too many layers specified (maximum is 4)", "layerIndices");
      }
      // Fail if any color channel is assigned to a layer that doesn't exist
      for(int index = 0; index < layerCount; ++index) {
        if(layerIndices[index] > terrainData.alphamapLayers) {
          throw new IndexOutOfRangeException(
            "Layer index is higher than number of layers present in terrain"
          );
        }
      }
      // Take the layer weights from Unity...
      float[,,] layerWeights = terrainData.GetAlphamaps(
        0, 0, terrainWidthAndHeight, terrainWidthAndHeight
      );
      // And store them in our texture's color channels according to the layer mapping table
      var line = new Color32[terrainWidthAndHeight];
      for(int v = 0; v < terrainWidthAndHeight; ++v) {
        for(int u = 0; u < terrainWidthAndHeight; ++u) {
          line[u] = new Color32(0, 0, 0, 255);
          for(int index = 0; index < layerCount; ++index) {
            int layerIndex = layerIndices[index];
            if(layerIndex != -1) {
              byte colorValue = (byte) (layerWeights[v, u, layerIndex] * 255.0f + 0.5f);
              assignColor(ref line[u], index, colorValue);
            }
          }
        }
        texture.SetPixels32(0, v, terrainWidthAndHeight, 1, line);
      }
      return texture;
    }
    /// Determines the minimum and maximum height in a height array
    /// Height array that will be scanned
    /// The minimum and maximum heights found within the array
    private static HeightRange getHeightExtremes(float[,] heights) {
      if(heights == null) {
        return HeightRange.Default;
      }
      int rowCount = heights.GetLength(0);
      int colCount = heights.GetLength(1);
      if((rowCount == 0) || (colCount == 0)) {
        return HeightRange.Default;
      }
      var extremes = new HeightRange(heights[0, 0], heights[0, 0]);
      for(int v = 0; v < rowCount; ++v) {
        for(int u = 0; u < colCount; ++u) {
          float height = heights[v, u];
          if(height < extremes.Min) {
            extremes.Min = height;
          }
          if(height > extremes.Max) {
            extremes.Max = height;
          }
        }
      }
      return extremes;
    }
    /// Returns the lenght of an array and 0 if the array is null
    /// Array whose length will be returned
    /// The length of the array, even if the array itself is null
    private static int getArrayLength(int[] array) {
      if(array == null) {
        return 0;
      } else {
        return array.Length;
      }
    }
    /// Assigns a color channel by index ot a 32 bit RGB color value
    /// Color in which a color channel will be assigned by index
    /// Index of the color channel that will be assigned
    /// Value that will be assigned to the color channel
    private static void assignColor(ref Color32 color, int colorChannelIndex, byte value) {
      switch(colorChannelIndex) {
        case 0: { color.r = value; break; }
        case 1: { color.g = value; break; }
        case 2: { color.b = value; break; }
        case 3: { color.a = value; break; }
        default: { throw new IndexOutOfRangeException("Color channel index out of range"); }
      }
    }
  }
} // namespace Framework.Terrain