/**
 * WinPR: Windows Portable Runtime
 * File Functions
 *
 * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <winpr/config.h>

#include <winpr/crt.h>
#include <winpr/handle.h>

#include <winpr/file.h>

#ifdef WINPR_HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef WINPR_HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include "../log.h"
#define TAG WINPR_TAG("file")

/**
 * File System Behavior in the Microsoft Windows Environment:
 * http://download.microsoft.com/download/4/3/8/43889780-8d45-4b2e-9d3a-c696a890309f/File%20System%20Behavior%20Overview.pdf
 */

LPSTR FilePatternFindNextWildcardA(LPCSTR lpPattern, DWORD* pFlags)
{
	LPSTR lpWildcard = nullptr;
	*pFlags = 0;
	lpWildcard = strpbrk(lpPattern, "*?~");

	if (lpWildcard)
	{
		if (*lpWildcard == '*')
		{
			*pFlags = WILDCARD_STAR;
			return lpWildcard;
		}
		else if (*lpWildcard == '?')
		{
			*pFlags = WILDCARD_QM;
			return lpWildcard;
		}
		else if (*lpWildcard == '~')
		{
			if (lpWildcard[1] == '*')
			{
				*pFlags = WILDCARD_DOS_STAR;
				return lpWildcard;
			}
			else if (lpWildcard[1] == '?')
			{
				*pFlags = WILDCARD_DOS_QM;
				return lpWildcard;
			}
			else if (lpWildcard[1] == '.')
			{
				*pFlags = WILDCARD_DOS_DOT;
				return lpWildcard;
			}
		}
	}

	return nullptr;
}

static BOOL FilePatternMatchSubExpressionA(LPCSTR lpFileName, size_t cchFileName, LPCSTR lpX,
                                           size_t cchX, LPCSTR lpY, size_t cchY, LPCSTR lpWildcard,
                                           LPCSTR* ppMatchEnd)
{
	LPCSTR lpMatch = nullptr;

	if (!lpFileName)
		return FALSE;

	if (*lpWildcard == '*')
	{
		/*
		 *                            S
		 *                         <-----<
		 *                      X  |     |  e       Y
		 * X * Y ==        (0)----->-(1)->-----(2)-----(3)
		 */

		/*
		 * State 0: match 'X'
		 */
		if (_strnicmp(lpFileName, lpX, cchX) != 0)
			return FALSE;

		/*
		 * State 1: match 'S' or 'e'
		 *
		 * We use 'e' to transition to state 2
		 */

		/**
		 * State 2: match Y
		 */

		if (cchY != 0)
		{
			/* TODO: case insensitive character search */
			lpMatch = strchr(&lpFileName[cchX], *lpY);

			if (!lpMatch)
				return FALSE;

			if (_strnicmp(lpMatch, lpY, cchY) != 0)
				return FALSE;
		}
		else
		{
			lpMatch = &lpFileName[cchFileName];
		}

		/**
		 * State 3: final state
		 */
		*ppMatchEnd = &lpMatch[cchY];
		return TRUE;
	}
	else if (*lpWildcard == '?')
	{
		/**
		 *                     X     S     Y
		 * X ? Y ==        (0)---(1)---(2)---(3)
		 */

		/*
		 * State 0: match 'X'
		 *
		 * '?' consumes exactly one character at position cchX, so the file
		 * name must hold at least cchX + 1 characters. Without this the
		 * cchY != 0 branch below reads &lpFileName[cchX + 1], one past the
		 * terminator, when cchFileName == cchX.
		 */
		if (cchFileName < cchX + 1)
			return FALSE;

		if (_strnicmp(lpFileName, lpX, cchX) != 0)
			return FALSE;

		/*
		 * State 1: match 'S'
		 */

		/**
		 * State 2: match Y
		 */

		if (cchY != 0)
		{
			/* TODO: case insensitive character search */
			lpMatch = strchr(&lpFileName[cchX + 1], *lpY);

			if (!lpMatch)
				return FALSE;

			if (_strnicmp(lpMatch, lpY, cchY) != 0)
				return FALSE;
		}
		else
		{
			lpMatch = &lpFileName[cchX + 1];
		}

		/**
		 * State 3: final state
		 */
		*ppMatchEnd = &lpMatch[cchY];
		return TRUE;
	}
	else if (*lpWildcard == '~')
	{
		WLog_ERR(TAG, "warning: unimplemented '~' pattern match");
		return TRUE;
	}

	return FALSE;
}

