package pong

import (
	"encoding/json"
	"fmt"
	"log"
	"log/slog"
	"net"
	"strings"
	"time"

	"golang.org/x/exp/rand"
)

type GameClient struct {
	Username string
	Conn     net.Conn
}

const posXBound = 52
const negXBound = posXBound * -1
const posYBound = 50
const negYBound = posYBound * -1

func StartGame(conn1, conn2 net.Conn, username1, username2 string) {

	var player1 GameClient
	var player2 GameClient

	egress := make(chan StateUpdate)
	ingress := make(chan StateUpdate)

	player1 = GameClient{
		Username: username1,
		Conn:     conn1,
	}

	p1msg := StateUpdate{
		FieldPath: "isPlayer1",
		Value:     []byte{1},
	}
	b, err := json.Marshal(p1msg)
	if err != nil {
		return
	}
	_, err = player1.Conn.Write(b)
	if err != nil {
		fmt.Println("Error writing player1 msg to player1")
		return
	}

	player2 = GameClient{
		Username: username2,
		Conn:     conn2,
	}

	p2msg := StateUpdate{
		FieldPath: "isPlayer1",
		Value:     []byte{0},
	}
	b, err = json.Marshal(p2msg)
	if err != nil {
		return
	}
	_, err = player2.Conn.Write(b)
	if err != nil {
		fmt.Println("Error writing player1 msg to player2")
		return
	}

	time.Sleep(1 * time.Second)
	broadcastUpdate(StateUpdate{
		FieldPath: "Message",
		Value:     []byte("Ready..."),
	}, player1, player2)
	time.Sleep(1 * time.Second)
	broadcastUpdate(StateUpdate{
		FieldPath: "Message",
		Value:     []byte("Set..."),
	}, player1, player2)
	time.Sleep(1 * time.Second)
	broadcastUpdate(StateUpdate{
		FieldPath: "Message",
		Value:     []byte("Go!"),
	}, player1, player2)
	time.Sleep(1 * time.Second)
	bv := float32(rand.Intn(2)*2 - 1)

	state := GameState{
		Score: map[string]int{player1.Username: 0, player2.Username: 0},
		Player1: Player{
			client: player1,
			Pos: Vector{
				X: -50,
				Y: 0,
			},
			Size: Vector{
				X: 1,
				Y: 10,
			},
		},
		Player2: Player{
			client: player2,
			Pos: Vector{
				X: 50,
				Y: 0,
			},
			Size: Vector{
				X: 1,
				Y: 10,
			},
		},
		Ball: Ball{
			Pos: Vector{
				X: 0,
				Y: 0,
			},
			Vel: Vector{
				X: bv,
				Y: 0,
			},
		},
	}
	go gameLoop(&state, ingress, egress, player1, player2)
}

func gameLoop(state *GameState, ingress, egress chan (StateUpdate), player1, player2 GameClient) {
	// Player 1 read loop
	go func() {
		for {

			bytes := make([]byte, 512)
			n, err := state.Player1.client.Conn.Read(bytes)
			msg := StateUpdate{}
			err = json.Unmarshal(bytes[:n], &msg)
			if err != nil {
				log.Println("error reading player 1's update request:", err)
				return
			}
			ingress <- msg
			if msg.FieldPath == "Winner" {
				return
			}
		}
	}()

	// Player 2 read loop
	go func() {
		for {

			bytes := make([]byte, 512)
			n, err := state.Player2.client.Conn.Read(bytes)
			msg := StateUpdate{}
			err = json.Unmarshal(bytes[:n], &msg)
			if err != nil {
				log.Println("error reading player 2's update request:", err)
				return
			}
			ingress <- msg
			if msg.FieldPath == "Winner" {
				return
			}
		}
	}()

	go func() {
		for {
			msg := <-egress
			broadcastUpdate(msg, player1, player2)
			if msg.FieldPath == "Winner" {
				return
			}
		}
	}()

	ticker := time.NewTicker(time.Second / 64)

	for {
		select {
		case msg := <-ingress:
			err := handlePlayerRequest(msg, state)
			if err != nil {
				fmt.Println("FUCK!~", err)
			}
			if msg.FieldPath == "Winner" {
				slog.Debug("Closing game loop on winner message")
				return
			}

		case _ = <-ticker.C:
			update := process(state, player1, player2)
			egress <- update
			if update.FieldPath == "Winner" {
				slog.Debug("Closing game loop")
				return
			}
		}
	}
}

