package precomputed_shade

import (
	"bytes"
	"errors"
	"lab.zaar.be/thefish/alchemyst-go/engine/fov/basic"
	"lab.zaar.be/thefish/alchemyst-go/engine/gamemap"
	"lab.zaar.be/thefish/alchemyst-go/engine/types"
	"math"
	"sort"
)

//Реализация алгоритма от chilly_durango
//https://www.reddit.com/r/roguelikedev/comments/5n1tx3/fov_algorithm_sharencompare/
//Пока не побеждена засветка стен (или это у меня руки кривые) - сложность почти O(N^2)

//Не только у меня:
//chilly_durango:
//I turned down the Radio 4 in my car to think about this on the way home - that's how much this question has been
// bugging me. The two best from all the awful compromises I could consider were:
// - Only lighting walls with light from the player (cheap trick, dutch)
// - Match light value for walls with the highest light value in adjacent floor cell visible to player (seems costly)

/*
Why am I here? Well, I just don't know what to call it - I'm sure it's an established method, and I'm aware there are
probably optimisations to be had. I thought if I roughed out the algorithm here, the r/roguelikedev community would
surely be able to help! I haven't included optimisations here, but if anyone wants them I got 'em :)

Method

Beforehand

- List the cells in your largest-possible FOV, storing X and Y values relative to the center.
- Store the distance from the center for each cell, and sort the list by this in ascending order.
- Store the range of angles occludedAngles by each cell in this list, in clockwise order as absolute integers only.
- Create a 360-char string of 0s called EmptyShade, and a 360-char string of 1s called FullShade

Runtime

- Store two strings – CurrentShade and NextShade
- Set CurrentShade to EmptyShade to start.
- While CurrentShade =/= FullShade: step through the Cell List:

	- If the distance to the current cell is not equal to the previous distance checked then replace the contents
      of the CurrentShade variable with the contents of the NextShade variable.

	- If the tested cell is opaque – for each angle in the range occludedAngles by the cell, place a 1 at the position
      determined by angle%360 in the NextShade string.

    - For each angle in the range occludedAngles by the cell, add 1 to the lit value for that cell for each 0
      encountered at the position determined by angle%360 in the CurrentShade string.

Notes
Benefits

No messing around with octants

Highly efficient - each cell is only visited once, and checks within that cell are rare. It performs as fast as any
other LOS I've tried but with more options

Human-readable - code and output are highly legible, making it very easy to work with

Flexible - I'm using it for FOV, LOS, renderer, and lighting. Each process is calling the same function, within which
flags control how much data is evaluated and output. It only uses the data it needs to in the context where
its needed – so monsters that need a list of things they can see only check if a cell is visible or not, and don’t
bother calculating how much visibility they have there. This cuts processing dramatically.

Other links

cfov by Konstantin Stupnik on RogueTemple

Pre-Computed Visiblity Trees on RogueBasin

/r/roguelikedev FAQ Friday on FOV which kicked off this train of thought

/u/pnjeffries on his FOV algorithm which inspired this one

Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed
*/

const MIN_LIT_TO_BE_VISIBLE = 3
const MIN_WALL_LIT_TO_BE_VISIBLE = 4

var errNotFoundCell = errors.New("Cell not found")
var errOutOfBounds = errors.New("Cell out of bounds")

type Cell struct {
	types.Coords
	distance       float64
	occludedAngles []int //angles occluded by this cell
	lit            int   //light "amount"
}

type CellList []*Cell

type DistanceSorter CellList

func (a DistanceSorter) Len() int           { return len(a) }
func (a DistanceSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a DistanceSorter) Less(i, j int) bool { return a[i].distance < a[j].distance }

type precomputedShade struct {
	originCoords   types.Coords
	MaxTorchRadius int
	CellList       CellList
	LightWalls     bool
}

func NewPrecomputedShade(maxTorchRadius int) *precomputedShade {
	result := &precomputedShade{MaxTorchRadius: maxTorchRadius, LightWalls: true}
	result.PrecomputeFovMap()
	return result
}

func (ps *precomputedShade) FindByCoords(c types.Coords) (int, *Cell, error) {
	for i := range ps.CellList {
		if ps.CellList[i].Coords == c {
			// Found!
			return i, ps.CellList[i], nil
		}
	}
	return 0, &Cell{}, errNotFoundCell
}

func (ps *precomputedShade) IsInFov(coords types.Coords) bool {
	rc := ps.fromLevelCoords(coords)
	if rc.X == 0 && rc.Y == 0 {
		return true
	}
	_, cell, err := ps.FindByCoords(rc)
	if err != nil {
		return false
	}
	return cell.lit > 0
}

func (ps *precomputedShade) SetLightWalls(value bool) {
	ps.LightWalls = value
}

func (ps *precomputedShade) Init() {
	ps.PrecomputeFovMap()
}

