package main

import (
	"fmt"
	"strings"

	h "git.bullercodeworks.com/brian/adventofcode/helpers"
)

func main() {
	inp := h.StdinToStringSlice()
	part1(inp)
	fmt.Println()
	part2(inp)
}

func parse(input []string) ([]*Part, map[string]*Workflow) {
	var parts []*Part
	workflows := make(map[string]*Workflow)
	var parseParts bool
	for i := range input {
		if input[i] == "" {
			parseParts = true
			continue
		}
		if parseParts {
			// Parse Parts
			parts = append(parts, NewPart(input[i]))
		} else {
			// Parse Rules
			w := NewWorkflow(input[i])
			workflows[w.name] = w
		}
	}
	// Add 'Accepted' Workflow
	workflows["A"] = &Workflow{
		name: "A",
		process: func(p *Part, workflows map[string]*Workflow) bool {
			return true
		},
	}
	// Add 'Rejected' Workflow
	workflows["R"] = &Workflow{
		name: "R",
		process: func(p *Part, workflows map[string]*Workflow) bool {
			return false
		},
	}
	return parts, workflows
}

func part1(input []string) {
	parts, workflows := parse(input)
	var total int
	for i := range parts {
		if workflows["in"].process(parts[i], workflows) {
			total += parts[i].Value()
		}
	}
	fmt.Println("# Part 1")
	fmt.Println(total)
}

func part2(input []string) {
	_, workflows := parse(input)
	fmt.Println("# Part 2")
	fmt.Println(findAcceptable(workflows, "in", map[byte]Range{'x': DefaultRange(), 'm': DefaultRange(), 'a': DefaultRange(), 's': DefaultRange()}))
}

func findAcceptable(workflows map[string]*Workflow, w string, ranges map[byte]Range) int {
	var result int
	if w == "R" {
		return result
	} else if w == "A" {
		result = 1
		for _, r := range ranges {
			result *= r.end - r.start + 1
		}
		return result
	}
	currWrk := workflows[w]
	for _, rule := range currWrk.rawRules {
		updRanges := copyRangeMap(ranges)
		if rule.cmp == '<' {
			updRng := getRangeFor(updRanges, rule.cat)
			updRng.end = rule.val - 1
			updRanges[rule.cat] = updRng
			upd := getRangeFor(ranges, rule.cat)
			upd.start = rule.val
			ranges[rule.cat] = upd
			result += findAcceptable(workflows, rule.res, updRanges)
		} else if rule.cmp == '>' {
			updRng := getRangeFor(updRanges, rule.cat)
			updRng.start = rule.val + 1
			updRanges[rule.cat] = updRng
			upd := getRangeFor(ranges, rule.cat)
			upd.end = rule.val
			ranges[rule.cat] = upd
			result += findAcceptable(workflows, rule.res, updRanges)
		} else {
			result += findAcceptable(workflows, rule.res, ranges)
		}
	}
	return result
}
func copyRangeMap(ranges map[byte]Range) map[byte]Range {
	n := make(map[byte]Range)
	for k, v := range ranges {
		n[k] = v
	}
	return n
}
func getRangeFor(ranges map[byte]Range, cat byte) Range {
	if v, ok := ranges[cat]; ok {
		return v
	}
	fmt.Println(ranges)
	fmt.Println("Requested:", string(cat))
	panic("Range not found")
}

type Range struct {
	start, end int
}

func DefaultRange() Range { return Range{start: 1, end: 4000} }

type Part struct {
	categories map[byte]int
	x, m, a, s int
}

func NewPart(input string) *Part {
	p := Part{categories: make(map[byte]int)}
	fmt.Sscanf(input, "{x=%d,m=%d,a=%d,s=%d}", &p.x, &p.m, &p.a, &p.s)
	p.categories['x'] = p.x
	p.categories['m'] = p.m
	p.categories['a'] = p.a
	p.categories['s'] = p.s
	return &p
}

func (p *Part) Value() int { return p.x + p.m + p.a + p.s }

type Rule struct {
	cat byte
	cmp byte
	val int
	res string
}

func NewRule(input string) Rule {
	t := strings.Index(input, ":")
	if t >= 0 {
		return Rule{
			cat: input[0],
			cmp: input[1],
			val: h.Atoi(input[2:t]),
			res: input[t+1:],
		}
	} else {
		return Rule{res: input}
	}
}
func (r Rule) String() string {
	return fmt.Sprintf("%s%s%d:%s", string(r.cat), string(r.cmp), r.val, r.res)
}

type Workflow struct {
	name     string
	rawRules []Rule
	process  func(*Part, map[string]*Workflow) bool
}

func NewWorkflow(input string) *Workflow {
	start := strings.Index(input, "{")
	w := Workflow{
		name: input[:start],
	}
	rules := strings.Split(strings.Trim(input[start:], "{}"), ",")
	for i := range rules {
		w.rawRules = append(w.rawRules, NewRule(rules[i]))
	}
	w.process = func(p *Part, workflows map[string]*Workflow) bool {
		for _, rule := range w.rawRules {
			switch rule.cmp {
			case '>':
				if p.categories[rule.cat] > rule.val {
					return workflows[rule.res].process(p, workflows)
				}
			case '<':
				if p.categories[rule.cat] < rule.val {
					return workflows[rule.res].process(p, workflows)
				}
			default:
				return workflows[rule.res].process(p, workflows)
			}
		}
		return true
	}

	return &w
}