package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"io/ioutil"
	"math"
	"math/rand"
	"os"
	"sort"
	"strconv"

	"github.com/llgcode/draw2d/draw2dimg"
	"gopkg.in/yaml.v2"
)

// parameters
var windowWidth, windowHeight = 1000, 700
var goidSize = 3
var goidColor = color.RGBA{200, 200, 100, 255} // gray, 50% transparency
var populationSize = 20
var loops = 500
var numNeighbours = 5
var separationFactor = float64(goidSize * 5)
var coherenceFactor = 8

type steps struct {
	Step          int `yaml:"step"`
	Amount        int `yaml:"amount"`
	NumNeighbours int `yaml:"neighbours"`
}

type Coor struct {
	X int `yaml:"x"`
	Y int `yaml:"y"`
}
type OutPut struct {
	Goid int  `yaml:"goid"`
	Loop int  `yaml:"loop"`
	Data Coor `yaml:"data"`
}

func fixGoids(gs []*Goid, actual int, wished int) ([]*Goid, int) {
	if actual < wished {
		g := createRandomGoid()
		gs = append(gs, &g)
		return gs, actual + 1
	} else if wished < actual {
		return gs[:len(gs)-1], actual - 1
	} else {
		return gs, actual
	}
}

func main() {
	fo, err := os.Create("/tmp/goids.yaml")
	if err != nil {
		panic(err)
	}
	conf, err := ioutil.ReadFile("config.yaml")
	if err != nil {
		panic(err)
	}
	td := []steps{}
	err = yaml.Unmarshal(conf, &td)
	defer func() {
		if err := fo.Close(); err != nil {
			panic(err)
		}
	}()
	clearScreen()
	hideCursor()
	var goids []*Goid

	for i := 0; i < populationSize; i++ {
		g := createRandomGoid()
		goids = append(goids, &g)
	}

	current_stage := 0

	for i := 0; i < loops; i++ {
		goids, populationSize = fixGoids(goids, populationSize, td[current_stage].Amount)
		numNeighbours = td[current_stage].NumNeighbours
		if i > td[current_stage].Step {
			current_stage += 1
		}
		move(goids, fo, i)
		frame := draw(goids)
		printImage(frame.SubImage(frame.Rect))
	}
	showCursor()
	
	out, err := ioutil.ReadFile("/tmp/goids.yaml")
	if err != nil {
		panic(err)
	}
	output := []OutPut{}
	err = yaml.Unmarshal(out, &output)
	var total int = 0
	for _, goid := range output {
		if total < goid.Goid {
			total = goid.Goid
		}
	}

	outfile, err := os.Create("./goids.txt")
	if err != nil {
		panic(err)
	}
	for i := 0; i <= total; i++{
		var x []string
		var y []string
		outfile.Write([]byte(fmt.Sprintf("GOID: %d\n", i)))
		for z := 0; z <= loops; z++ {
			check := false
			for _, g := range output {
				if g.Goid == i && g.Loop == z{
					check = true
  				x = append(x, strconv.Itoa(g.Data.X))
					y = append(y, strconv.Itoa(g.Data.Y))
					break
				}
			}
			if !check  {
  				x = append(x,  "-")
					y = append(y,  "-")
			}
		}

		outfile.Write([]byte(fmt.Sprintf("%v\n", x)))
		outfile.Write([]byte(fmt.Sprintf("%v\n", y)))
	}
}

// Goid represents a drawn goid
type Goid struct {
	X     int // position
	Y     int
	Vx    int // velocity
	Vy    int
	R     int // radius
	Color color.Color
}

func createRandomGoid() (g Goid) {
	g = Goid{
		X:     rand.Intn(windowWidth),
		Y:     rand.Intn(windowHeight),
		Vx:    rand.Intn(goidSize),
		Vy:    rand.Intn(goidSize),
		R:     goidSize,
		Color: goidColor,
	}
	return
}

