package snakesimulator

import (
	context "context"
	fmt "fmt"
	math "math"
	"math/rand"
	"net"
	"time"

	"gonum.org/v1/gonum/mat"

	genetic "../genetic"
	grpc "google.golang.org/grpc"
)

type SnakeSimulator struct {
	field            *Field
	snake            *Snake
	stats            *Stats
	speed            uint32
	fieldUpdateQueue chan bool
	snakeUpdateQueue chan bool
	statsUpdateQueue chan bool
	speedQueue       chan uint32
}

func NewSnakeSimulator() (s *SnakeSimulator) {
	s = &SnakeSimulator{
		field: &Field{
			Food:   &Point{},
			Width:  40,
			Height: 40,
		},
		snake: &Snake{
			Points: []*Point{
				&Point{X: 20, Y: 20},
				&Point{X: 21, Y: 20},
				&Point{X: 22, Y: 20},
			},
		},
		stats:            &Stats{},
		fieldUpdateQueue: make(chan bool, 2),
		snakeUpdateQueue: make(chan bool, 2),
		statsUpdateQueue: make(chan bool, 2),
		speedQueue:       make(chan uint32, 1),
		speed:            10,
	}
	return
}

func (s *SnakeSimulator) Verify(population *genetic.Population) (results []*genetic.IndividalResult) {
	s.stats.Generation++
	s.statsUpdateQueue <- true

	results = make([]*genetic.IndividalResult, len(population.Networks))
	for index, inidividual := range population.Networks {
		s.stats.Individual = uint32(index)
		s.statsUpdateQueue <- true

		s.field.GenerateNextFood()
		rand.Seed(time.Now().UnixNano())
		switch rand.Uint32() % 4 {
		case 1:
			s.snake = &Snake{
				Points: []*Point{
					&Point{X: 20, Y: 20},
					&Point{X: 21, Y: 20},
					&Point{X: 22, Y: 20},
				},
			}
		case 2:
			s.snake = &Snake{
				Points: []*Point{
					&Point{X: 22, Y: 20},
					&Point{X: 21, Y: 20},
					&Point{X: 20, Y: 20},
				},
			}
		case 3:
			s.snake = &Snake{
				Points: []*Point{
					&Point{X: 20, Y: 20},
					&Point{X: 20, Y: 21},
					&Point{X: 20, Y: 22},
				},
			}
		default:
			s.snake = &Snake{
				Points: []*Point{
					&Point{X: 20, Y: 22},
					&Point{X: 20, Y: 21},
					&Point{X: 20, Y: 20},
				},
			}
		}

		i := 0
		s.stats.Move = 0
		for i < 300 {
			s.statsUpdateQueue <- true
			//Read speed from client
			select {
			case newSpeed := <-s.speedQueue:
				fmt.Printf("Apply new speed: %v\n", newSpeed)
				if newSpeed < 10 {
					if newSpeed > 0 {
						s.speed = newSpeed
					} else {
						s.speed = 0
					}
				}
			default:
			}
			i++
			if s.speed > 0 {
				s.snakeUpdateQueue <- true
			}
			direction, _ := inidividual.Predict(mat.NewDense(inidividual.Sizes[0], 1, s.GetHeadState()))
			newHead := s.snake.NewHead(Direction(direction + 1))
			if newHead.X == s.field.Food.X && newHead.Y == s.field.Food.Y {
				s.snake.Feed(newHead)
				s.field.GenerateNextFood()
				s.fieldUpdateQueue <- true
			} else if newHead.X >= s.field.Width || newHead.Y >= s.field.Height {
				// fmt.Printf("Game over\n")
				// time.Sleep(1000 * time.Millisecond)
				break
			} else if selfCollisionIndex := s.snake.SelfCollision(newHead); selfCollisionIndex > 0 {
				if selfCollisionIndex == 1 {
					// fmt.Printf("Step backward, skip\n")
					continue
				}
				fmt.Printf("Game over self collision\n")
				break
			} else {
				s.snake.Move(newHead)
			}
			s.stats.Move++

			if s.speed > 0 {
				time.Sleep(100 / time.Duration(s.speed) * time.Millisecond)
			}
		}

		results[index] = &genetic.IndividalResult{
			Result: float64(len(s.snake.Points)) * float64(s.stats.Move),
			Index:  index,
		}
	}
	return
}

