package main

import (
	"encoding/binary"
	"image/color"
	"math"
	"math/rand"
	"os"
	"strconv"

	"github.com/chewxy/math32"
	"github.com/goki/gi/gi3d"
	"github.com/goki/gi/gist"
	"github.com/goki/mat32"
	"github.com/spaolacci/murmur3"
)

type classDetails struct {
	class       string
	brightColor color.RGBA
	medColor    color.RGBA
	dimColor    color.RGBA
	odds        float32
	fudge       float32
	minMass     float32
	deltaMass   float32
	minRadii    float32
	deltaRadii  float32
	minLum      float32
	deltaLum    float32
	pixels      int32
}

type star struct {
	id          int
	class       string
	brightColor color.RGBA
	dimColor    color.RGBA
	pixels      int32
	mass        float32
	radii       float32
	luminance   float32
	// 3D position
	x float32
	y float32
	z float32
	// sector location, 0 <= sx, sy, sz < 100
	sx float32
	sy float32
	sz float32
	// display location, 0 <= dx, dy, dz < 1000
}

type sector struct {
	x uint32
	y uint32
	z uint32
}

type position struct {
	x float32
	y float32
	z float32
}

type jump struct {
	color       gist.Color
	activeColor gist.Color
	parsecs     int
	distance    float32
	s1ID        int
	s2ID        int
}

type simpleLine struct {
	from     position
	to       position
	jumpInfo *jump
	lines    *gi3d.Lines
}

const (
	intensityStep = 8
	faster        = true
	fastest       = false
)

