package strmatcher

import (
	"container/list"
)

const validCharCount = 53

type MatchType struct {
	matchType Type
	exist     bool
}

const (
	TrieEdge bool = true
	FailEdge bool = false
)

type Edge struct {
	edgeType bool
	nextNode int
}

type ACAutomaton struct {
	trie   [][validCharCount]Edge
	fail   []int
	exists []MatchType
	count  int
}

func newNode() [validCharCount]Edge {
	var s [validCharCount]Edge
	for i := range s {
		s[i] = Edge{
			edgeType: FailEdge,
			nextNode: 0,
		}
	}
	return s
}

var char2Index = []int{
	'A':  0,
	'a':  0,
	'B':  1,
	'b':  1,
	'C':  2,
	'c':  2,
	'D':  3,
	'd':  3,
	'E':  4,
	'e':  4,
	'F':  5,
	'f':  5,
	'G':  6,
	'g':  6,
	'H':  7,
	'h':  7,
	'I':  8,
	'i':  8,
	'J':  9,
	'j':  9,
	'K':  10,
	'k':  10,
	'L':  11,
	'l':  11,
	'M':  12,
	'm':  12,
	'N':  13,
	'n':  13,
	'O':  14,
	'o':  14,
	'P':  15,
	'p':  15,
	'Q':  16,
	'q':  16,
	'R':  17,
	'r':  17,
	'S':  18,
	's':  18,
	'T':  19,
	't':  19,
	'U':  20,
	'u':  20,
	'V':  21,
	'v':  21,
	'W':  22,
	'w':  22,
	'X':  23,
	'x':  23,
	'Y':  24,
	'y':  24,
	'Z':  25,
	'z':  25,
	'!':  26,
	'$':  27,
	'&':  28,
	'\'': 29,
	'(':  30,
	')':  31,
	'*':  32,
	'+':  33,
	',':  34,
	';':  35,
	'=':  36,
	':':  37,
	'%':  38,
	'-':  39,
	'.':  40,
	'_':  41,
	'~':  42,
	'0':  43,
	'1':  44,
	'2':  45,
	'3':  46,
	'4':  47,
	'5':  48,
	'6':  49,
	'7':  50,
	'8':  51,
	'9':  52,
}

func NewACAutomaton() *ACAutomaton {
	ac := new(ACAutomaton)
	ac.trie = append(ac.trie, newNode())
	ac.fail = append(ac.fail, 0)
	ac.exists = append(ac.exists, MatchType{
		matchType: Full,
		exist:     false,
	})
	return ac
}

func (ac *ACAutomaton) Add(domain string, t Type) {
	node := 0
	for i := len(domain) - 1; i >= 0; i-- {
		idx := char2Index[domain[i]]
		if ac.trie[node][idx].nextNode == 0 {
			ac.count++
			if len(ac.trie) < ac.count+1 {
				ac.trie = append(ac.trie, newNode())
				ac.fail = append(ac.fail, 0)
				ac.exists = append(ac.exists, MatchType{
					matchType: Full,
					exist:     false,
				})
			}
			ac.trie[node][idx] = Edge{
				edgeType: TrieEdge,
				nextNode: ac.count,
			}
		}
		node = ac.trie[node][idx].nextNode
	}
	ac.exists[node] = MatchType{
		matchType: t,
		exist:     true,
	}
	switch t {
	case Domain:
		ac.exists[node] = MatchType{
			matchType: Full,
			exist:     true,
		}
		idx := char2Index['.']
		if ac.trie[node][idx].nextNode == 0 {
			ac.count++
			if len(ac.trie) < ac.count+1 {
				ac.trie = append(ac.trie, newNode())
				ac.fail = append(ac.fail, 0)
				ac.exists = append(ac.exists, MatchType{
					matchType: Full,
					exist:     false,
				})
			}
			ac.trie[node][idx] = Edge{
				edgeType: TrieEdge,
				nextNode: ac.count,
			}
		}
		node = ac.trie[node][idx].nextNode
		ac.exists[node] = MatchType{
			matchType: t,
			exist:     true,
		}
	default:
		break
	}
}

func (ac *ACAutomaton) Build() {
	queue := list.New()
	for i := 0; i < validCharCount; i++ {
		if ac.trie[0][i].nextNode != 0 {
			queue.PushBack(ac.trie[0][i])
		}
	}
	for {
		front := queue.Front()
		if front == nil {
			break
		} else {
			node := front.Value.(Edge).nextNode
			queue.Remove(front)
			for i := 0; i < validCharCount; i++ {
				if ac.trie[node][i].nextNode != 0 {
					ac.fail[ac.trie[node][i].nextNode] = ac.trie[ac.fail[node]][i].nextNode
					queue.PushBack(ac.trie[node][i])
				} else {
					ac.trie[node][i] = Edge{
						edgeType: FailEdge,
						nextNode: ac.trie[ac.fail[node]][i].nextNode,
					}
				}
			}
		}
	}
}

func (ac *ACAutomaton) Match(s string) bool {
	node := 0
	fullMatch := true
	// 1. the match string is all through trie edge. FULL MATCH or DOMAIN
	// 2. the match string is through a fail edge. NOT FULL MATCH
	// 2.1 Through a fail edge, but there exists a valid node. SUBSTR
	for i := len(s) - 1; i >= 0; i-- {
		chr := int(s[i])
		if chr >= len(char2Index) {
			return false
		}
		idx := char2Index[chr]
		fullMatch = fullMatch && ac.trie[node][idx].edgeType
		node = ac.trie[node][idx].nextNode
		switch ac.exists[node].matchType {
		case Substr:
			return true
		case Domain:
			if fullMatch {
				return true
			}
		default:
			break
		}
	}
	return fullMatch && ac.exists[node].exist
}