BOOL FilePatternMatchA(LPCSTR lpFileName, LPCSTR lpPattern)
{
	BOOL match = 0;
	LPCSTR lpTail = nullptr;
	size_t cchTail = 0;
	DWORD dwFlags = 0;
	DWORD dwNextFlags = 0;

	/**
	 * Wild Card Matching
	 *
	 * '*'	matches 0 or more characters
	 * '?'	matches exactly one character
	 *
	 * '~*'	DOS_STAR - matches 0 or more characters until encountering and matching final '.'
	 *
	 * '~?'	DOS_QM - matches any single character, or upon encountering a period or end of name
	 *               string, advances the expression to the end of the set of contiguous DOS_QMs.
	 *
	 * '~.'	DOS_DOT - matches either a '.' or zero characters beyond name string.
	 */

	if (!lpPattern)
		return FALSE;

	if (!lpFileName)
		return FALSE;

	const size_t cchPattern = strlen(lpPattern);
	const size_t cchFileName = strlen(lpFileName);

	/**
	 * First and foremost the file system starts off name matching with the expression “*”.
	 * If the expression contains a single wild card character ‘*’ all matches are satisfied
	 * immediately. This is the most common wild card character used in Windows and expression
	 * evaluation is optimized by looking for this character first.
	 */

	if ((lpPattern[0] == '*') && (cchPattern == 1))
		return TRUE;

	/**
	 * Subsequently evaluation of the “*X” expression is performed. This is a case where
	 * the expression starts off with a wild card character and contains some non-wild card
	 * characters towards the tail end of the name. This is evaluated by making sure the
	 * expression starts off with the character ‘*’ and does not contain any wildcards in
	 * the latter part of the expression. The tail part of the expression beyond the first
	 * character ‘*’ is matched against the file name at the end uppercasing each character
	 * if necessary during the comparison.
	 */

	if (lpPattern[0] == '*')
	{
		lpTail = &lpPattern[1];
		cchTail = strlen(lpTail);

		if (!FilePatternFindNextWildcardA(lpTail, &dwFlags))
		{
			/* tail contains no wildcards */
			if (cchFileName < cchTail)
				return FALSE;

			if (_stricmp(&lpFileName[cchFileName - cchTail], lpTail) == 0)
				return TRUE;

			return FALSE;
		}
	}

	/**
	 * The remaining expressions are evaluated in a non deterministic
	 * finite order as listed below, where:
	 *
	 * 'S' is any single character
	 * 'S-.' is any single character except the final '.'
	 * 'e' is a null character transition
	 * 'EOF' is the end of the name string
	 *
	 *                            S
	 *                         <-----<
	 *                      X  |     |  e       Y
	 * X * Y ==        (0)----->-(1)->-----(2)-----(3)
	 *
	 *
	 *                           S-.
	 *                         <-----<
	 *                      X  |     |  e       Y
	 * X ~* Y ==       (0)----->-(1)->-----(2)-----(3)
	 *
	 *
	 *                     X     S     S     Y
	 * X ?? Y ==       (0)---(1)---(2)---(3)---(4)
	 *
	 *
	 *                     X     S-.     S-.     Y
	 * X ~?~? ==      (0)---(1)-----(2)-----(3)---(4)
	 *                        |       |_______|
	 *                        |            ^  |
	 *                        |_______________|
	 *                            ^EOF of .^
	 *
	 */
	LPCSTR lpWildcard = FilePatternFindNextWildcardA(lpPattern, &dwFlags);

	if (lpWildcard)
	{
		LPCSTR lpMatchEnd = nullptr;
		size_t cchNextWildcard = 0;
		const size_t cchSubPattern = cchPattern;
		LPCSTR lpSubPattern = lpPattern;
		size_t cchSubFileName = cchFileName;
		LPCSTR lpSubFileName = lpFileName;
		size_t cchWildcard = ((dwFlags & WILDCARD_DOS) ? 2 : 1);
		LPCSTR lpNextWildcard =
		    FilePatternFindNextWildcardA(&lpWildcard[cchWildcard], &dwNextFlags);

		if (!lpNextWildcard)
		{
			LPCSTR lpX = lpSubPattern;

			if (lpWildcard < lpSubPattern)
				return FALSE;
			const size_t cchX = WINPR_ASSERTING_INT_CAST(size_t, (lpWildcard - lpSubPattern));
			LPCSTR lpY = &lpSubPattern[cchX + cchWildcard];

			if (lpY < lpSubPattern)
				return FALSE;
			const size_t lpYSSubPattern = WINPR_ASSERTING_INT_CAST(size_t, (lpY - lpSubPattern));

			if (cchSubPattern < lpYSSubPattern)
				return FALSE;
			const size_t cchY = cchSubPattern - lpYSSubPattern;

			return FilePatternMatchSubExpressionA(lpSubFileName, cchSubFileName, lpX, cchX, lpY,
			                                      cchY, lpWildcard, &lpMatchEnd);
		}
		else
		{
			while (lpNextWildcard)
			{
				if (lpSubFileName < lpFileName)
					return FALSE;

				cchSubFileName =
				    cchFileName - WINPR_ASSERTING_INT_CAST(size_t, (lpSubFileName - lpFileName));
				cchNextWildcard = ((dwNextFlags & WILDCARD_DOS) ? 2 : 1);
				LPCSTR lpX = lpSubPattern;

				if (lpWildcard < lpSubPattern)
					return FALSE;
				const size_t cchX = WINPR_ASSERTING_INT_CAST(size_t, (lpWildcard - lpSubPattern));
				LPCSTR lpY = &lpSubPattern[cchX + cchWildcard];

				if (lpNextWildcard < lpWildcard)
					return FALSE;

				const size_t diff = WINPR_ASSERTING_INT_CAST(size_t, (lpNextWildcard - lpWildcard));
				if (diff < cchWildcard)
					return FALSE;

				const size_t cchY = diff - cchWildcard;
				match = FilePatternMatchSubExpressionA(lpSubFileName, cchSubFileName, lpX, cchX,
				                                       lpY, cchY, lpWildcard, &lpMatchEnd);

				if (!match)
					return FALSE;

				lpSubFileName = lpMatchEnd;
				cchWildcard = cchNextWildcard;
				lpWildcard = lpNextWildcard;
				dwFlags = dwNextFlags;
				lpNextWildcard =
				    FilePatternFindNextWildcardA(&lpWildcard[cchWildcard], &dwNextFlags);
			}

			return TRUE;
		}
	}
	else
	{
		/* no wildcard characters */
		if (_stricmp(lpFileName, lpPattern) == 0)
			return TRUE;
	}

	return FALSE;
}