var (
	size       = position{x: 2, y: 2, z: 1}
	sizeFloats = position{x: float32(size.x), y: float32(size.y), z: float32(size.z)}
	offsets    = position{x: sizeFloats.x / -2.0, y: sizeFloats.y / -2.0, z: sizeFloats.z / -2.0}

	intensity = []uint8{
		0, 0, intensityStep, 2 * intensityStep, 3 * intensityStep, 4 * intensityStep, 5 * intensityStep,
	}
	jumpColors = []gist.Color{
		gist.Color(color.RGBA{R: math.MaxUint8 - eighth, G: 0, B: 0, A: math.MaxUint8 - intensity[0]}),
		gist.Color(color.RGBA{R: math.MaxUint8 - eighth, G: half + eighth - eighth, B: 0, A: math.MaxUint8 - intensity[1]}),
		gist.Color(color.RGBA{R: math.MaxUint8 - eighth, G: math.MaxUint8 - eighth, B: 0, A: math.MaxUint8 - intensity[2]}),
		gist.Color(color.RGBA{R: 0, G: math.MaxUint8 - eighth, B: 0, A: math.MaxUint8 - intensity[3]}),
		gist.Color(color.RGBA{R: 0, G: 0, B: math.MaxUint8 - eighth, A: math.MaxUint8 - intensity[4]}),
		//gist.Color(color.RGBA{R: math.MaxUint8 - quarter, G: 0, B: math.MaxUint8 - quarter, A: math.MaxUint8 - intensity[5]}),//
	}

	tween  = uint8(sevenEighths)
	med    = uint8(threeQuarters)
	dim    = uint8(half)
	noJump = jump{color: gist.Color(color.RGBA{R: 0, G: 0, B: 0, A: 0}), activeColor: gist.Color(color.RGBA{R: 0, G: 0, B: 0, A: 0}),
		parsecs: 0, distance: 20480.0, s1ID: -1, s2ID: -1}
	noLine = simpleLine{from: position{x: 0, y: 0, z: 0}, to: position{x: 0, y: 0, z: 0}, jumpInfo: &noJump, lines: &gi3d.Lines{}}

	jumpsByStar = make(map[int][]*jump)

	classO = classDetails{
		class:       "O",
		brightColor: color.RGBA{R: 0, G: 0, B: tween, A: opaque},
		medColor:    color.RGBA{R: 0, G: 0, B: tween, A: opaque},
		dimColor:    color.RGBA{R: 0, G: 0, B: med, A: opaque},
		odds:        .0000003,
		fudge:       .0000000402,
		minMass:     16.00001,
		deltaMass:   243.2,
		minRadii:    6,
		deltaRadii:  17.3,
		minLum:      30000,
		deltaLum:    147000.2,
		pixels:      11,
	}

	classB = classDetails{
		class:       "B",
		brightColor: color.RGBA{R: dim, G: dim, B: tween, A: opaque},
		medColor:    color.RGBA{R: dim / two, G: dim / two, B: half, A: opaque},
		dimColor:    color.RGBA{R: dim / (two * two), G: dim / (two * two), B: tween / two, A: opaque},
		odds:        .0013,
		fudge:       .0003,
		minMass:     2.1,
		deltaMass:   13.9,
		minRadii:    1.8,
		deltaRadii:  4.8,
		minLum:      25,
		deltaLum:    29975,
		pixels:      8,
	}

	classA = classDetails{
		class:       "A",
		brightColor: color.RGBA{R: tween, G: tween, B: tween, A: opaque},
		medColor:    color.RGBA{R: sevenEighths, G: sevenEighths, B: sevenEighths, A: opaque},
		dimColor:    color.RGBA{R: half, G: half, B: half, A: opaque},
		odds:        .006,
		fudge:       .0018,
		minMass:     1.4,
		deltaMass:   .7,
		minRadii:    1.4,
		deltaRadii:  .4,
		minLum:      5,
		deltaLum:    20,
		pixels:      6,
	}

	classF = classDetails{
		class:       "F",
		brightColor: color.RGBA{R: tween, G: tween, B: sevenEighths, A: opaque},
		medColor:    color.RGBA{R: sevenEighths, G: sevenEighths, B: half, A: opaque},
		dimColor:    color.RGBA{R: half, G: half, B: quarter / two, A: opaque},
		odds:        .03,
		fudge:       .012,
		minMass:     1.04,
		deltaMass:   .36,
		minRadii:    1.15,
		deltaRadii:  .25,
		minLum:      1.5,
		deltaLum:    3.5,
		pixels:      5,
	}

	classG = classDetails{
		class:       "G",
		brightColor: color.RGBA{R: tween, G: tween, B: 0, A: opaque},
		medColor:    color.RGBA{R: sevenEighths, G: sevenEighths, B: 0, A: opaque},
		dimColor:    color.RGBA{R: half, G: half, B: 0, A: opaque},
		odds:        .076,
		fudge:       .01102,
		minMass:     .8,
		deltaMass:   .24,
		minRadii:    .96,
		deltaRadii:  .19,
		minLum:      .6,
		deltaLum:    .9,
		pixels:      4,
	}

	classK = classDetails{
		class:       "K",
		brightColor: color.RGBA{R: tween, G: tween - eighth, B: tween - quarter, A: opaque},
		medColor:    color.RGBA{R: threeQuarters, G: threeQuarters - eighth, B: half, A: opaque},
		dimColor:    color.RGBA{R: half, G: half - eighth, B: quarter, A: opaque},
		odds:        .121,
		fudge:       .042,
		minMass:     .45,
		deltaMass:   .35,
		minRadii:    .7,
		deltaRadii:  .26,
		minLum:      .08,
		deltaLum:    .52,
		pixels:      3,
	}

	classM = classDetails{
		class:       "M",
		brightColor: color.RGBA{R: tween, G: 0, B: 0, A: opaque},
		medColor:    color.RGBA{R: sevenEighths, G: 0, B: 0, A: opaque},
		dimColor:    color.RGBA{R: threeQuarters, G: 0, B: 0, A: opaque},
		odds:        .7645,
		fudge:       .04,
		minMass:     1.04,
		deltaMass:   .36,
		minRadii:    1.15,
		deltaRadii:  .25,
		minLum:      1.5,
		deltaLum:    3.5,
		pixels:      2,
	}

	starDetailsByClass = [7]classDetails{classO, classB, classA, classF, classG, classK, classM}
	// classByZoom        = [11]int{7, 7, 7, 7, 7, 7, 6, 5, 4, 3, 2}
)

func getStarDetails(classDetails classDetails, sector sector, random1m *rand.Rand) []*star {
	stars := make([]*star, 0)
	loopSize := int32(800 * (classDetails.odds - classDetails.fudge + 2*classDetails.fudge*random1m.Float32()))
	for i := 0; i < int(loopSize); i++ {
		nextStar := star{}
		nextStar.id = len(stars)
		random1 := random1m.Float32()
		nextStar.sx = random1m.Float32()
		nextStar.sy = random1m.Float32()
		nextStar.sz = random1m.Float32()
		nextStar.x = float32(sector.x) + nextStar.sx
		nextStar.y = float32(sector.y) + nextStar.sy
		nextStar.z = float32(sector.z) + nextStar.sz
		nextStar.class = classDetails.class
		nextStar.brightColor = classDetails.brightColor
		nextStar.dimColor = classDetails.dimColor
		nextStar.mass = classDetails.minMass + classDetails.deltaMass*(1+random1)
		nextStar.radii = (classDetails.minRadii + random1*classDetails.deltaRadii) / 2
		nextStar.luminance = classDetails.minLum + random1*classDetails.deltaLum
		nextStar.pixels = classDetails.pixels
		stars = append(stars, &nextStar)
	}

	return stars
}

