package precomputed_permissive

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

/*
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 occluded 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 occluded by the cell, place a 1 at the position determined by angle%360 in the NextShade string.

For each angle in the range occluded by the cell, add 1 to the shade 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
*/

var NotFoundCell = errors.New("Cell not found")

type Cell struct {
	types.Coords
	distance float64
	occluded []int //indexes of cells in CellList
}

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 }

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

type precomputedPermissive struct {
	MaxTorchRadius int
	CellList       CellList

	cosTable map[int]float64
	sinTable map[int]float64
}

func NewPrecomputedPermissive(maxTorchRadius int) *precomputedPermissive {
	result := &precomputedPermissive{MaxTorchRadius: maxTorchRadius}
	result.PrecomputeFovMap()
	return result
}

func (pp *precomputedPermissive) IsInFov(coords types.Coords) bool {
	return true
}

func (pp *precomputedPermissive) ComputeFov(coords types.Coords, radius int) {

}

func (pp *precomputedPermissive) PrecomputeFovMap() {
	max := pp.MaxTorchRadius
	minusMax := (-1) * max
	zeroCoords := types.Coords{0, 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, y}
			distance := zeroCoords.DistanceTo(iterCoords)
			if distance <= float64(max) {
				pp.CellList = append(pp.CellList, &Cell{iterCoords, distance, nil})
			}
		}
	}
	//Do not change cell order after this!
	sort.Sort(DistanceSorter(pp.CellList))
	//debug
	//for _, cell := range pp.CellList {
	//	fmt.Printf("\n coords: %v, distance: %f, len_occl: %d", cell.Coords, cell.distance, len(cell.occluded))
	//}

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

		occlusion := make([]int, max)
		traversedCells := make([]*Cell, max)
		lineX = 0
		lineY = 0
		for j := 0; j < max; j++ {
			lineX -= dx
			lineY -= dy

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

			idx, cell, err := pp.FindByCoords(types.Coords{roundedX, roundedY})
			if err != nil {
				//inexistent coord found
				break;
			}
			occlusion[j] = idx
			traversedCells[j] = cell
		}
		// -2 because we do not want to cell occlude itself
		for k := len(occlusion) - 2; k >= 0; k-- {
			if traversedCells[k] == nil {
				continue;
			}
			if traversedCells[k].occluded == nil {
				traversedCells[k].occluded = occlusion[k + 1:]
			}
			//Remove duplicates
			traversedCells[k].occluded = unique(append(traversedCells[k].occluded, occlusion[k + 1:]...))
		}
		fmt.Printf("\n next: %d", i)
	}

	fmt.Printf("before len: %d", len(pp.CellList))
	fmt.Printf("after len: %d", len(pp.CellList))

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


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
}