package main

import (
	"fmt"
	"math"
	"strings"

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

func main() {
	inp := helpers.StdinToStringSlice()
	part1(inp)
	part2(inp)
}

var allReactions map[string]*Reaction

func part1(inp []string) {
	allReactions = make(map[string]*Reaction)
	for _, v := range inp {
		r := NewReaction(v)
		allReactions[r.out] = r
	}
	factory := NewFactory(allReactions)
	factory.setInfinite("ORE", true)
	factory.generate("FUEL")
	fmt.Println("# Part 1")
	fmt.Println("Ore Used:", factory.generated["ORE"])
}

func part2(inp []string) {
	fuel := GetMaxFuelForOre(inp, 1000000000000)
	fmt.Println("# Part 2")
	fmt.Println("Fuel:", fuel)
}

func FindRequiredOre(inp []string, fuel int) int {
	allReactions := make(map[string]*Reaction)
	for _, v := range inp {
		r := NewReaction(v)
		allReactions[r.out] = r
	}
	return Produce("FUEL", fuel, allReactions, make(map[string]int))
}

func Produce(chem string, amt int, allReactions map[string]*Reaction, excess map[string]int) int {
	if chem == "ORE" {
		return amt
	}
	if excess[chem] >= amt {
		excess[chem] -= excess[chem]
		excess[chem] = 0
	}
	recipe := allReactions[chem]
	times := int(math.Ceil(float64(amt) / float64(recipe.outQty)))
	ret := 0
	for k, inAmt := range recipe.in {
		ret += Produce(k, inAmt*times, allReactions, excess)
	}
	numProduced := times * recipe.outQty
	excess[chem] += numProduced - amt
	return ret
}

func GetMaxFuelForOre(inp []string, ore int) int {
	var start, guesses, lastGuess, fuelGuess int
	end := ore
	for {
		guesses++
		fuelGuess = (end-start)/2 + start
		requiredOre := FindRequiredOre(inp, fuelGuess)
		if requiredOre == ore {
			break
		}
		if requiredOre > ore {
			end = fuelGuess
		} else {
			start = fuelGuess
		}
		if fuelGuess == lastGuess {
			break
		}
		lastGuess = fuelGuess
	}
	return fuelGuess
}

// this is too slow... Gonna re-engineer it...
func part2Slow(inp []string) {
	allReactions = make(map[string]*Reaction)
	for _, v := range inp {
		r := NewReaction(v)
		allReactions[r.out] = r
	}
	factory := NewFactory(allReactions)
	oreStart := 1000000000000
	factory.setStock("ORE", oreStart)
	fmt.Println("\n# Part 2")
	fmt.Println("Ore in stock", factory.stock["ORE"])
	var out1 string
	for factory.generate("FUEL") {
		clearStr := strings.Repeat("", len(out1))
		out1 = fmt.Sprintf("%s%3d%% (%d)", clearStr, ((factory.stock["ORE"] * 100) / oreStart), factory.generated["FUEL"])
		fmt.Print(out1)
	}
}

type Factory struct {
	stock     map[string]int
	recipes   map[string]*Reaction
	generated map[string]int
	used      map[string]int
	infinite  map[string]bool
}

func NewFactory(recipes map[string]*Reaction) *Factory {
	f := Factory{
		stock:     make(map[string]int),
		recipes:   recipes,
		generated: make(map[string]int),
		used:      make(map[string]int),
		infinite:  make(map[string]bool),
	}
	return &f
}

func (f *Factory) setStock(chem string, amt int) {
	f.stock[chem] = amt
}

func (f *Factory) setInfinite(chem string, is bool) {
	f.infinite[chem] = is
}

func (f *Factory) count(chem string) int {
	return f.stock[chem]
}

func (f *Factory) use(chem string, amt int) bool {
	if f.count(chem) < amt {
		return false
	}
	f.stock[chem] = f.stock[chem] - amt
	f.used[chem] = f.used[chem] + amt
	return true
}

func (f *Factory) generate(chem string) bool {
	if f.infinite[chem] {
		f.stock[chem] = f.stock[chem] + 1
		f.generated[chem] = f.generated[chem] + 1
		return true
	}
	react, ok := f.recipes[chem]
	if !ok {
		return false
	}
	for k, v := range react.in {
		for f.count(k) < v {
			f.generate(k)
		}
		f.use(k, v)
	}
	f.stock[chem] = f.stock[chem] + react.outQty
	f.generated[chem] = f.generated[chem] + react.outQty
	return true
}

type Reaction struct {
	in     map[string]int
	out    string
	outQty int

	ingredients []*Reaction
}

func NewReaction(inp string) *Reaction {
	pts := strings.Split(inp, " => ")
	if len(pts) != 2 {
		panic("Invalid Reaction Input: " + inp)
	}
	ret := Reaction{
		in: make(map[string]int),
	}
	inps := strings.Split(pts[0], ", ")
	for k := range inps {
		var qty int
		var prod string
		_, err := fmt.Sscanf(inps[k], "%d %s", &qty, &prod)
		if err != nil {
			panic("Invalid Reaction Input: " + inp)
		}
		ret.in[prod] = qty
	}
	_, err := fmt.Sscanf(pts[1], "%d %s", &ret.outQty, &ret.out)
	if err != nil {
		panic("Invalid Reaction Input: " + inp)
	}
	return &ret
}

func (r Reaction) String() string {
	var ret string
	for k, v := range r.in {
		if len(ret) > 0 {
			ret = ret + ","
		}
		ret = fmt.Sprintf("%s %d %s", ret, v, k)
	}
	return fmt.Sprintf("%s => %d %s", ret, r.outQty, r.out)
}
