package aoc

import (
	"errors"
	"fmt"
)

type CoordByteMap struct {
	Field  map[Coordinate]byte
	Height int
	Width  int

	// The Top-Left-most X/Y
	TLX, TLY int
	// The Bottom-Right-most X/Y
	BRX, BRY int
}

func NewCoordByteMap() CoordByteMap {
	return CoordByteMap{
		Field: make(map[Coordinate]byte),
	}
}

func StringSliceToCoordByteMap(input []string) CoordByteMap {
	ret := CoordByteMap{
		Field:  make(map[Coordinate]byte),
		Height: len(input),
	}
	for y := range input {
		for x := range input[y] {
			ret.Put(Coordinate{X: x, Y: y}, input[y][x])
		}
	}
	return ret
}

// FindFirst searches left to right, top to bottom for the first
// instance of b.
func (m *CoordByteMap) FindFirst(b byte) (Coordinate, error) {
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			c := Coordinate{X: x, Y: y}
			if m.Get(c) == b {
				return c, nil
			}
		}
	}
	return Coordinate{}, errors.New("Not Found")
}

func (m *CoordByteMap) FindAll(b ...byte) []Coordinate {
	var ret []Coordinate
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			c := Coordinate{X: x, Y: y}
			for i := range b {
				if m.Get(c) == b[i] {
					ret = append(ret, c)
				}
			}
		}
	}
	return ret
}

func (m *CoordByteMap) FindAllNot(b ...byte) map[Coordinate]byte {
	ret := make(map[Coordinate]byte)
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			c := Coordinate{X: x, Y: y}
			v := m.Get(c)
			var is bool
			for i := range b {
				if v == b[i] {
					is = true
					break
				}
			}
			if !is {
				ret[c] = v
			}
		}
	}
	return ret
}

func (m *CoordByteMap) Count(b byte) int {
	var ret int
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			if m.Get(Coordinate{X: x, Y: y}) == b {
				ret++
			}
		}
	}
	return ret
}

func (m *CoordByteMap) Get(pos Coordinate) byte {
	if v, ok := m.Field[pos]; ok {
		return v
	}
	return 0
}

func (m *CoordByteMap) Opt(pos Coordinate, def byte) byte {
	if v, ok := m.Field[pos]; ok {
		return v
	}
	return def
}

func (m *CoordByteMap) Put(pos Coordinate, val byte) {
	if pos.X < m.TLX {
		m.TLX = pos.X
	}
	if pos.Y < m.TLY {
		m.TLY = pos.Y
	}
	if pos.X > m.BRX {
		m.BRX = pos.X
	}
	if pos.Y > m.BRY {
		m.BRY = pos.Y
	}
	m.Field[pos] = val
	if pos.X+1 > m.Width {
		m.Width = m.BRX - m.TLX + 1
	}
	if pos.Y+1 > m.Height {
		m.Height = m.BRY - m.TLY + 1
	}
}

func (m *CoordByteMap) GrowNorth(size int, val byte) {
	tlY := m.TLY - 1
	for x := m.TLX; x <= m.BRX; x++ {
		m.Put(Coordinate{X: x, Y: tlY}, val)
	}
}
func (m *CoordByteMap) GrowEast(size int, val byte) {
	brX := m.BRX + 1
	for y := m.TLY; y <= m.BRY; y++ {
		m.Put(Coordinate{X: brX, Y: y}, val)
	}
}
func (m *CoordByteMap) GrowSouth(size int, val byte) {
	tlY := m.BRY + 1
	for x := m.TLX; x <= m.BRX; x++ {
		m.Put(Coordinate{X: x, Y: tlY}, val)
	}
}
func (m *CoordByteMap) GrowWest(size int, val byte) {
	brX := m.TLX - 1
	for y := m.TLY; y <= m.BRY; y++ {
		m.Put(Coordinate{X: brX, Y: y}, val)
	}
}

// This grows the map in all directions
func (m *CoordByteMap) Grow(size int, val byte) {
	m.GrowNorth(size, val)
	m.GrowEast(size, val)
	m.GrowSouth(size, val)
	m.GrowWest(size, val)
}