func getSectorDetails(fromSector sector) (result []*star) {
	result = make([]*star, 0)
	random1m := getHash(fromSector)
	classCount := 0
	for _, starDetails := range starDetailsByClass {
		nextClass := getStarDetails(starDetails, fromSector, random1m)
		result = append(result, nextClass...)
		classCount++
		// if classCount > classByZoom[zoomIndex] {
		//  	break
		//}
	}

	return result
}

func getHash(aSector sector) *rand.Rand {
	id := murmur3.New64()
	buf := make([]byte, 4)
	binary.LittleEndian.PutUint32(buf, aSector.x)
	_, err := id.Write(buf)
	if err != nil {
		print("Failed to hash part 1")
	}

	binary.LittleEndian.PutUint32(buf, aSector.y)
	_, err = id.Write(buf)
	if err != nil {
		print("Failed to hash part two")
	}

	binary.LittleEndian.PutUint32(buf, aSector.z)
	_, err = id.Write(buf)
	if err != nil {
		print("Failed to hash part 3")
	}

	return rand.New(rand.NewSource(int64(id.Sum64())))
}

func distance(s1 *star, s2 *star) float32 {
	return math32.Sqrt((s1.x-s2.x)*(s1.x-s2.x) + (s1.y-s2.y)*(s1.y-s2.y) + (s1.z-s2.z)*(s1.z-s2.z))
}

var (
	stars []*star
	lines []*simpleLine

	sName       = "sphere"
	sphereModel *gi3d.Sphere

	rendered      = false
	connectedStar int
	highWater     int
)

