package main

import (
	"fmt"
	"log"
	"math"
	"math/rand"
	"sync"
	"time"
)

type RoomItem struct {
	Index int `json:"i"`
	Type  int `json:"t"`
}

type Room struct {
	Id         string     `json:"id"`
	Field      []int      `json:"-"`
	Items      []RoomItem `json:"items"`
	Size       int        `json:"size"`
	Players    []*Player  `json:"players"`
	MaxPlayers int        `json:"maxPlayers"`
	IsClosed   bool       `json:"isClosed"`
	CreatedAt  time.Time  `json:"-"`
	Lock sync.Mutex `json:"-"`
}

type WsDataRoom struct {
	Id         string     `json:"id"`
	Items      []RoomItem `json:"items"`
	Size       int        `json:"size"`
	Players    []WsDataPlayer  `json:"players"`
	MaxPlayers int        `json:"maxPlayers"`
	IsClosed   bool       `json:"isClosed"`
}

type WsData struct {
	PlayerId string `json:"playerId"`
	Now      int64  `json:"now"`
	Room     WsDataRoom  `json:"room"`
}

func NewRoom() *Room {
	room := &Room{
		Id:         fmt.Sprintf("%d", rand.Int()),
		Size:       32,
		MaxPlayers: 4,
		IsClosed:   false,
		CreatedAt:  time.Now(),
	}

	for i := 0; i < room.Size*room.Size; i++ {
		room.Field = append(room.Field, 0)
	}

	for i := 0; i < room.MaxPlayers; i++ {
		room.Field[rand.Int()%(room.Size*room.Size)] = 2
	}

	return room
}

func (room *Room) GetDrawableField() []int {
	df := append([]int{}, room.Field...)

	for _, player := range room.Players {
		for _, t := range player.Tail {
			df[t.Y*room.Size+t.X] = 3 + player.Color
		}
	}

	return df
}

func (room *Room) MakeNewItem(item int) bool {
  df := room.GetDrawableField()

	for attempt := 0; attempt < 32; attempt++ {
		x := rand.Int() % room.Size
		y := rand.Int() % room.Size

		exists := false

    if df[y * room.Size + x] != 0 {
      exists = true
    }

    if !exists {
      for _, p := range room.Players {
        if math.Abs(float64(p.X - x)) < 4 || math.Abs(float64(p.Y - y)) < 4 {
          exists = true
          break
        }
      }
    }

		if !exists {
			room.Field[y*room.Size+x] = item
			return true
		}
	}

	return false
}

func (room *Room) GetNewPlayerTail() []XY {
	df := room.GetDrawableField()

	for attempt := 0; attempt < 32; attempt++ {
		x := rand.Int() % room.Size
		y := rand.Int() % room.Size

		exists := false

		for qx := x - 3; qx <= x+3; qx++ {
			qx1 := qx
			if qx1 < 0 {
				qx1 = room.Size + qx1
			} else if qx1 >= room.Size {
				qx1 = qx1 % room.Size
			}

			for qy := y - 3; qy <= y+3; qy++ {
				qy1 := qy
				if qy1 < 0 {
					qy1 = room.Size + qy1
				} else if qy1 >= room.Size {
					qy1 = qy1 % room.Size
				}

				if df[qy1*room.Size+qx1] != 0 {
					exists = true
					break
				}
			}

			if exists {
				break
			}
		}

		if !exists {
			var tail []XY

			switch rand.Int() % 4 {
			case 0:
				y -= 2
				if y < 0 {
					y += room.Size
				}

				for q := 0; q < 3; q++ {
					tail = append(tail, XY{
						X: x,
						Y: y,
					})

					y = (y + 1) % room.Size
				}

				break

			case 1:
				y = (y + 2) % room.Size
				if y < 0 {
					y += room.Size
				}

				for q := 0; q < 3; q++ {
					tail = append(tail, XY{
						X: x,
						Y: y,
					})

					y = y - 1
					if y < 0 {
						y += room.Size
					}
				}

				break

			case 2:
				x -= 2
				if x < 0 {
					x += room.Size
				}

				for q := 0; q < 3; q++ {
					tail = append(tail, XY{
						X: x,
						Y: y,
					})

					x = (x + 1) % room.Size
				}

				break

			case 3:
				x = (x + 2) % room.Size
				if x < 0 {
					x += room.Size
				}

				for q := 0; q < 3; q++ {
					tail = append(tail, XY{
						X: x,
						Y: y,
					})

					x = x - 1
					if x < 0 {
						x += room.Size
					}
				}

				break

			}

			return tail
		}
	}

	return nil
}

func (room *Room) GetAvailableColors() []int {
	var existingColors []int

	for _, player := range room.Players {
		existingColors = append(existingColors, player.Color)
	}

	var colors []int
	for i := 0; i < 6; i++ {
		exists := false
		for _, c := range existingColors {
			if i == c {
				exists = true
				break
			}
		}

		if !exists {
			colors = append(colors, i)
		}
	}

	return colors
}

func (room *Room) UpdateItems() {
	room.Items = []RoomItem{}
	for i, t := range room.Field {
		if t != 0 {
			room.Items = append(room.Items, RoomItem{
				Index: i,
				Type:  t,
			})
		}
	}
}