// Runs the passed function on every point in the map
// returning a new bytemap
func (m CoordByteMap) Map(l func(c Coordinate, in byte) byte) CoordByteMap {
	i := m
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			c := Coordinate{X: x, Y: y}
			nV := l(c, m.Get(c))
			fmt.Println("MAP: Putting", string(nV), "at", c)
			i.Put(c, l(c, m.Get(c)))
		}
	}
	return i
}

func (m CoordByteMap) String() string {
	var ret string
	for y := m.TLY; y <= m.BRY; y++ {
		for x := m.TLX; x <= m.BRX; x++ {
			ret = ret + string(m.Field[Coordinate{X: x, Y: y}])
		}
		ret = ret + "\n"
	}
	return ret
}

// And the 3d Version
type Coord3dByteMap struct {
	Field      map[Coordinate3d]byte
	MinX, MaxX int
	MinY, MaxY int
	MinZ, MaxZ int
}

func StringSliceToCoord3dByteMap(input []string) Coord3dByteMap {
	ret := Coord3dByteMap{
		Field: make(map[Coordinate3d]byte),
	}
	for y := range input {
		for x := range input[y] {
			ret.Put(Coordinate3d{X: x, Y: y, Z: 0}, input[y][x])
		}
	}
	return ret
}

func (m *Coord3dByteMap) Copy() Coord3dByteMap {
	ret := Coord3dByteMap{
		Field: make(map[Coordinate3d]byte),
		MinX:  m.MinX, MaxX: m.MaxX,
		MinY: m.MinY, MaxY: m.MaxY,
		MinZ: m.MinZ, MaxZ: m.MaxZ,
	}
	for z := m.MinZ; z <= m.MaxZ; z++ {
		for y := m.MinY; y <= m.MaxY; y++ {
			for x := m.MinX; x <= m.MaxX; x++ {
				c := Coordinate3d{X: x, Y: y, Z: z}
				ret.Field[c] = m.Get(c)
			}
		}
	}
	return ret
}

func (m *Coord3dByteMap) Get(pos Coordinate3d) byte {
	if _, ok := m.Field[pos]; !ok {
		return 0
	}
	return m.Field[pos]
}
func (m *Coord3dByteMap) Opt(pos Coordinate3d, def byte) byte {
	if v, ok := m.Field[pos]; ok {
		return v
	}
	return def
}

func (m *Coord3dByteMap) Put(pos Coordinate3d, val byte) {
	m.Field[pos] = val
	if pos.X < m.MinX {
		m.MinX = pos.X
	}
	if pos.X > m.MaxX {
		m.MaxX = pos.X
	}
	if pos.Y < m.MinY {
		m.MinY = pos.X
	}
	if pos.Y > m.MaxY {
		m.MaxY = pos.Y
	}
	if pos.Z < m.MinZ {
		m.MinZ = pos.Z
	}
	if pos.Z > m.MaxZ {
		m.MaxZ = pos.Z
	}
}

func (m *Coord3dByteMap) CountNeighbors(pos Coordinate3d, val byte) int {
	var res int
	for z := pos.Z - 1; z <= pos.Z+1; z++ {
		for y := pos.Y - 1; y <= pos.Y+1; y++ {
			for x := pos.X - 1; x <= pos.X+1; x++ {
				if m.Get(Coordinate3d{X: x, Y: y, Z: z}) == val {
					res++
				}
			}
		}
	}
	return res
}

func (m Coord3dByteMap) Count(val byte) int {
	var ret int
	for z := m.MinZ; z <= m.MaxZ; z++ {
		for y := m.MinY; y <= m.MaxY; y++ {
			for x := m.MinX; x <= m.MaxX; x++ {
				if m.Get(Coordinate3d{X: x, Y: y, Z: z}) == val {
					ret++
				}
			}
		}
	}
	return ret
}

func (m Coord3dByteMap) String() string {
	var ret string
	for z := m.MinZ; z <= m.MaxZ; z++ {
		for y := m.MinY; y <= m.MaxY; y++ {
			for x := m.MinX; x <= m.MaxX; x++ {
				ret = ret + string(m.Field[Coordinate3d{X: x, Y: y, Z: z}])
			}
			ret = ret + "\n"
		}
		ret = ret + "\n\n"
	}
	return ret
}
