// Silence may be golden, but sometimes words paint the canvas of understanding.
class Keywords {
	content = ''
	keywords = []

	opts = {
		ignoredTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'img'],
		sameBlock: false,
		selectMultiple: false,
	}

	constructor(opts = {}) {
		const { content, keywords, ...others } = opts

		if (content) {
			this.setContent(content)
		}

		if (keywords) {
			this.setKeywords(keywords)
		}

		this.opts = { ...this.opts, ...others }
	}

	setContent(input) {
		this.content = input
	}

	setKeywords(highlights) {
		this.keywords = [...new Set(highlights)]
	}

	set(input, highlights) {
		this.setContent(input)
		this.setKeywords(highlights)
	}

	getBoundaries() {
		let boundaries = []

		this.getKeywordsRange().forEach(function (range) {
			boundaries.push({
				type: 'start',
				index: range.keywordStartPos,
				highlight: range.phrase,
				start: range.sourceStartPos,
				end: range.sourceEndPos,
			})
			boundaries.push({
				type: 'stop',
				index: range.keywordEndPos,
				highlight: range.sourceStartPos,
			})
		})

		boundaries.sort(function (a, b) {
			if (a.index !== b.index) {
				return b.index - a.index
			} else if (a.type === 'stop' && b.type === 'start') {
				return 1
			} else if (a.type === 'start' && b.type === 'stop') {
				return -1
			} else {
				return 0
			}
		})

		return boundaries
	}

	isValidPortion(portion, opts = null) {
		opts = {
			hasNoLink: true,
			hasNoImage: false,
			hasNoStartingBlockTag: false,
			...(opts || {}),
		}

		let rules = {
			hasNoLink: () => portion.match(/<a\b[^>]*?>/i) !== null,
			hasNoImage: () => portion.match(/<img\b[^>]*?>/i) !== null,
			hasNoStartingBlockTag: () => portion.match(/^\s*(<p\b[^>]*?>|[^<\s][^<]*?)/i) === null,
		}

		for (let option in opts) {
			if (opts[option] && rules[option]()) {
				return false
			}
		}

		return true
	}

	getKeywordsRange(startFrom = 0, endAt = null) {
		let content = this.content.toLocaleLowerCase()
		const ignoredTagsRanges = this.getIgnoredTagsRange()

		let ranges = []
		const cachedRanges = {}

		this.keywords.forEach((keyword) => {
			let error = ''
			let index = startFrom
			let sharesBoundary = false

			while (((index = content.indexOf(keyword.toLowerCase(), index)), !endAt ? index !== -1 : index <= endAt)) {
				let closingIndex = index + keyword.length - 1
				let rangeIsValid = true
				let phrase = ''

				// is whole word
				if (
					(content[index - 1] && content[index - 1].match(/[א-תa-z]/i)) ||
					(content[closingIndex + 1] && content[closingIndex + 1].match(/[א-תa-z]/i))
				) {
					error = 'is not a full word'
					rangeIsValid = false
				}

				// skip html attribute
				if (rangeIsValid) {
					let origin = index
					while (origin > 0) {
						if (['<'].includes(this.content[origin - 1])) {
							break
						}
						origin--
					}

					let intercept = closingIndex
					while (intercept < content.length) {
						if (['>'].includes(this.content[intercept])) {
							break
						}
						intercept++
					}

					const pattern = new RegExp(`<[^>]*\\b(?:[^\\s>]+)\\s*=\\s*["']?[^"'>]*\\b${keyword}\\b[^"'>]*["']?[^>]*>`, 'i');
					if (pattern.test(this.content.slice(Math.max(0, origin - 1), Math.min(intercept + 1, content.length)))) {
						error = 'is an attribute';
						rangeIsValid = false;
					}
				}

				// skip ignored tags
				if (rangeIsValid) {
					for (let i = 0; i < ignoredTagsRanges.length; i++) {
						const ignoredTagRange = ignoredTagsRanges[i]
						if (
							(index >= ignoredTagRange[0] && index <= ignoredTagRange[1]) ||
							(closingIndex >= ignoredTagRange[0] && closingIndex <= ignoredTagRange[1])
						) {
							rangeIsValid = false
							error = ignoredTagRange[2]
							break
						}
					}
				}

				// skip used ranges and block
				if (rangeIsValid) {
					for (let i = 0; i < ranges.length; i++) {
						const usedKeywordRange = ranges[i]

						// is not immediately after
						if (usedKeywordRange[2] <= index && usedKeywordRange[3] > closingIndex) {
							if (
								(index >= usedKeywordRange[0] && index <= usedKeywordRange[1]) ||
								(closingIndex >= usedKeywordRange[0] && closingIndex <= usedKeywordRange[1])
							) {
								rangeIsValid = false
								error = 'overlap with ' + this.content.slice(usedKeywordRange[0], usedKeywordRange[1])
								break
							}

							if (
								(index - 2 >= usedKeywordRange[0] && index - 2 <= usedKeywordRange[1]) ||
								(closingIndex + 2 >= usedKeywordRange[0] && closingIndex + 2 <= usedKeywordRange[1])
							) {
								rangeIsValid = false
								error = 'too close to ' + this.content.slice(usedKeywordRange[0], usedKeywordRange[1])
								break
							}

							sharesBoundary = true
						}
					}
				}

				// keyword already in use
				if (rangeIsValid) {
					phrase = this.content.slice(index, closingIndex + 1)
					rangeIsValid = !ranges.some((range) => phrase.toLowerCase() == range.phrase.toLowerCase())

					if (!rangeIsValid) {
						error = 'duplicate'
					}
				}

				if (rangeIsValid) {
					let origin = index
					outer: while (origin > 0) {
						if (['\r', '\n'].includes(this.content[origin - 1])) {
							break
						}

						if (["<ol", "<li"].includes(this.content.slice(Math.max(0, origin - 3), origin))) {
							while (origin < closingIndex) {
								if ([">"].includes(this.content[origin - 1])) {
									break outer;
								}

								origin++
								
							}
						}

						origin--
					}

					let intercept = closingIndex
					while (intercept < content.length) {
						if (['\r', '\n'].includes(this.content[intercept])) {
							break
						}

						if (["</ol", "</li"].includes(this.content.slice(intercept, intercept + 4))) {
							break;
						}
						intercept++
					}

					const portion = this.content.slice(origin, intercept)

					if (this.isValidPortion(portion)) {
						const portionIdx = cachedRanges[portion]

						if (portionIdx === undefined) {
							cachedRanges[portion] = ranges.length
							ranges.push({
								keywordStartPos: index,
								highlightStartPos: index - origin,
								keywordEndPos: closingIndex,
								sourceStartPos: origin,
								sourceEndPos: intercept,
								phrase,
								keyword,
								source: portion,
								more: [],
							})
						} else {
							ranges[portionIdx].more.push(keyword)
						}
						
						closingIndex = intercept
						if (!this.opts.selectMultiple) {
							break
						}
					}
				}

				index = closingIndex + 1 // add space
			}
		})

		return ranges
	}

	getIgnoredTagsRange() {
		let index = 0
		let content = this.content.toLowerCase()

		const ranges = []
		const { ignoredTags } = this.opts

		for (let tag in ignoredTags) {
			const opening_tag = `<${ignoredTags[tag]} `
			const closing_tag = ['img'].includes(ignoredTags[tag]) ? '/>' : `</${ignoredTags[tag]}>`

			while (((index = content.indexOf(opening_tag, index)), index !== -1)) {
				let closingTagIndex = content.indexOf(closing_tag, index + opening_tag.length)
				if (closingTagIndex == -1) {
					break
				}

				closingTagIndex += closing_tag.length

				if (tag == 'a') {
					index -= 2
					closingTagIndex += 2
				}

				ranges.push([index, closingTagIndex, ignoredTags[tag]])

				index = closingTagIndex
			}
		}

		// handle []
		while (((index = content.indexOf('[', index)), index !== -1)) {
			let closingTagOpeningIndex = content.indexOf('[/', index + 1)
			if (closingTagOpeningIndex == -1) {
				closingTagOpeningIndex = index + 1
			}

			let closingTagClosingIndex = content.indexOf(']', closingTagOpeningIndex + 2)
			if (closingTagClosingIndex == -1) {
				break
			}

			ranges.push([index, closingTagClosingIndex, ignoredTags['shortcode']])

			index = closingTagClosingIndex
		}

		return ranges.sort((a, b) => a[0] - b[0])
	}

	textToWords(content, minLength = 0) {
		return content.split(' ')
	}

	textToSentences(content, minLength = 0) {
		if (['.', ',', '!', '?'].includes(content[content.length - 1])) {
			content = trim(content.substring(0, content.length - 1))
		}

		if (!content || content.length < minLength) {
			return []
		}

		const replace = ['.\r', '. ', '!', '?'] //[', ', ': ', '; ', ' – ', ' (', ') ', ' {', '} ', '—', '”'];

		//change divided symbols inside tags to special codes
		const matches = content.match(/<[^>]+>/gi)
		for (let i = 0; i < matches.length; i++) {
			let tag = matches[i]
			let tag_replaced = tag

			for (const r in replace) {
				if (tag.indexOf(replace[r]) !== -1) {
					const rExp = new RegExp(escapeRegExp(replace[r]), 'ig')
					tag_replaced = tag_replaced.replace(rExp, `[rp${r}]`)
				}
			}

			if (tag_replaced != tag) {
				const tagExp = new RegExp(escapeRegExp(tag), 'ig')
				content = content.replace(tagExp, tag_replaced)
			}
		}

		//divide sentence to phrases
		for (const r in replace) {
			const rExp = new RegExp(escapeRegExp(replace[r]), 'ig')
			content = content.replace(rExp, `${replace[r]}\n`)
		}

		//change special codes to divided symbols inside tags
		for (const r in replace) {
			const rExp = new RegExp(escapeRegExp(`[rp${r}]`), 'ig')
			content = trim(content.replace(rExp, replace[r]))
		}

		return content.split('\n').map((c) => trim(c))
	}

	textToParagraphs(content) {
		const output = []
		const replace_unicode = [
			['\u003c', '\u003e', '\u0022', '&nbsp;'],
			['<', '>', '"', ' '],
		]

		for (let r in replace_unicode[0]) {
			const rExp = new RegExp(escapeRegExp(replace_unicode[0][r]), 'ig')
			content = trim(content.replace(rExp, replace_unicode[1][r]))
		}

		const replace = [
			['<div', '<br', '<li', '<p', '<h1', '<h2', '<h3', '<h4', '<h5', '<h6'],
			['\n<div', '\n<br', '\n<li', '\n<p', '\n<h1', '\n<h2', '\n<h3', '\n<h4', '\n<h5', '\n<h6'],
		]

		for (let r in replace[0]) {
			const rExp = new RegExp(escapeRegExp(replace[0][r]), 'ig')
			content = trim(content.replace(rExp, replace[1][r]))
		}

		const sentences = content.replace(/\[[^\]]+\]/i, '\n').split('\n')

		for (const s in sentences) {
			let sentence = sentences[s]

			if (!sentence || sentence == '') {
				continue
			}
			// todo: remove empty tags

			// remove comments
			if (sentence.substring(0, 4) == '<!--' && sentence.slice(-3) == '-->') {
				continue
			}

			// todo: remove numbering

			// remove empty
			sentence = trim(sentence)
			if (!sentence || sentence == '') {
				continue
			}

			output.push(sentence)
		}

		return output
	}
}
