package ds1

const (
	maxWallLayers         = 4
	maxFloorLayers        = 2
	maxShadowLayers       = 1
	maxSubstitutionLayers = 1
)

// LayerGroupType represents a type of layer (floor, wall, shadow, etc)
type LayerGroupType int

// Layer group types
const (
	FloorLayerGroup LayerGroupType = iota
	WallLayerGroup
	ShadowLayerGroup
	SubstitutionLayerGroup
)

func (l LayerGroupType) String() string {
	switch l {
	case FloorLayerGroup:
		return "floor"
	case WallLayerGroup:
		return "wall"
	case ShadowLayerGroup:
		return "shadow"
	case SubstitutionLayerGroup:
		return "substitution"
	}

	// should not be reached
	return "unknown"
}

type layerGroup []*Layer

type ds1Layers struct {
	width, height int
	Floors        layerGroup
	Walls         layerGroup
	Shadows       layerGroup
	Substitutions layerGroup
}

func (l *ds1Layers) ensureInit() {
	if l.Floors == nil {
		l.Floors = make(layerGroup, 0)
	}

	if l.Walls == nil {
		l.Walls = make(layerGroup, 0)
	}

	if l.Shadows == nil {
		l.Shadows = make(layerGroup, 0)
	}

	if l.Substitutions == nil {
		l.Substitutions = make(layerGroup, 0)
	}
}

// removes nil layers from all layer groups
func (l *ds1Layers) cull() {
	l.cullNilLayers(FloorLayerGroup)
	l.cullNilLayers(WallLayerGroup)
	l.cullNilLayers(ShadowLayerGroup)
	l.cullNilLayers(SubstitutionLayerGroup)
}

// removes nil layers of given layer group type
func (l *ds1Layers) cullNilLayers(t LayerGroupType) {
	group := l.GetLayersGroup(t)
	if group == nil {
		return
	}

	// from last to first layer, remove first encountered nil layer and restart the culling procedure.
	// exit culling procedure when no nil layers are found in entire group.
culling:
	for {
		for idx := len(*group) - 1; idx >= 0; idx-- {
			if (*group)[idx] == nil {
				*group = append((*group)[:idx], (*group)[idx+1:]...)
				continue culling
			}
		}
		break culling // encountered no new nil layers
	}
}

func (l *ds1Layers) Size() (w, h int) {
	l.ensureInit()
	l.cull()

	return l.width, l.height
}

func (l *ds1Layers) SetSize(w, h int) {
	l.width, l.height = w, h

	l.enforceSize(FloorLayerGroup)
	l.enforceSize(WallLayerGroup)
	l.enforceSize(ShadowLayerGroup)
	l.enforceSize(SubstitutionLayerGroup)
}

func (l *ds1Layers) enforceSize(t LayerGroupType) {
	l.ensureInit()
	l.cull()

	group := l.GetLayersGroup(t)
	if group == nil {
		return
	}

	for idx := range *group {
		(*group)[idx].SetSize(l.width, l.height)
	}
}

func (l *ds1Layers) Width() int {
	w, _ := l.Size()
	return w
}

func (l *ds1Layers) SetWidth(w int) {
	l.SetSize(w, l.height)
}

func (l *ds1Layers) Height() int {
	_, h := l.Size()
	return h
}

func (l *ds1Layers) SetHeight(h int) {
	l.SetSize(l.width, h)
}

// generic push func for all layer types
func (l *ds1Layers) push(t LayerGroupType, layer *Layer) {
	l.ensureInit()
	l.cull()
	layer.SetSize(l.Size())

	group := l.GetLayersGroup(t)

	max := GetMaxGroupLen(t)

	if len(*group) < max {
		*group = append(*group, layer)
	}
}

// generic pop func for all layer types
func (l *ds1Layers) pop(t LayerGroupType) *Layer {
	l.ensureInit()
	l.cull()

	group := l.GetLayersGroup(t)
	if group == nil {
		return nil
	}

	var theLayer *Layer

	// remove last layer of slice and return it
	if len(*group) > 0 {
		lastIdx := len(*group) - 1
		theLayer = (*group)[lastIdx]
		*group = (*group)[:lastIdx]

		return theLayer
	}

	return nil
}

func (l *ds1Layers) get(t LayerGroupType, idx int) *Layer {
	l.ensureInit()
	l.cull()

	group := l.GetLayersGroup(t)
	if group == nil {
		return nil
	}

	if idx >= len(*group) || idx < 0 {
		return nil
	}

	return (*group)[idx]
}

