package kata

import (
	"fmt"
	"sort"
	"strings"
	"unicode"
)

type count map[rune]int

func (c count) String() (s string) {
	var sl []string
	for r := 'a'; r <= 'z'; r++ {
		n, ok := c[r]
		if ok {
			sl = append(sl, fmt.Sprintf("%c: %d", r, n))
		}
	}
	s = "[" + strings.Join(sl, ", ") + "]"
	return s
}

func (c count) ascending() []string {
	res := make([]string, len(c))
	i := 0
	for r, n := range c {
		res[i] = string(rune(n)) + string(r)
		i++
	}
	sort.Strings(res)
	return res
}

func newCount() count {
	c := make(count, 26)
	for r := 'a'; r <= 'z'; r++ {
		c[r] = 0
	}
	return c
}


func (c count) ingest(s string) {
	// Count letters.
	for _, r := range s {
		if unicode.IsLower(r) {
			c[r]++
		}
	}

	// Remove non-repeated letters and order.
	for r := 'a'; r <= 'z'; r++ {
		occurrences := c[r]
		if occurrences <= 1 {
			delete(c, r)
			continue
		}
	}
}

type hitList map[int]count

func (h hitList) descendingKeys() []int {
	keys:= make([]int, len(h))
	i := 0
	for k := range h {
		keys[i] = k
		i++
	}
	sort.Sort(sort.Reverse(sort.IntSlice(keys)))
	return keys
}

func (h hitList) String() string {
	var sl []string
	for n, c := range h {
		sl = append(sl, fmt.Sprintf("%d: %s", n, c))
	}
	res := "[" + strings.Join(sl, ", ") + "]"
	return res
}

// Cull removes from each count entries not at least equal to the one in the other count.
func cull(c1, c2 count) (r1, r2 count) {
	r1, r2 = count{}, count{}
	for r, n1 := range c1 {
		n2, ok := c2[r]
		if !ok || n1 >= n2 {
			r1[r] = n1
		}
	}
	for r, n2 := range c2 {
		n1, ok := c1[r]
		if !ok || n2 >= n1 {
			r2[r] = n2
		}
	}
	return
}

// Invert build an inverse count, assuming already culled counts.
func invert(c1, c2 count) hitList {
	union := hitList{}
	for r, n := range c1 {
		if _, ok := union[n]; !ok {
			union[n] = count{}
		}
		// On the first string, every letter is missing, so just initialize.
		union[n][r] = '1'
	}
	for r, n := range c2 {
		if _, ok := union[n]; !ok {
			union[n] = count{}
		}
		// On any subsequent string, letters may exist or not, so be careful.
		_, ok  := union[n][r]
		if !ok {
			union[n][r] = '2'
		} else {
			union[n][r] = '='
		}
	}
	return union
}

func Mix(s1, s2 string) string {
	c1, c2 := newCount(), newCount()
	c1.ingest(s1)
	c2.ingest(s2)
	c1, c2 = cull(c1, c2)
	union := invert(c1, c2)
	var sl []string
	for _, n := range union.descendingKeys() {
		for _, hits := range union[n].ascending() {
			source, r := hits[0], hits[1]
			sl = append(sl, fmt.Sprintf("%c:%s", source, strings.Repeat(string(r), n)))
		}
	}
	s := strings.Join(sl, "/")
	return s
}