func renderStars(sc *gi3d.Scene) {
	if !rendered {
		stars = make([]*star, 0)
		id := 0
		for x := uint32(0); x < 2; x++ {
			for y := uint32(0); y < 2; y++ {
				for z := uint32(0); z < 2; z++ {
					sector := sector{x: x, y: y, z: z}
					for _, star := range getSectorDetails(sector) {
						star.id = id
						id++
						stars = append(stars, star)
					}
				}
			}
		}
		if len(stars) > 0 {
			sphereModel = &gi3d.Sphere{}
			sphereModel.Reset()
			sphereModel = gi3d.AddNewSphere(sc, sName, 0.002, 24)
			lines = make([]*simpleLine, 0)
			sName = "sphere"
			for _, star := range stars {
				starSphere := gi3d.AddNewSolid(sc, sc, sName, sphereModel.Name())
				starSphere.Pose.Pos.Set(star.x+offsets.x, star.y+offsets.y, star.z+offsets.z)
				starSphere.Mat.Color.SetUInt8(star.brightColor.R, star.brightColor.G, star.brightColor.B, star.brightColor.A)
			}
			for id, star := range stars {
				for _, jump := range checkForJumps(stars, star, id) {
					lines = append(lines, jump)
					if jump.jumpInfo.distance < 3.0 {
						jumpsByStar[star.id] = append(jumpsByStar[star.id], jump.jumpInfo)
						if star.id == jump.jumpInfo.s2ID {
							jumpsByStar[jump.jumpInfo.s1ID] = append(jumpsByStar[jump.jumpInfo.s1ID], jump.jumpInfo)
						} else {
							jumpsByStar[jump.jumpInfo.s2ID] = append(jumpsByStar[jump.jumpInfo.s2ID], jump.jumpInfo)
						}
					}
				}
			}

			if !fastest {
				rendered = true
				highWater = -1
				for lNumber := 0; lNumber < len(stars); lNumber++ {
					tJumps := traceJumps(lNumber)
					if len(tJumps) > highWater {
						highWater = len(tJumps)
						connectedStar = lNumber
					}
				}
				f, err := os.Create("traveler-report.csv")

				if err == nil {
					alreadyPrinted := make([]int, 0)
					_, err := f.Write([]byte(csvTextHdr))
					if err != nil {
						os.Exit(-1)
					}
					for _, nextJump := range traceJumps(connectedStar) {
						if !contains(alreadyPrinted, nextJump.s1ID) && nextJump.s1ID > -1 {
							_, err := f.Write([]byte(worldFromStar(nextJump.s1ID).worldCSV))
							if err != nil {
								os.Exit(-1)
							}
							alreadyPrinted = append(alreadyPrinted, nextJump.s1ID)
						}
						if !contains(alreadyPrinted, nextJump.s2ID) && nextJump.s2ID > -1  {
							_, err := f.Write([]byte(worldFromStar(nextJump.s2ID).worldCSV))
							if err != nil {
								os.Exit(-1)
							}
							alreadyPrinted = append(alreadyPrinted, nextJump.s2ID)
						}

					}
				}

				if !faster {
					popMax := 0
					bigWorld := worldFromStar(stars[0].id)
					bigStar := *stars[0]
					techMax := 0
					techWorld := worldFromStar(stars[0].id)
					techStar := *stars[0]

					for _, star := range stars {
						world := worldFromStar(star.id)
						if world.techLevelBase > techMax {
							techMax = world.techLevelBase
							techWorld = world
							techStar = *star
						}
						if world.popBase > popMax {
							popMax = world.popBase
							bigWorld = world
							bigStar = *star
						}
					}

					if techMax > popMax {
						techMax += 1
					}
					if bigWorld.popBase > popMax {
						techMax += 1
					}
					if bigStar.pixels > 0 {
						techMax += 1
					}
					if techWorld.popBase > popMax {
						techMax += 1
					}
					if techStar.pixels > 0 {
						techMax += 1
					}
				}
			}
			// fastest case
			for id, lin := range lines {
				thickness := float32(0.00010)
				if lin.jumpInfo.color.A < math.MaxUint8-47 {
					thickness = 0.00012
				} else if lin.jumpInfo.color.A < math.MaxUint8-39 {
					thickness = 0.00015
				}
				if lin.jumpInfo.s1ID != lin.jumpInfo.s2ID {
					lin.lines = gi3d.AddNewLines(sc, "Lines-"+strconv.Itoa(lin.jumpInfo.s1ID)+"-"+strconv.Itoa(lin.jumpInfo.s2ID),
						[]mat32.Vec3{
							{X: lin.from.x + offsets.x, Y: lin.from.y + offsets.y, Z: lin.from.z + offsets.z},
							{X: lin.to.x + offsets.x, Y: lin.to.y + offsets.y, Z: lin.to.z + offsets.z},
						},
						mat32.Vec2{X: thickness, Y: thickness},
						gi3d.OpenLines,
					)
					solidLine := gi3d.AddNewSolid(sc, sc, "Lines-"+strconv.Itoa(id), lin.lines.Name())
					// solidLine.Pose.Pos.Set(lin.from.x - .5, lin.from.y - .5, lin.from.z + 8)
					// lns.Mat.Color.SetUInt8(255, 255, 0, 128)
					solidLine.Mat.Color = lin.jumpInfo.color
				}
			}
		}
	}
}

func checkForJumps(stars []*star, star *star, id int) (result []*simpleLine) {
	result = make([]*simpleLine, 0)
	for innerId, innerStar := range stars {
		if innerId == id {
			continue
		}
		jumpColor := checkFor1jump(star, innerStar)
		if jumpColor.color.A > 0 {
			// symmetric, so no copies
			result = addIfNew(result, jumpColor)
		}
	}
	closest := []*simpleLine{&noLine, &noLine, &noLine}
	if len(result) > 3 {
		for _, nextSimpleLine := range result {
			if nextSimpleLine.jumpInfo.distance < closest[0].jumpInfo.distance {
				closest[2] = closest[1]
				closest[1] = closest[0]
				closest[0] = nextSimpleLine
			} else if nextSimpleLine.jumpInfo.distance < closest[1].jumpInfo.distance {
				closest[2] = closest[1]
				closest[1] = nextSimpleLine
			} else if nextSimpleLine.jumpInfo.distance < closest[2].jumpInfo.distance {
				closest[2] = nextSimpleLine
			}
		}
		result = closest
	}

	return
}

func checkFor1jump(s1 *star, s2 *star) (result *jump) {
	jumpLength := distance(s1, s2) * 100 * parsecsPerLightYear
	delta := int(jumpLength)
	if delta < len(jumpColors) {
		if s1.id != s2.id {
			result = &jump{jumpColors[delta], jumpColors[delta], delta, jumpLength, s1.id, s2.id}
		} else {
			result = &noJump
		}
	} else {
		result = &noJump
	}
	// Return transparent black if there isn't one

	return
}