func (room *Room) SendData(currTime int64) {
	room.UpdateItems()

	var pl []*Player
  var wsPlayers []WsDataPlayer

	isAnyUpdated := false
	for _, player := range room.Players {
		if player.IsLastTimeUpdated {
			isAnyUpdated = true
		}

    wsPlayers = append(wsPlayers, WsDataPlayer{
      Id: player.Id,
      RoomId: player.RoomId,

      Color: player.Color,
      Speed: player.Speed,
      UpdatedAt: player.UpdatedAt,

      X: player.X,
      Y: player.Y,
      Dx: player.Dx,
      Dy: player.Dy,

      Tail: player.Tail,

      GameOver: player.GameOver,
    })
	}

	for _, player := range room.Players {
    player.Lock.Lock()
    if player.Connection == nil {
      player.Lock.Unlock()
      continue
    }
    shouldSend := isAnyUpdated || currTime - player.LastSendAt > 1000
    player.Lock.Unlock()

		if shouldSend {
      data := WsData{
        PlayerId: player.Id,
        Now: currTime,
        Room: WsDataRoom{
          Id: room.Id,
          Items: room.Items,
          Size: room.Size,
          Players: wsPlayers,
          MaxPlayers: room.MaxPlayers,
          IsClosed: room.IsClosed,
        },
      }

      go func(player *Player, data WsData){
        player.Lock.Lock()
        if err := player.Connection.WriteJSON(data); err != nil {
          player.Connection = nil
          log.Println(err)
        }

        player.LastSendAt = currTime
        player.Lock.Unlock()
      }(player, data)
		}

		pl = append(pl, player)
	}

	room.Players = pl

	if len(pl) == 0 {
		room.IsClosed = true
	}
}

func (room *Room) Process() {
  log.Println("Room started", room.Id)

	for !room.IsClosed && len(room.Players) > 0 {
		time.Sleep(1 * time.Millisecond)

		room.Lock.Lock()
		currTime := time.Now().UnixNano() / int64(time.Millisecond)

		df := room.GetDrawableField()

		hasActivePlayers := false
		for _, player := range room.Players {
			player.IsLastTimeUpdated = false

			if !player.GameOver {
				hasActivePlayers = true
			}

			if player.LastInput.Direction != 0 {
				/*hasInTail := false
				if len(player.Tail) == len(player.LastInput.Tail) {
					for _, coord := range player.Tail {
						if math.Abs(float64(coord.X-player.LastInput.X)) <= 1 && math.Abs(float64(coord.Y-player.LastInput.Y)) <= 1 {
							hasInTail = true
							break
						}
					}
				}*/

				if true {
					switch player.LastInput.Direction {
					case 1:
						if player.Dx != 0 {
							/*player.X = player.LastInput.X
							player.Y = player.LastInput.Y
							player.Tail = player.LastInput.Tail*/

							player.Dx = 0
							player.Dy = -1

							player.IsLastTimeUpdated = true
						}
						break
					case 2:
						if player.Dy != 0 {
							/*player.X = player.LastInput.X
							player.Y = player.LastInput.Y
							player.Tail = player.LastInput.Tail*/

							player.Dx = 1
							player.Dy = 0

							player.IsLastTimeUpdated = true
						}
						break
					case 3:
						if player.Dx != 0 {
							/*player.X = player.LastInput.X
							player.Y = player.LastInput.Y
							player.Tail = player.LastInput.Tail*/

							player.Dx = 0
							player.Dy = 1

							player.IsLastTimeUpdated = true
						}
						break
					case 4:
						if player.Dy != 0 {
							/*player.X = player.LastInput.X
							player.Y = player.LastInput.Y
							player.Tail = player.LastInput.Tail*/

							player.Dx = -1
							player.Dy = 0

							player.IsLastTimeUpdated = true
						}
						break
					}
				}

				player.LastInput.Direction = 0

				df = room.GetDrawableField()
			}

			if currTime-player.UpdatedAt < player.Speed {
				continue
			}

			player.UpdatedAt = currTime

			x := (player.X + room.Size + player.Dx) % room.Size
			y := (player.Y + room.Size + player.Dy) % room.Size

			index := y*room.Size + x

			if df[index] == 2 {
				player.Tail = append([]XY{
					{
						X: x,
						Y: y,
					},
				}, player.Tail...)

				room.Field[index] = 0

				player.Speed -= 10
				if player.Speed < 100 {
					player.Speed = 100
				}

				player.IsLastTimeUpdated = true

				room.MakeNewItem(1)
				room.MakeNewItem(2)
			} else if df[index] == 0 {
				for i := len(player.Tail) - 1; i > 0; i-- {
					player.Tail[i].X = player.Tail[i-1].X
					player.Tail[i].Y = player.Tail[i-1].Y
				}

				player.Tail[0].X = x
				player.Tail[0].Y = y
			} else {
				player.GameOver = true
				player.IsLastTimeUpdated = true
				continue
			}

			player.X = x
			player.Y = y
		}

		if !room.IsClosed && !hasActivePlayers {
			room.IsClosed = true
		}

		room.SendData(currTime)
		room.Lock.Unlock()
	}

	roomsLock.Lock()

	var newRooms []*Room
	for _, room := range rooms {
		if room.IsClosed || len(room.Players) == 0 {
			continue
		}

		newRooms = append(newRooms, room)
	}

	rooms = newRooms
	roomsLock.Unlock()

  log.Println("Room finished", room.Id)
}