// find the nearest neighbours
func (g *Goid) nearestNeighbours(goids []*Goid) (neighbours []Goid) {
	neighbours = make([]Goid, len(goids))
	for _, goid := range goids {
		neighbours = append(neighbours, *goid)
	}
	sort.SliceStable(neighbours, func(i, j int) bool {
		return g.distance(neighbours[i]) < g.distance(neighbours[j])
	})
	return
}

// distance between 2 goids
func (g *Goid) distance(n Goid) float64 {
	x := g.X - n.X
	y := g.Y - n.Y
	return math.Sqrt(float64(x*x + y*y))

}

// move the goids with the 3 classic boid rules
func move(goids []*Goid, file *os.File, loop int) {
	for i, goid := range goids {
		neighbours := goid.nearestNeighbours(goids)
		separate(goid, neighbours)
		align(goid, neighbours)
		cohere(goid, neighbours)
		position := fmt.Sprintf("- goid: %d\n  loop: %d\n  data:\n    x: %d\n    y: %d\n", i, loop, goid.X, goid.Y)
		file.Write([]byte(position))
		stayInWindow(goid)
	}
}

// if goid goes out of the window frame it comes back on the other side
func stayInWindow(goid *Goid) {
	if goid.X < 0 {
		goid.X = windowWidth - goid.X
	} else if goid.X > windowWidth {
		goid.X = windowWidth - goid.X
	}
	if goid.Y < 0 {
		goid.Y = windowHeight - goid.Y
	} else if goid.Y > windowHeight {
		goid.Y = windowHeight - goid.Y
	}
}

// steer to avoid crowding local goids
func separate(g *Goid, neighbours []Goid) {
	x, y := 0, 0
	for _, n := range neighbours[0:numNeighbours] {
		if g.distance(n) < separationFactor {
			x += g.X - n.X
			y += g.Y - n.Y
		}
	}
	g.Vx = x
	g.Vy = y
	g.X += x
	g.Y += y
}

// steer towards the average heading of local goids
func align(g *Goid, neighbours []Goid) {
	x, y := 0, 0
	for _, n := range neighbours[0:numNeighbours] {
		x += n.Vx
		y += n.Vy
	}
	dx, dy := x/numNeighbours, y/numNeighbours
	g.Vx += dx
	g.Vy += dy
	g.X += dx
	g.Y += dy
}

// steer to move toward the average position of local goids
func cohere(g *Goid, neighbours []Goid) {
	x, y := 0, 0
	for _, n := range neighbours[0:numNeighbours] {
		x += n.X
		y += n.Y
	}
	dx, dy := ((x/numNeighbours)-g.X)/coherenceFactor, ((y/numNeighbours)-g.Y)/coherenceFactor
	g.Vx += dx
	g.Vy += dy
	g.X += dx
	g.Y += dy
}

// draw the goids
func draw(goids []*Goid) *image.RGBA {
	dest := image.NewRGBA(image.Rect(0, 0, windowWidth, windowHeight))
	gc := draw2dimg.NewGraphicContext(dest)
	for _, goid := range goids {
		gc.SetFillColor(goid.Color)
		gc.MoveTo(float64(goid.X), float64(goid.Y))
		gc.ArcTo(float64(goid.X), float64(goid.Y), float64(goid.R), float64(goid.R), 0, -math.Pi*2)
		gc.LineTo(float64(goid.X-goid.Vx), float64(goid.Y-goid.Vy))
		gc.Close()
		gc.Fill()
	}
	return dest
}

// ANSI escape sequence codes to perform action on terminal
func hideCursor() {
	fmt.Print("\033[?25l")
}

func showCursor() {
	fmt.Print("\x1b[?25h\n")
}

func clearScreen() {
	fmt.Print("\x1b[2J")
}

// this only works for iTerm!
func printImage(img image.Image) {
	var buf bytes.Buffer
	png.Encode(&buf, img)
	imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
	fmt.Printf("\x1b[2;0H\x1b]1337;File=inline=1:%s\a", imgBase64Str)
}
