package main

import (
	"fmt"
	"strings"

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

var Mine *Ticket
var ValidTickets []*Ticket
var AllFields []*Field
var FieldMap map[int]string
var UnknownFields map[int]bool

func main() {
	fmt.Println("# Day 16")
	inp := h.StdinToStringSlice()
	FieldMap = make(map[int]string)
	UnknownFields = make(map[int]bool)
	findValidTickets(inp)
	solveMyTicket()
}

const (
	Criteria = iota
	MyTicket
	NearbyTickets
)

func findValidTickets(inp []string) {
	var invalids []int
	section := Criteria
	for k := range inp {
		if inp[k] == "" {
			// End of a section
			section++
			fmt.Println("Section", section)
			continue
		} else if inp[k] == "your ticket:" || inp[k] == "nearby tickets:" {
			continue
		}
		switch section {
		case Criteria:
			pts := strings.Split(inp[k], ": ")
			f := Field{
				Index: -1,
				Name:  pts[0],
			}
			ranges := strings.Split(pts[1], " or ")
			for rk := range ranges {
				vals := strings.Split(ranges[rk], "-")
				f.AddRange(h.Atoi(vals[0]), h.Atoi(vals[1]))
			}
			AllFields = append(AllFields, &f)
			UnknownFields[k] = true
		case MyTicket:
			vals := strings.Split(inp[k], ",")
			var pts []int
			for vk := range vals {
				pts = append(pts, h.Atoi(vals[vk]))
			}
			Mine = NewTicket(k, pts)

		case NearbyTickets:
			vals := strings.Split(inp[k], ",")
			var pts []int
			for vk := range vals {
				pts = append(pts, h.Atoi(vals[vk]))
			}
			t := NewTicket(k, pts)
			if t.Valid {
				ValidTickets = append(ValidTickets, t)
			} else {
				for _, iv := range t.InvalidValues {
					invalids = append(invalids, iv)
				}
			}
		}
	}
	var sum int
	for k := range invalids {
		sum = sum + invalids[k]
	}
	fmt.Println("## Part 1")
	fmt.Println("Ticket Scanning Error Rate:", sum)
}

func solveMyTicket() {
	fmt.Println()
	var restarts int
	for restarts < len(AllFields) {
		for k := 0; k < len(AllFields); k++ {
			f := AllFields[k]
			if f.Index != -1 {
				continue
			}
			poss := findCommonPossibilities(f.Name)
			if len(poss) == 1 {
				foundField(poss[0], f.Name)
				k = 0
				restarts++
			}
		}
	}
	Mine.SetFields(AllFields)
	ans := 1
	for _, f := range Mine.Fields {
		if strings.HasPrefix(f.Name, "departure") {
			ans = ans * f.Value
		}
	}
	fmt.Println("## Part 2")
	fmt.Println("Answer:", ans)
}

func foundField(num int, name string) {
	for _, f := range AllFields {
		if f.Name == name {
			f.Index = num
		}
	}
	FieldMap[num] = name
	delete(UnknownFields, num)
}

func findCommonPossibilities(name string) []int {
	var poss, not []int
	for k := range UnknownFields {
		poss = append(poss, k)
	}
	for _, t := range ValidTickets {
		for fk, fv := range t.PossibleFieldMap {
			if !h.StringSliceContains(fv, name) {
				not = append(not, fk)
			}
		}
	}
	// Remove all of 'not' from 'poss'
	for k := range not {
		var rem int
		for pk := range poss {
			if poss[pk] == not[k] {
				rem = pk
				break
			}
		}
		if len(poss) == 0 {
			return poss
		}
		poss = append(poss[:rem], poss[rem+1:]...)
	}
	return poss
}

type Range struct {
	Min int
	Max int
}

type Field struct {
	Index  int
	Name   string
	Ranges []Range
	Value  int
}

func (f *Field) ValidForValue(val int) bool {
	for _, r := range f.Ranges {
		if val >= r.Min && val <= r.Max {
			return true
		}
	}
	return false
}

func (f *Field) AddRange(min, max int) {
	f.Ranges = append(f.Ranges, Range{Min: min, Max: max})
}

type Ticket struct {
	Number           int
	Fields           map[string]*Field
	UnnamedFields    []int
	PossibleFieldMap map[int][]string
	InvalidValues    []int
	Valid            bool
}

func NewTicket(num int, inp []int) *Ticket {
	t := Ticket{
		Number:           num,
		Fields:           make(map[string]*Field),
		PossibleFieldMap: make(map[int][]string),
		Valid:            true,
	}
	for k := range inp {
		t.UnnamedFields = append(t.UnnamedFields, inp[k])
		var found bool
		for _, f := range AllFields {
			if f.ValidForValue(inp[k]) {
				found = true
				t.PossibleFieldMap[k] = append(t.PossibleFieldMap[k], f.Name)
			}
		}
		if !found {
			t.InvalidValues = append(t.InvalidValues, inp[k])
			t.Valid = false
		}
	}
	return &t
}

func (t *Ticket) DefineField(f Field) {
	f.Value = t.UnnamedFields[f.Index]
	t.Fields[f.Name] = &f
}

func (t *Ticket) SetFields(fields []*Field) {
	for _, f := range fields {
		t.DefineField(*f)
	}
}