/*
 * Logserver
 * Copyright (C) 2017-2025 Joel Reardon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef __MATCH__H__
#define __MATCH__H__

#include <iostream>

#include <cassert>
#include <string>
#include <strings.h>

#include "constants.h"

using namespace std;

/* class that takes a line and tells you if the string matches on it. */
// TODO: can take a Line object, possible try to match on original, or would
// allow folding in values into a line that can be searched for but aren't seen.
// e.g., lines that have base64 decodings queued up
class Match {
public:
	explicit Match(int parms) : _parms(parms) {}

	virtual ~Match() {}

	// search defaults
	// look for matches
	static constexpr int PRESENT = 0;
	// match anywhere
	static constexpr int ANCHOR_NONE = 0;

	// search customizations
	// look for non-matches
	static constexpr int MISSING = 1;
	// match at start of line
	static constexpr int ANCHOR_LEFT = 2;
	// match at end of line
	static constexpr int ANCHOR_RIGHT = 4;

	static constexpr int CASE_SENSITIVE = 8;

	/* returns true if the line matches the keyword given the match
	 * parameters stored */
	bool is_match(const string_view& line) const {
		return (_parms & MISSING) ^ match_impl(line);
	}

	/* used to generate the line of information regarding current keywords
	 * being sought. When the word has ! it means it is negated, if it is
	 * preceeded by : it is left anchor, and suceeded by : then a right
	 * anchor. Ellipses indicate that the search is not yet done */
	string get_description() const {
		string ret;
		if (_parms & Match::ANCHOR_LEFT) ret += ":";
		if (_parms & Match::MISSING) ret += "!";
		ret += _keyword;
		if (_parms & Match::ANCHOR_RIGHT) ret += ":";
		return ret;
	}

	/* called by line filter keyword when the user is done typing the
	 * keyword and we now have the finalized keyword */
	void commit_keyword(const string& keyword) {
		_keyword = keyword;
		check_case();
	}

protected:
	/* returns true if this is a case sensitive search */
	bool inline is_case_sensitive() const {
		return (_parms & Match::CASE_SENSITIVE);
	}

	/* check case sets the parms to CASE_SENSITIVE if it notices that there
	 * are any upper case letters. If a capital letter is added and removed,
	 * the keyword stays case sensitive, to allow case sensitive lower case
	 * search without having an extra interface key.
	 */
	void inline check_case() {
		if (_parms & Match::CASE_SENSITIVE) return;
		for (char c : _keyword) {
			if (c != tolower(c)) _parms |= Match::CASE_SENSITIVE;
		}
	}

	/* returns true if the text is neither anchored to left or right */
	bool inline no_anchor() const {
		return (_parms & (ANCHOR_LEFT | ANCHOR_RIGHT)) == 0;
	}

	// heart of string matching. returns true if keyword is in line
	bool match_impl(const string_view& line) const {
		if (line.length() < _keyword.length()) return false;
		if (_keyword == "") return true;

		if (_parms & ANCHOR_LEFT) {
			if (is_case_sensitive())
				return !strncmp(_keyword.data(), line.data(),
					        _keyword.length());
			else
				return !strncasecmp(_keyword.data(),
						    line.data(),
						    _keyword.length());
                } else if (_parms & ANCHOR_RIGHT) {
			size_t pos = line.length() - _keyword.length();
			if (is_case_sensitive())
				return !strncmp(_keyword.data(),
						line.data() + pos,
						_keyword.length());
			else
				return !strncasecmp(_keyword.data(),
						    line.data() + pos,
						    _keyword.length());
		} else {
			if (is_case_sensitive())
				return line.find(_keyword) != string::npos;
			else {
				auto it = search(line.begin(), line.end(),
						 _keyword.begin(), _keyword.end(),
						 [](char c1, char c2) {
							return tolower(c1) == c2;
						});
				return (it != line.end());
			}
		}
	}

	// parameters to match, including anchor and whether we search for it
	// present or missing. refer to public constexpr here for options
	int _parms;

	// commited keyword that we are seeking
	string _keyword;
};

#endif  // __MATCH__H__