func process(state *GameState, player1, player2 GameClient) StateUpdate {
	// Move players
	// Check if player edge is out of bounds
	//	If out of bounds reset velocity to zero and position to edge

	if state.Player1.Pos.Y+state.Player1.Size.Y/2 > posYBound {
		state.Player1.Pos.Y = posYBound - state.Player1.Size.Y/2 - 1
	}
	if state.Player1.Pos.Y-state.Player1.Size.Y/2 < negYBound {
		state.Player1.Pos.Y = negYBound + state.Player1.Size.Y/2 + 1
	}

	if state.Player2.Pos.Y+state.Player2.Size.Y/2 > posYBound {
		state.Player2.Pos.Y = posYBound - state.Player2.Size.Y/2 - 1
	}
	if state.Player2.Pos.Y-state.Player2.Size.Y/2 < negYBound {
		state.Player2.Pos.Y = negYBound + state.Player2.Size.Y/2 + 1
	}

	// Move ball
	// Check if ball is out of bounds
	//	if out of bounds y,
	//		bounce by inverting y velocity and finding difference from bounds to out and reflect distance
	//	if out of bounds x,
	//		check if paddle is nearby, bounce by inverting and finding the remaining distance to the new position.
	//		or adjust score and ball position

	state.Ball.Pos.X = state.Ball.Pos.X + state.Ball.Vel.X
	state.Ball.Pos.Y = state.Ball.Pos.Y + state.Ball.Vel.Y

	if state.Ball.Pos.Y >= posYBound-1 && state.Ball.Vel.Y > 0 {
		state.Ball.Pos.Y = (posYBound - 1) - (state.Ball.Pos.Y - (posYBound - 1))
		state.Ball.Vel.Y = state.Ball.Vel.Y * -1
	}
	if state.Ball.Pos.Y <= negYBound+1 && state.Ball.Vel.Y < 0 {
		state.Ball.Pos.Y = (negYBound + 1) - (state.Ball.Pos.Y - (negYBound + 1))
		state.Ball.Vel.Y = state.Ball.Vel.Y * -1
	}

	// If the ball is within 1 pixel of x bounds and heading to the left (Player 1)
	if state.Ball.Pos.X <= negXBound+1 && state.Ball.Vel.X < 0 {
		// Paddle hit!
		if state.Ball.Pos.Y <= state.Player1.Pos.Y+state.Player1.Size.Y && state.Ball.Pos.Y >= state.Player1.Pos.Y-state.Player1.Size.Y {
			slog.Debug("Player1 paddle hit!")
			state.Ball.Pos.X = (negXBound + 1) - (state.Ball.Pos.X - (negXBound + 1))
			state.Ball.Vel.X = state.Ball.Vel.X * -1.001
			angleTweak := (state.Ball.Pos.Y - state.Player1.Pos.Y) / (state.Player1.Size.Y / 2)
			state.Ball.Vel.Y = angleTweak / 10
		} else {
			slog.Debug("Player1 paddle miss...")
			state.Ball.Pos.X = 0
			state.Ball.Pos.Y = 0
			state.Ball.Vel.X = 1
			state.Ball.Vel.Y = 0
			if state.Score[player2.Username] >= 9 {
				return StateUpdate{
					FieldPath: "Winner",
					Value:     []byte(player2.Username),
				}
			}
			state.Score[player2.Username] = state.Score[player2.Username] + 1
		}
	}

	// If the ball is within 1 pixel of x bounds and heading towards player2 (to the right)
	if state.Ball.Pos.X > posXBound-1 && state.Ball.Vel.X > 0 {
		// Paddle hit!
		if state.Ball.Pos.Y <= state.Player2.Pos.Y+state.Player2.Size.Y && state.Ball.Pos.Y >= state.Player2.Pos.Y-state.Player2.Size.Y {
			slog.Debug("Player2 paddle hit!")
			state.Ball.Pos.X = (posXBound - 1) - (state.Ball.Pos.X - (posXBound - 1))
			state.Ball.Vel.X = state.Ball.Vel.X * -1.001
			angleTweak := (state.Ball.Pos.Y - state.Player2.Pos.Y) / (state.Player2.Size.Y / 2)
			state.Ball.Vel.Y = angleTweak / 10
		} else {
			slog.Debug("Player2 paddle miss...")
			state.Ball.Pos.X = 0
			state.Ball.Pos.Y = 0
			state.Ball.Vel.X = -1
			state.Ball.Vel.Y = 0
			if state.Score[player1.Username] >= 9 {
				return StateUpdate{
					FieldPath: "Winner",
					Value:     []byte(player1.Username),
				}
			}
			state.Score[player1.Username] = state.Score[player1.Username] + 1
		}
	}

	ns, err := json.Marshal(state)
	if err != nil {
		slog.Debug("error marshalling entire state update", slog.Any("error", err))
	}

	return StateUpdate{
		FieldPath: "All",
		Value:     ns,
	}
}

func handlePlayerRequest(update StateUpdate, state *GameState) error {

	fields := strings.Split(update.FieldPath, ".")

	// type GameState struct {
	// 	Message string
	// 	Winner  string
	// 	Score   map[string]int
	// 	Player1 Player
	// 	Player2 Player
	// 	Ball    Ball
	// }

	switch fields[0] {
	case "Message":
		state.Message = string(update.Value)
	case "Winner":
		state.Winner = string(update.Value)
	case "Player1":
		switch fields[1] {
		case "Pos":
			v1 := Vector{}
			err := json.Unmarshal(update.Value, &v1)
			if err != nil {
				slog.Debug("error unmarshalling player1 update")
			}
			state.Player1.Pos = v1
		}
	case "Player2":
		switch fields[1] {
		case "Pos":
			v2 := Vector{}
			err := json.Unmarshal(update.Value, &v2)
			if err != nil {
				slog.Debug("error unmarshalling player2 update")
			}
			state.Player2.Pos = v2
		}
	case "Ball":
		b := Ball{}
		err := json.Unmarshal(update.Value, &b)
		if err != nil {
			slog.Debug("error unmarshalling ball update")
		}
		state.Ball = b
	}
	return nil
}

func broadcastUpdate(update StateUpdate, player1, player2 GameClient) error {
	msg, err := json.Marshal(update)
	if err != nil {
		return err
	}
	_, err = player1.Conn.Write(msg)
	if err != nil {
		return err
	}
	_, err = player2.Conn.Write(msg)
	if err != nil {
		return err
	}

	return nil
}