func (l *ds1Layers) insert(t LayerGroupType, idx int, newLayer *Layer) {
	l.ensureInit()
	l.cull()

	if newLayer == nil {
		return
	}

	newLayer.SetSize(l.Size())

	group := l.GetLayersGroup(t)
	if group == nil {
		return
	}

	if len(*group)+1 > GetMaxGroupLen(t) {
		return
	}

	if len(*group) == 0 {
		*group = append(*group, newLayer) // nolint:staticcheck // we possibly use group later
		return
	}

	if l := len(*group) - 1; idx > l {
		idx = l
	}

	// example:
	// suppose
	//		idx=1
	//		newLayer=c
	// 		existing layerGroup is [a, b]
	*group = append((*group)[:idx], append([]*Layer{newLayer}, (*group)[idx:]...)...)
}

func (l *ds1Layers) delete(t LayerGroupType, idx int) {
	l.ensureInit()
	l.cull()

	group := l.GetLayersGroup(t)
	if group == nil {
		return
	}

	if idx >= len(*group) || idx < 0 {
		return
	}

	(*group)[idx] = nil

	l.cull()
}

func (l *ds1Layers) GetFloor(idx int) *Layer {
	return l.get(FloorLayerGroup, idx)
}

func (l *ds1Layers) PushFloor(floor *Layer) *ds1Layers {
	l.push(FloorLayerGroup, floor)
	return l
}

func (l *ds1Layers) PopFloor() *Layer {
	return l.pop(FloorLayerGroup)
}

func (l *ds1Layers) InsertFloor(idx int, newFloor *Layer) {
	l.insert(FloorLayerGroup, idx, newFloor)
}

func (l *ds1Layers) DeleteFloor(idx int) {
	l.delete(FloorLayerGroup, idx)
}

func (l *ds1Layers) GetWall(idx int) *Layer {
	return l.get(WallLayerGroup, idx)
}

func (l *ds1Layers) PushWall(wall *Layer) *ds1Layers {
	l.push(WallLayerGroup, wall)
	return l
}

func (l *ds1Layers) PopWall() *Layer {
	return l.pop(WallLayerGroup)
}

func (l *ds1Layers) InsertWall(idx int, newWall *Layer) {
	l.insert(WallLayerGroup, idx, newWall)
}

func (l *ds1Layers) DeleteWall(idx int) {
	l.delete(WallLayerGroup, idx)
}

func (l *ds1Layers) GetShadow(idx int) *Layer {
	return l.get(ShadowLayerGroup, idx)
}

func (l *ds1Layers) PushShadow(shadow *Layer) *ds1Layers {
	l.push(ShadowLayerGroup, shadow)
	return l
}

func (l *ds1Layers) PopShadow() *Layer {
	return l.pop(ShadowLayerGroup)
}

func (l *ds1Layers) InsertShadow(idx int, newShadow *Layer) {
	l.insert(ShadowLayerGroup, idx, newShadow)
}

func (l *ds1Layers) DeleteShadow(idx int) {
	l.delete(ShadowLayerGroup, idx)
}

func (l *ds1Layers) GetSubstitution(idx int) *Layer {
	return l.get(SubstitutionLayerGroup, idx)
}

func (l *ds1Layers) PushSubstitution(sub *Layer) *ds1Layers {
	l.push(SubstitutionLayerGroup, sub)
	return l
}

func (l *ds1Layers) PopSubstitution() *Layer {
	return l.pop(SubstitutionLayerGroup)
}

func (l *ds1Layers) InsertSubstitution(idx int, newSubstitution *Layer) {
	l.insert(SubstitutionLayerGroup, idx, newSubstitution)
}

func (l *ds1Layers) DeleteSubstitution(idx int) {
	l.delete(ShadowLayerGroup, idx)
}

// GetLayersGroup returns layer group depending on type given
func (l *ds1Layers) GetLayersGroup(t LayerGroupType) (group *layerGroup) {
	switch t {
	case FloorLayerGroup:
		group = &l.Floors
	case WallLayerGroup:
		group = &l.Walls
	case ShadowLayerGroup:
		group = &l.Shadows
	case SubstitutionLayerGroup:
		group = &l.Substitutions
	default:
		return nil
	}

	return group
}

// GetMaxGroupLen returns maximum length of layer group of type given
func GetMaxGroupLen(t LayerGroupType) (max int) {
	switch t {
	case FloorLayerGroup:
		max = maxFloorLayers
	case WallLayerGroup:
		max = maxWallLayers
	case ShadowLayerGroup:
		max = maxShadowLayers
	case SubstitutionLayerGroup:
		max = maxSubstitutionLayers
	default:
		return 0
	}

	return max
}