func (s *SnakeSimulator) GetHeadState() []float64 {
	headX := int32(s.snake.Points[0].X)
	headY := int32(s.snake.Points[0].Y)
	foodX := int32(s.field.Food.X)
	foodY := int32(s.field.Food.Y)
	width := int32(s.field.Width)
	height := int32(s.field.Height)
	diag := float64(width) * math.Sqrt2

	lWall := headX
	rWall := width - headX
	tWall := headY
	bWall := height - headY
	lFood := int32(0)
	rFood := int32(0)
	tFood := int32(0)
	bFood := int32(0)

	tlFood := float64(0)
	trFood := float64(0)
	blFood := float64(0)
	brFood := float64(0)
	tlWall := float64(0)
	trWall := float64(0)
	blWall := float64(0)
	brWall := float64(0)

	if foodX == headX {
		if foodY > headY {
			bFood = foodY - headY
		} else {
			tFood = headY - foodY
		}
	}

	if foodY == headY {
		if foodX > headX {
			rFood = foodX - headX
		} else {
			lFood = headX - foodX
		}
	}

	if lWall > tWall {
		tlWall = float64(tWall) * math.Sqrt2
	} else {
		tlWall = float64(lWall) * math.Sqrt2
	}

	if rWall > tWall {
		trWall = float64(tWall) * math.Sqrt2
	} else {
		trWall = float64(rWall) * math.Sqrt2
	}

	if lWall > bWall {
		blWall = float64(bWall) * math.Sqrt2
	} else {
		blWall = float64(lWall) * math.Sqrt2
	}

	if rWall > bWall {
		blWall = float64(bWall) * math.Sqrt2
	} else {
		brWall = float64(rWall) * math.Sqrt2
	}

	foodDiagXDiff := math.Abs(float64(foodX - headX))
	foodDiagYDiff := math.Abs(float64(foodY - headY))
	if foodDiagXDiff == foodDiagYDiff {
		if math.Signbit(float64(foodX - headX)) {
			if math.Signbit(float64(foodY - headY)) {
				trFood = foodDiagXDiff * math.Sqrt2
			} else {
				brFood = foodDiagXDiff * math.Sqrt2
			}
		} else {
			if math.Signbit(float64(foodY - headY)) {
				tlFood = foodDiagXDiff * math.Sqrt2
			} else {
				blFood = foodDiagXDiff * math.Sqrt2
			}
		}
	}

	return []float64{
		float64(lWall) / float64(width),
		float64(lFood) / float64(lWall),
		float64(rWall) / float64(width),
		float64(rFood) / float64(rWall),
		float64(tWall) / float64(height),
		float64(tFood) / float64(tWall),
		float64(bWall) / float64(height),
		float64(bFood) / float64(bWall),
		float64(tlWall) / diag,
		float64(tlFood) / diag,
		float64(trFood) / diag,
		float64(trWall) / diag,
		float64(blFood) / diag,
		float64(blWall) / diag,
		float64(brFood) / diag,
		float64(brWall) / diag,
	}
}

func (s *SnakeSimulator) StartServer() {
	go func() {
		grpcServer := grpc.NewServer()
		RegisterSnakeSimulatorServer(grpcServer, s)
		lis, err := net.Listen("tcp", "localhost:65002")
		if err != nil {
			fmt.Printf("Failed to listen: %v\n", err)
		}

		fmt.Printf("Listen SnakeSimulator localhost:65002\n")
		if err := grpcServer.Serve(lis); err != nil {
			fmt.Printf("Failed to serve: %v\n", err)
		}
	}()
}

func (s *SnakeSimulator) Run() {
	s.field.GenerateNextFood()
	for true {
		direction := rand.Int31()%4 + 1
		newHead := s.snake.NewHead(Direction(direction))
		if newHead.X == s.field.Food.X && newHead.Y == s.field.Food.Y {
			s.snake.Feed(newHead)
			s.field.GenerateNextFood()
			s.fieldUpdateQueue <- true
		} else if newHead.X > s.field.Width || newHead.Y > s.field.Height {
			fmt.Printf("Game over\n")
			break
		} else if selfCollisionIndex := s.snake.SelfCollision(newHead); selfCollisionIndex > 0 {
			if selfCollisionIndex == 1 {
				fmt.Printf("Step backward, skip\n")
				continue
			}
			fmt.Printf("Game over self collision\n")
			break
		} else {
			s.snake.Move(newHead)
		}
		s.snakeUpdateQueue <- true
		time.Sleep(50 * time.Millisecond)
	}
}

func (s *SnakeSimulator) Field(_ *None, srv SnakeSimulator_FieldServer) error {
	ctx := srv.Context()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		srv.Send(s.field)
		<-s.fieldUpdateQueue
	}
}

func (s *SnakeSimulator) Snake(_ *None, srv SnakeSimulator_SnakeServer) error {
	ctx := srv.Context()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		srv.Send(s.snake)
		<-s.snakeUpdateQueue
	}
}

func (s *SnakeSimulator) Stats(_ *None, srv SnakeSimulator_StatsServer) error {
	ctx := srv.Context()
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		srv.Send(s.stats)
		<-s.statsUpdateQueue
	}
}

func (s *SnakeSimulator) SetSpeed(ctx context.Context, speed *Speed) (*None, error) {
	s.speedQueue <- speed.Speed
	return &None{}, nil
}