package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"

	"github.com/davecgh/go-spew/spew"
)

type GroupType int

const (
	GroupImmune GroupType = iota
	GroupInfection
)

func (g GroupType) String() string {
	if g == GroupImmune {
		return "immunesystem"
	}
	return "infection"
}

type Group struct {
	ID         int
	Type       GroupType
	Amount     int
	HP         int
	Weak       []string
	Immune     []string
	Damage     int
	DamageType string
	Initiative int
	EP         int
}

func (g *Group) PotentialDamage(target *Group) int {
	if inSlice(g.DamageType, target.Immune) {
		return 0
	}
	if inSlice(g.DamageType, target.Weak) {
		return g.EP * 2
	}
	return g.EP
}

type ByInit []Group

func (b ByInit) Len() int {
	return len(b)
}

func (b ByInit) Swap(i, j int) {
	b[i], b[j] = b[j], b[i]
}

func (b ByInit) Less(i, j int) bool {
	return b[i].Initiative > b[j].Initiative
}

func waitEnter() {
	fmt.Printf("Press ENTER to continue\n")
	bufio.NewReader(os.Stdin).ReadBytes('\n')
}

func sliceEqual(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for t := range a {
		if a[t] != b[t] {
			return false
		}
	}
	return true
}

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

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func manhattan(a, b Point) int {
	return abs(a.X-b.X) + abs(a.Y-b.Y) + abs(a.Z-b.Z)
}

func inSlice(a string, b []string) bool {
	for _, c := range b {
		if a == c {
			return true
		}
	}
	return false
}

func findGroup(groups []Group, id int, groupType GroupType) *Group {
	for t := range groups {
		if groups[t].ID == id && groups[t].Type == groupType {
			return &groups[t]
		}
	}
	return nil
}

func existsInTurnOrder(group *Group, turnorder []Turn) *Group {
	for t := range turnorder {
		if turnorder[t].Target.ID == group.ID && turnorder[t].Target.Type == group.Type {
			return turnorder[t].Target
		}
	}
	return nil
}

func findTarget(attacker *Group, turnorder []Turn, groups []Group) (*Group, int) {
	highest := -1
	index := -1
	init := -1
	hEP := -1
	for t := range groups {
		if groups[t].Amount <= 0 {
			continue
		}
		if (attacker.ID == groups[t].ID && attacker.Type == groups[t].Type) || attacker.Type == groups[t].Type {
			continue
		}
		if existsInTurnOrder(&groups[t], turnorder) != nil {
			continue
		}

		potential := attacker.PotentialDamage(&groups[t])
		if potential > highest {
			highest = potential
			index = t
			init = groups[t].Initiative
			hEP = groups[t].EP
		} else if potential == highest && (groups[t].EP > hEP || groups[t].Initiative > init) {
			highest = potential
			index = t
			init = groups[t].Initiative
			hEP = groups[t].EP
		}
	}
	if index > -1 {
		return &groups[index], highest
	}
	return nil, -1
}

type Point struct {
	X, Y, Z int
}

type Puzzle struct {
	Groups []Group
}

