package coords

import (
	"strconv"
	"strings"
)

type Point struct {
	X        int
	Y        int
	RectArea int
}

// Turn input data into useable structured point objects.
func ParsePoints(data []string) []*Point {
	points := []*Point{}

	for _, pointInfo := range data {
		components := strings.Split(pointInfo, ", ")
		x, _ := strconv.Atoi(components[0])
		y, _ := strconv.Atoi(components[1])
		points = append(points, &Point{
			X: x,
			Y: y,
		})
	}

	return points
}

// Mutates the passed-in data in-place, computing the rectilinear
// area for all points within the space bounded by (0,0) and the points themselves.
// Sets the area of the 4 corner-most points (which definitionally must have
// unbounded areas) to -1
func ComputeRectilinearAreas(data []*Point) {
	// first we find the bounds
	maxX, maxY := findUpperBounds(data)

	// now for each integer point in our range, we compute the distance to each
	// point. We save the closest point and its distance, then:
	// 1. If there are two or more identically closest points, we do nothing.
	// 2. Otherwise, we increment RectArea for the closest point.
	for x := -maxX; x <= maxX*2; x++ {
		for y := -maxY; y <= maxY*2; y++ {
			closest := findClosest(data, &Point{X: x, Y: y})
			if closest == nil {
				continue
			}

			closest.RectArea++
		}
	}

	// now we find any unbounded points and set their areas to the sentinel
	// value
	detectUnboundedPoints(data)
}

// ComputeNearnessRegion takes a list of points, returns the number of points
// whose rectilinear distance to *all* the listed points sums to less than the
// target value.
func ComputeNearnessRegion(points []*Point, target int) int {
	maxX, maxY := findUpperBounds(points)

	// this is a 'brute-force' solution that just picks a suitably wide
	// area to search.
	area := 0
	for x := -target; x < maxX+target; x++ {
		for y := -target; y < maxY+target; y++ {
			if sumDistances(points, &Point{X: x, Y: y}) < target {
				area++
			}
		}
	}

	return area
}

// sumDistances calculates the distance from each point in `points` to `target`, and
// returns the sum of all of those distances
func sumDistances(points []*Point, target *Point) int {
	area := 0
	for _, point := range points {
		area += Distance(point, target)
	}
	return area
}

// Find the nearest point in `data` to `point`, using rectilinear distance.
func findClosest(data []*Point, point *Point) *Point {
	closest := data[0]
	distance := Distance(data[0], point)

	for i := 1; i < len(data); i++ {
		candidate := data[i]
		newDistance := Distance(candidate, point)

		if newDistance < distance {
			closest = candidate
			distance = newDistance
		} else if newDistance == distance {
			closest = nil
		}
	}

	return closest
}

func detectUnboundedPoints(data []*Point) {
	for _, candidate := range data {
		// look in each direction
		up := findClosest(data, &Point{X: candidate.X, Y: candidate.Y + 1000000})
		down := findClosest(data, &Point{X: candidate.X, Y: candidate.Y - 1000000})
		left := findClosest(data, &Point{X: candidate.X + 1000000, Y: candidate.Y})
		right := findClosest(data, &Point{X: candidate.X - 1000000, Y: candidate.Y})

		// if any of those points are closest to our point, we're unbounded
		if up == candidate || down == candidate ||
			left == candidate || right == candidate {
			candidate.RectArea = -1
		}
	}
}

func findUpperBounds(points []*Point) (int, int) {
	maxX := 0
	maxY := 0
	for _, point := range points {
		if point.X > maxX {
			maxX = point.X
		}
		if point.Y > maxY {
			maxY = point.Y
		}
	}

	return maxX, maxY
}

func Distance(p1 *Point, p2 *Point) int {
	return abs(p2.Y-p1.Y) + abs(p2.X-p1.X)
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}