func (ps *precomputedShade) PrecomputeFovMap() {
	max := ps.MaxTorchRadius
	minusMax := (-1) * max
	zeroCoords := types.Coords{X: 0, Y: 0}
	var x, y int
	//fill list
	for x = minusMax; x < max+1; x++ {
		for y = minusMax; y < max+1; y++ {
			if x == 0 && y == 0 {
				continue
			}
			iterCoords := types.Coords{X: x, Y: y}
			distance := zeroCoords.DistanceTo(iterCoords)
			if distance <= float64(max) {
				ps.CellList = append(ps.CellList, &Cell{iterCoords, distance, nil, 0})
			}
		}
	}
	//Do not change cell order after this!
	sort.Sort(DistanceSorter(ps.CellList))
	//debug
	//for _, cell := range ps.CellList {
	//	fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
	//}

	//Bresanham lines / Raycast
	var lineX, lineY float64
	for i := 0; i < 720; i++ { // 1/2 of angles
		dx := math.Sin(float64(i) / (float64(360) / math.Pi)) //1/2 of angle
		dy := math.Cos(float64(i) / (float64(360) / math.Pi))

		lineX = 0
		lineY = 0
		for j := 0; j < max; j++ {
			lineX -= dx
			lineY -= dy

			roundedX := int(basic.Round(lineX))
			roundedY := int(basic.Round(lineY))

			_, cell, err := ps.FindByCoords(types.Coords{X: roundedX, Y: roundedY})

			if err != nil {
				//inexistent coord found
				break
			}
			cell.occludedAngles = unique(append(cell.occludedAngles, i))
		}

	}

	//for _, cell := range ps.CellList {
	//	fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occludedAngles))
	//}
}

func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords, radius int) {
	for i, _ := range ps.CellList {
		ps.CellList[i].lit = 0
	}
	ps.originCoords = initCoords

	if radius > ps.MaxTorchRadius {
		radius = ps.MaxTorchRadius
	}

	level.GetTile(initCoords).Visible = true

	var fullShade = make([]byte, 720) // 1/2 of angles
	for i := range fullShade {
		fullShade[i] = 1
	}
	var emptyShade = make([]byte, 720) // 1/2 of angles
	currentShade := emptyShade
	nextShade := emptyShade

	i := 0
	prevDistance := 0.0
	for !bytes.Equal(currentShade, fullShade) {
		if i == len(ps.CellList)-1 {
			break
		}
		cell := ps.CellList[i]
		i++
		if cell.distance != prevDistance {
			currentShade = nextShade
		}

		if cell.distance > float64(radius) {
			break
		}

		lc, err := ps.toLevelCoords(level, initCoords, cell.Coords)
		if err != nil {
			continue
		}

		//fmt.Printf("\n level coords: %v", lc)
		for _, angle := range cell.occludedAngles {

			if level.GetTile(lc).BlocksSight && ps.LightWalls {
				//if (nextShade[angle] == 0 && currentShade[angle] == 0) {
				if nextShade[angle] == 0 {
					level.GetTile(lc).Visible = true
					level.GetTile(lc).Explored = true
				}
				nextShade[angle] = 1
			}

			if currentShade[angle] == 0 {
				cell.lit = cell.lit + 1
			}

		}
	}
}

func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Coords, radius int) {

	level.SetAllInvisible()
	ps.recalc(level, initCoords, radius)

	for _, cell := range ps.CellList {
		//fmt.Printf("\n coords: %v, distance: %f, lit: %d", cell.Coords, cell.distance, cell.lit)
		cs, err := ps.toLevelCoords(level, initCoords, cell.Coords)
		if cell.lit > 0 && cell.lit > MIN_LIT_TO_BE_VISIBLE {
			//if cell.lit > 0 && cell.lit / (ps.MaxTorchRadius - int(cell.distance - 0.4) - 1) > MIN_LIT_TO_BE_VISIBLE {
			if err != nil {
				continue
			}
			level.GetTile(cs).Visible = true
			level.GetTile(cs).Explored = true
		}

		//light walls, crutch
		//if level.GetTile(cs).BlocksSight && ps.LightWalls {
		//	if cell.IsAdjacentTo(&types.Coords{0,0}) {
		//		level.GetTile(cs).Visible = true
		//	} else {
		//		maybeLit := false
		//		for _, maybeNb := range ps.CellList {
		//			if  //int(maybeNb.distance) == int(cell.distance-1) &&
		//				maybeNb.IsAdjacentTo(&cell.Coords) &&
		//				(maybeNb.X == cell.X || maybeNb.Y == cell.Y) &&
		//				maybeNb.lit > MIN_WALL_LIT_TO_BE_VISIBLE { //magic constant!
		//				maybeLit = true
		//			}
		//		}
		//		if maybeLit {
		//			level.GetTile(cs).Visible = true
		//			level.GetTile(cs).Explored = true
		//		}
		//	}
		//}
	}
}

func (ps *precomputedShade) toLevelCoords(level *gamemap.Level, initCoords, relativeCoords types.Coords) (types.Coords, error) {
	realCoords := types.Coords{X: initCoords.X + relativeCoords.X, Y: initCoords.Y + relativeCoords.Y}
	if !level.InBounds(realCoords) {
		return types.Coords{}, errOutOfBounds
	}
	return realCoords, nil
}

func (ps *precomputedShade) fromLevelCoords(lc types.Coords) types.Coords {
	relativeCoords := types.Coords{X: lc.X - ps.originCoords.X, Y: lc.Y - ps.originCoords.Y}
	return relativeCoords
}

func unique(intSlice []int) []int {
	keys := make(map[int]bool)
	list := []int{}
	for _, entry := range intSlice {
		if _, value := keys[entry]; !value {
			keys[entry] = true
			list = append(list, entry)
		}
	}
	return list
}