package dfaExUtil

/*
 * 扩展DFA算法
 *
 * 一种二层树实现类DFA算法（DFA为多层树结构；go语言特性中的map结构过于“重量级”导致内存占用很大；此外还可能存在大量相同字符结点）
 *
 * 第一层map为所有字母/汉字作为key；value为第二层map
 * 第二层map为第一层冲突字母/汉字的自定义hash作为key；value指示是否为敏感词结束标识
 *
 * 测试结果：50万+的敏感词：
 *   构造树耗时稍优于原始DFA；
 *   内存使用为原DFA的不到1/4：原DFA占用495M内存，此算法使用111M；
 *   查询效率比原DFA低10%~20%左右；主要是多一次map查询和多一次hash计算；
 *
 */

/*
 * 注意使用[]rune的问题(此问题已通过使用固定位数hash解决)：
 * []rune中文使用的是unicode编码；若“中”编码为#4E2D；而#4E2D对应“N-”;
 * 即："N-"与"中"unicode编码均为#4E2D，即会产生hash冲突
 *
 */

import (
	"fmt"
	"strings"

	hash "goutil/dfaExUtil/hash64"
)

// hash使用的类型(uint64对应hash64函数；uint32对应hash32函数)
type hashType = uint64

type DFAEx struct {
	// 忽略大小写；true-忽略大小写；false-大小写敏感
	ignoreCase bool
	// hash冲突个数
	hashCollisions int
	// 树根
	// 字符/hash/uint8(b10000000)(最高字符表示是否结束/低7位表示字符位置)
	root map[rune]map[hashType]uint8
}

// 新建敏感词对象
// wordList - 敏感词列表
// ignoreCase - [可选；默认false] 是否忽略大小写
func NewDFAEx(wordList []string, ignoreCase ...bool) (dfaEx *DFAEx) {
	var iCase bool
	var mapSize int

	if len(ignoreCase) > 0 {
		iCase = ignoreCase[0]
	}

	mapSize = len(wordList) * 10
	// 防止过小
	if mapSize < 1_000 {
		mapSize = 1_000
	}
	// 通常各语言的单rune非重复数不超过1万
	if mapSize > 10_000 {
		mapSize = 10_000
	}

	dfaEx = &DFAEx{
		ignoreCase: iCase,
		root:       make(map[rune]map[hashType]uint8, mapSize),
	}

	for _, v := range wordList {
		word := v
		if iCase {
			// 忽略大小写；所有字母转大写
			word = strings.ToUpper(word)
		}
		wordRune := []rune(word)
		if len(wordRune) > 0 {
			dfaEx.InsertWord(wordRune)
		}
	}

	return dfaEx
}

// 添加敏感词
func (dfaEx *DFAEx) InsertWord(word []rune) {
	var hs hashType
	var lastWord rune
	var lastHash hashType
	for i, v := range word {
		lastWord = v
		lastHash = hs
		if wdInfo, ok := dfaEx.root[v]; ok {
			// "字"已存在
			if hsV, ok := wdInfo[hs]; !ok {
				// hash不存在，添加hash
				wdInfo[hs] = uint8(i & 0x7F) // 第i位
			} else {
				// hash已存在，检测是否冲突
				if (hsV & 0x7F) != uint8(i) {
					// hash冲突
					dfaEx.hashCollisions++
					// fmt.Printf("hash冲突 %s %016X %d %d\n", string(v), hs, i+1, hsV&0x7F+1)
				}
			}
		} else {
			// "字"不存在，添加"字"和hash
			wdInfo = make(map[hashType]uint8)
			wdInfo[hs] = uint8(i & 0x7F) // 第i位
			dfaEx.root[v] = wdInfo
		}
		hs = hash.FastSumByRune2(v, hs) // hash更新
	}

	// 敏感词结束标志(uint8最高位置1)
	dfaEx.root[lastWord][lastHash] |= 0x80
}

// 字符串查找敏感词
func (dfaEx *DFAEx) IsMatch(str string) bool {
	starts, _ := dfaEx.SearchSentence(str, true)
	return len(starts) > 0
}

// 指定字符替换敏感词
func (dfaEx *DFAEx) HandleWord(str string, replace rune) string {
	starts, ends := dfaEx.SearchSentence(str)
	if len(starts) == 0 {
		return str
	}

	strRune := []rune(str)
	for i := 0; i < len(starts); i++ {
		for idx := starts[i]; idx <= ends[i]; idx++ {
			strRune[idx] = replace
		}
	}

	return string(strRune)
}

// 字符串查找敏感词
func (dfaEx *DFAEx) SearchSentence(str string, firstOpt ...bool) (starts, ends []int) {
	var first bool // 是否首次匹配就返回
	if len(firstOpt) > 0 {
		first = firstOpt[0]
	}
	strBak := str
	if dfaEx.ignoreCase {
		// 忽略大小写；所有字母转大写
		strBak = strings.ToUpper(str)
	}
	runeStr := []rune(strBak)
	for i := 0; i < len(runeStr); {
		end := dfaEx.searchByStart(i, runeStr)
		if end < 0 {
			// 继续下一个进行匹配
			i++
		} else {
			// 记录匹配位置；从匹配到的下一个位置继续
			starts = append(starts, i)
			ends = append(ends, end)
			if first {
				// 首次匹配就返回
				break
			}

			i = end + 1
		}
	}

	return
}

// 从指定的开始位置搜索语句
// start - 开始匹配的位置
// str - 待检测字符串
// 返回：匹配到的结束位置，未匹配到返回-1
func (dfaEx *DFAEx) searchByStart(start int, runeStr []rune) (end int) {
	var hs hashType
	end = -1 // 未匹配到返回值

	for i := start; i < len(runeStr); i++ {
		wd := runeStr[i]
		wdInfo, ok := dfaEx.root[wd]
		if !ok {
			// "字"不存在
			break
		}

		hsV, ok := wdInfo[hs]
		if !ok {
			// hash不存在
			break
		}

		// 检测是否句尾
		if (hsV & 0x80) != 0 {
			// 找到句尾，继续匹配，直到匹配到最长敏感词为止
			end = i
		}

		hs = hash.FastSumByRune2(wd, hs) // hash更新
	}

	return
}

// 获取hash冲突数
func (dfaEx *DFAEx) GetHashCollisions() int {
	return dfaEx.hashCollisions
}

// 调试接口
func (dfaEx *DFAEx) Print() {
	fmt.Println(dfaEx)
}

// 调试接口
func (dfaEx *DFAEx) PrintFmt(verbose bool) {
	var keys int
	var hashs int
	for k, v := range dfaEx.root {
		keys++
		if verbose {
			fmt.Println("---------------------------")
			fmt.Println(string(k))
		}
		for kk, vv := range v {
			hashs++
			if verbose {
				fmt.Printf("%016X %02X\n", kk, vv)
			}
		}
	}

	fmt.Println("================================")
	fmt.Println("keys:", keys, "hashs", hashs, "map count", keys+1, "hashCollisions Count", dfaEx.hashCollisions)
}