func addVisits(base []*jump, addition []*jump) (result []*jump) {
	result = base
	for _, nextJump := range addition {
		already := false
		for _, baseJump := range base {
			if (baseJump.s1ID == nextJump.s1ID &&
				baseJump.s2ID == nextJump.s2ID) ||
				(baseJump.s1ID == nextJump.s2ID &&
					baseJump.s2ID == nextJump.s1ID) {
				already = true
				break
			}
		}
		if !already {
			result = append(result, nextJump)
		}
	}

	return
}

func subtractVisits(base []*jump, subtraction []*jump) (result []*jump) {
	result = make([]*jump, 0)
	for _, baseJump := range base {
		if baseJump.s1ID != baseJump.s2ID {
			add := true
			for _, nextJump := range subtraction {
				if nextJump.s1ID != nextJump.s2ID {
					if (baseJump.s1ID == nextJump.s1ID &&
						baseJump.s2ID == nextJump.s2ID) ||
						(baseJump.s1ID == nextJump.s2ID &&
							baseJump.s2ID == nextJump.s1ID) {
						add = false
						break
					}
				} else {
					continue
				}
			}
			if add {
				result = append(result, baseJump)
			}
		}
	}
	return
}

func nextVisits(base []*jump, visited []*jump) (result []*jump) {
	result = make([]*jump, 0)
	for _, start := range base {
		for _, nextJump := range jumpsByStar[start.s1ID] {
			result, _ = maybeAppend(result, nextJump)
		}
		for _, nextJump := range jumpsByStar[start.s2ID] {
			result, _ = maybeAppend(result, nextJump)
		}
	}
	result = subtractVisits(result, visited)
	return
}

func maybeAppend(soFar []*jump, nextJump *jump) (result []*jump, yesAppend bool) {
	yesAppend = true
	result = soFar
	for _, alreadyJumps := range soFar {
		if (nextJump.s1ID == alreadyJumps.s1ID && nextJump.s2ID == alreadyJumps.s2ID) ||
			(nextJump.s2ID == alreadyJumps.s1ID && nextJump.s1ID == alreadyJumps.s2ID) {
			yesAppend = false
			break
		}
	}
	if yesAppend {
		result = append(result, nextJump)
	}
	return
}

func traceJumps(id int) (visited []*jump) {
	explore := jumpsByStar[id]
	visited = explore
	for longest := 0; longest < 48; longest++ {
		if len(explore) == 0 {
			break
		}
		explore = nextVisits(explore, visited)
		visited = addVisits(visited, explore)
	}
	return visited
}

func showStar(star star, sc *gi3d.Scene) {
	starSphere := gi3d.AddNewSolid(sc, sc, sName, sphereModel.Name())
	starSphere.Pose.Pos.Set(star.x+offsets.x, star.y+offsets.y, star.z+offsets.z)
	starSphere.Mat.Color.SetUInt8(star.brightColor.R, star.brightColor.G, star.brightColor.B, star.brightColor.A)
}

func showBigStar(star star, sc *gi3d.Scene) {
	starSphere := gi3d.AddNewSolid(sc, sc, sName, sphereModel.Name())
	starSphere.Pose.Pos.Set(star.x+offsets.x, star.y+offsets.y, star.z+offsets.z)
	starSphere.Mat.Color.SetUInt8(star.brightColor.R, star.brightColor.G, star.brightColor.B, star.brightColor.A)
}

func addIfNew(soFar []*simpleLine, jump *jump) (result []*simpleLine) {
	result = soFar
	if jump.s1ID >= jump.s2ID {
		return
	}
	already := false
	for _, line := range result {
		if (line.jumpInfo.s1ID == jump.s1ID &&
			line.jumpInfo.s2ID == jump.s2ID) ||
			(line.jumpInfo.s1ID == jump.s2ID &&
				line.jumpInfo.s2ID == jump.s1ID) {
			already = true
			break
		}
	}
	if !already {
		nextLine := &simpleLine{
			from:     position{x: stars[jump.s1ID].x, y: stars[jump.s1ID].y, z: stars[jump.s1ID].z},
			to:       position{x: stars[jump.s2ID].x, y: stars[jump.s2ID].y, z: stars[jump.s2ID].z},
			jumpInfo: jump,
		}
		result = append(result, nextLine)
	}
	return
}

func contains(soFar []int, next int) (yes bool) {
	yes = false
	for _, sID := range soFar {
		if sID == next {
			yes = true
			break
		}
	}
	return
}