using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Machete {

  [System.Serializable]
  struct StudentsConfig {
    public Student prefab;
    public int amount;
    public StudentPlacement placement;
  }

  [System.Serializable]
  struct CheaterConfig {
    public Cheater prefab;
    public PlayableCheater playablePrefab;
    public int amount;
    public int playable;
  }

  enum StudentPlacement {
    NoBorder,
    NoCenter,
    Wherever
  }

  public enum GridDirection {
    Up,
    Right,
    Down,
    Left,
    NoNeighbor
  }

	public class Grid : MonoBehaviour {

    public Message messagePrefab;
		[SerializeField][Range(3,10)]
		private int cols = 5;
		[SerializeField][Range(3,10)]
		private int rows = 5;
    [SerializeField]
    private float colPadding = 3;
    [SerializeField]
    private float rowPadding = 3;
		[SerializeField]
		private Desk deskPrefab;
    [SerializeField]
    private List<StudentsConfig> students;
    [SerializeField]
    private CheaterConfig cheatersConf;
    [SerializeField]
    private Vector3 studentPosOffset;

    private Desk[] grid;

    public List<Cheater> Cheaters {
      get { return cheaters; }
    }
    private List<Cheater> cheaters;
    private List<Desk> freeDesks;

		// Use this for initialization
		void Awake() {
      CreateGrid();
      PlaceStudents();
      PlaceCheaters();
    }

    void CreateGrid() {
      // Create grid of desks
      grid = new Desk[cols * rows];
      Vector3 position = transform.position;

      for (var i = 0; i < rows; i++) {
        for (var j = 0; j < cols; j++) {
          // Init desk
          Desk go = Instantiate(deskPrefab);
          go.transform.position = go.transform.position + position;
          go.IsBorder = i == 0 || (i == rows - 1) || j == 0 || j == (cols - 1);
          go.Coords = new Vector2Int(j, i);
          go.Grid = this;
          grid[cols * i + j] = go;
          go.transform.SetParent(transform);
          // Update col position
          position.x += colPadding;
        }
        // Update row position
        position.x = transform.position.x;
        position.z += rowPadding;
      }
		}

    void PlaceStudents() {
      List<Desk> availableBorders = new List<Desk>();
      List<Desk> availableCenters = new List<Desk>();

      foreach (Desk desk in grid) {
        if (desk.IsBorder) {
          availableBorders.Add(desk);
        } else {
          availableCenters.Add(desk);
        }
      }

      // Place in specific order
      students.Sort((a, b) => { return a.placement.CompareTo(b.placement); });

      for (var i = 0; i < students.Count; i++) {
        StudentsConfig config = students[i];

        for (var j = 0; j < config.amount; j++) {
          // To many students.
          if (availableBorders.Count == 0 && availableCenters.Count == 0) {
            Debug.LogWarning("Too many students.");
            break;
          }

          // Instantiate student.
          Student student = Instantiate(config.prefab);

          // Prioritize order of positioning.
          bool placed = false;
          int index = 0;
          if (config.placement == StudentPlacement.NoBorder && availableCenters.Count > 0) {
            index = Random.Range(0, availableCenters.Count);
            PlaceFromList(index, availableCenters, student);
            placed = true;
          } else if (config.placement == StudentPlacement.NoCenter && availableBorders.Count > 0) {
            index = Random.Range(0, availableBorders.Count);
            PlaceFromList(index, availableBorders, student);
            placed = true;
          }

          // If it wasn't possible do it anywhere
          if (!placed) {
            int available = availableCenters.Count + availableBorders.Count;
            index = Random.Range(0, available);
            // Check which list are you going to use.
            if (index < availableCenters.Count) {
              PlaceFromList(index, availableCenters, student);
            } else {
              index -= availableCenters.Count;
              PlaceFromList(index, availableBorders, student);
            }
          }
        }

        // If no more space no more space.
        if (availableBorders.Count == 0 && availableCenters.Count == 0) {
          break;
        }
      }

      freeDesks = new List<Desk>();
      freeDesks.AddRange(availableBorders);
      freeDesks.AddRange(availableCenters);
    }

    void PlaceCheaters() {
      cheaters = new List<Cheater>();
      // Get which cheaters are going to be playable.
      bool[] playables = new bool[cheatersConf.amount];
      List<int> cheatersIndex = new List<int>();

      for (var i = 0; i < cheatersConf.amount; i++) {
        cheatersIndex.Add(i);
      }

      for (var i = 0; i < cheatersConf.playable; i++) {
        int index = Random.Range(0, cheatersIndex.Count);
        playables[index] = true;
        cheatersIndex.RemoveAt(index);
      }
      // Place cheaters.
      PlaceCheatersAppart(cheatersConf.amount, freeDesks, playables);
    }

    void PlaceFromList(int index, List<Desk> list, Student student) {
      Desk desk = list[index];
      desk.Student = student;
      student.transform.SetParent(desk.transform);
      student.transform.position = studentPosOffset + desk.transform.position;
      list.RemoveAt(index);
    }

    void PlaceCheater(bool playable, Desk desk, List<Desk> freeSpaces) {
      // Replace student sitting there.
      if (desk.HasStudent && freeSpaces.Count == 0) {
        Debug.LogWarning("Too many students.");
      } else if (desk.HasStudent) {
        PlaceFromList(0, freeSpaces, desk.Student);
      // Remove desk from free spaces, cheater is sitting there.
      } else {
        freeSpaces.Remove(desk);
      }

      Cheater cheater = Instantiate(playable ? cheatersConf.playablePrefab : cheatersConf.prefab);
      desk.Student = cheater;
      cheater.Grid = this;
      cheater.transform.SetParent(desk.transform);
      cheater.transform.position = desk.transform.position + studentPosOffset;
      cheater.IsPlayable = false;

      // Add to cheaters list.
      cheaters.Add(cheater);
    }

    void PlaceCheatersAppart(int amount, List<Desk> freeSpaces, bool[] playables) {
      // TODO: Do not allow students to be touching. (special case for middle cases)
      if (amount > 6) {
        Debug.LogWarning("Appart placement can't be bigger than 6.");
        amount = 6;
      }
      // Place first student in the middle.
      bool inCol = Random.Range(0,2) < 1;
      int[] range = GetMiddleRange(inCol);
      Desk desk = GetDeskInRange(range, inCol);
      // Place Student.
      PlaceCheater(playables[0], desk, freeSpaces);

      // Place students from 1 to 4 from.
      for (var i = 1; i < Mathf.Max(amount, 5); i++) {
        range = GetCornerRange(i - 1);
        desk = GetDeskInRange(range);
        // Place Student.
        PlaceCheater(playables[i], desk, freeSpaces);
      }

      // Place sixth student.
      if (amount == 6) {
        range = GetMiddleRange(!inCol);
        desk = GetDeskInRange(range, inCol);
        // Place Student.
        PlaceCheater(playables[5], desk, freeSpaces);
      }
    }

    int[] GetCornerRange(int corner) {
      int[] index = new int[4];
      int[] colRange = GetMiddleRange(true);
      int[] rowRange = GetMiddleRange(false);

      switch(corner) {
        case 0: // Bottom left.
          index[0] = 0;
          index[1] = colRange[0] - 1;
          index[2] = 0;
          index[3] = rowRange[0] - 1;
          break;
        case 1: // Top right.
          index[0] = colRange[1] + 1;
          index[1] = cols - 1;
          index[2] = rowRange[1] + 1;
          index[3] = rows - 1;
          break;
        case 2: // Bottom right.
          index[0] = colRange[1] + 1;
          index[1] = cols - 1;
          index[2] = 0;
          index[3] = rowRange[0] - 1;
          break;
        case 3: // Top left.
          index[0] = 0;
          index[1] = colRange[0] - 1;
          index[2] = rowRange[1] + 1;
          index[3] = rows - 1;
          break;
      }

      return index;
    }

    int[] GetMiddleRange(bool leadingCol) {
      int amount = leadingCol ? cols : rows;
      bool evenStart = amount % 2 == 0;
      int middle = amount / 2;
      int[] index = new int[4];
      index[0] = evenStart ? middle - 1 : middle;
      index[1] = evenStart ? index[0] + 1 : index[0];
      index[2] = 0;
      index[3] = leadingCol ? cols - 1 : rows - 1;
      return index;
    }

    Desk GetDeskInRange(int[] range, bool leadingCol = true) {
      int start = leadingCol ? 0 : 2;
      int col = Random.Range(range[start], range[start + 1] + 1);
      start = leadingCol ? 2 : 0;
      int row = Random.Range(range[start], range[start + 1] + 1);
      return grid[col * rows + row];
    }

    public Desk GetNeighbor(Desk desk, GridDirection direction) {
      Vector2Int coords = desk.Coords;

      switch(direction) {
        case GridDirection.Up:
          if (coords.y == rows - 1) {
            return null;
          } else {
            coords.y += 1;
          }
          break;
        case GridDirection.Left:
          if (coords.x == 0) {
            return null;
          } else {
            coords.x -= 1;
          }
          break;
        case GridDirection.Down:
          if (coords.y == 0) {
            return null;
          } else {
            coords.y -= 1;
          }
          break;
        case GridDirection.Right:
          if (coords.x == cols - 1) {
            return null;
          } else {
            coords.x += 1;
          }
          break;
      }

      return grid[coords.y * cols + coords.x];
    }
	}
}