func NewPuzzle(filepath string, boost int) *Puzzle {
	p := Puzzle{
		Groups: make([]Group, 0, 10),
	}

	input, err := os.Open(filepath)
	if err != nil {
		panic(err.Error())
	}
	scanner := bufio.NewScanner(input)

	id := 1
	groupType := GroupImmune
	for t := 0; t < 2; t++ {
		scanner.Scan()
		if err := scanner.Err(); err != nil {
			panic(err.Error())
		}
		for scanner.Scan() {
			var num, hp, pow, init int
			immune := make([]string, 0, 4)
			weak := make([]string, 0, 4)
			var damagetype string

			line := scanner.Text()
			if line == "" {
				scanner.Scan()
				id = 1
				groupType = GroupInfection
				continue
			}

			parts := strings.FieldsFunc(line, func(c rune) bool {
				return c == '(' || c == ')'
			})

			if len(parts) > 1 {
				vparts := strings.Split(parts[1], ";")
				for _, vul := range vparts {
					vul = strings.Trim(vul, " ")
					vulp := strings.FieldsFunc(vul, func(c rune) bool {
						return c == ' ' || c == ','
					})
					for t := 2; t < len(vulp); t++ {
						switch vulp[0] {
						case "weak":
							weak = append(weak, vulp[t])
						case "immune":
							immune = append(immune, vulp[t])
						}
					}
				}
			}

			fmt.Sscanf(parts[0], "%d units each with %d hit points", &num, &hp)
			index := 0
			if len(parts) > 1 {
				index = 2
			}
			fmt.Sscanf(parts[index], " with an attack that does %d %s damage at initiative %d", &pow, &damagetype, &init)

			if groupType == GroupImmune {
				pow += boost
			}

			p.Groups = append(p.Groups, Group{
				ID:         id,
				Type:       groupType,
				Amount:     num,
				Damage:     pow,
				DamageType: damagetype,
				HP:         hp,
				Immune:     immune,
				Initiative: init,
				Weak:       weak,
				EP:         num * pow,
			})
			id++
		}
		if err := scanner.Err(); err != nil {
			panic(err.Error())
		}
	}

	// spew.Dump(p)

	input.Close()

	return &p
}

type Turn struct {
	Group  *Group
	Target *Group
}

func (t *Turn) Exec() int {
	damage := t.Group.PotentialDamage(t.Target)
	deaths := damage / t.Target.HP
	t.Target.Amount -= deaths
	return deaths
}

func (p *Puzzle) Sim(pretty bool) GroupType {
	for {
		turnorder := make([]Turn, 0, 10)

		sort.Sort(ByInit(p.Groups))

		if pretty {
			fmt.Println("\nTarget phase:")
		}
		// Decide turn order
		for t := range p.Groups {
			if p.Groups[t].Amount <= 0 {
				continue
			}

			target, potential := findTarget(&p.Groups[t], turnorder, p.Groups)
			if target == nil {
				if pretty {
					fmt.Println("no targets")
				}
				continue
			}

			if pretty {
				fmt.Printf("#%d:%s -> #%d:%s for %d damage\n", p.Groups[t].ID, p.Groups[t].Type, target.ID, target.Type, potential)
			}

			turnorder = append(turnorder, Turn{
				Group:  &p.Groups[t],
				Target: target,
			})
		}

		if len(turnorder) == 0 {
			fmt.Println("Combat over")
			if pretty {
				spew.Dump(p.Groups)
			}
			sum := 0
			groupType := GroupInfection
			for _, g := range p.Groups {
				if g.Amount > 0 {
					sum += g.Amount
					fmt.Printf("#%d:%s contains %d units\n", g.ID, g.Type, g.Amount)
					groupType = g.Type
				}
			}
			fmt.Println("Sum alive:", sum, groupType)
			return groupType
		}

		if pretty {
			fmt.Println("\nAttack phase:")
		}
		// Execute
		for _, t := range turnorder {
			if pretty {
				fmt.Printf("#%d:%s -> #%d:%s", t.Group.ID, t.Group.Type, t.Target.ID, t.Target.Type)
				num := t.Exec()
				fmt.Printf(", killing %d units\n", num)
			} else {
				t.Exec()
			}
		}
	}
}

func main() {
	// Test1
	t := NewPuzzle("test1.txt", 0)
	t.Sim(true)

	// Test2
	boost := 1
	for {
		t := NewPuzzle("test1.txt", boost)
		if t.Sim(false) == GroupImmune {
			break
		}
		boost++
	}

	// Puzzle1
	p := NewPuzzle("input.txt", 0)
	p.Sim(false)

	// Puzzle2
	boost = 1
	for {
		p := NewPuzzle("input.txt", boost)
		if p.Sim(false) == GroupImmune {
			break
		}
		boost++
	}
}