#include <all_far.h>

#include "Int.h"
#include "../lib/DirList.h"

BOOL ParseDirLine(Connection *Connect, BOOL AllFiles, FTPFileInfo *lpFindFileData);

//--------------------------------------------------------------------------------
BOOL FtpKeepAlive(Connection *hConnect)
{
	if (!hConnect) {
		WINPORT(SetLastError)(ECANCELED);	// ERROR_INTERNET_CONNECTION_ABORTED
		return FALSE;
	}

	hConnect->ErrorCode = ERROR_SUCCESS;

	if (!hConnect->ProcessCommand("pwd")) {
		WINPORT(SetLastError)(hConnect->ErrorCode);
		return FALSE;
	} else
		return TRUE;
}

BOOL FtpIsResume(Connection *hConnect)
{
	if (!hConnect)
		return FALSE;

	return ((Connection *)hConnect)->ResumeSupport;
}

BOOL FtpCmdLineAlive(Connection *hConnect)
{
	return hConnect && hConnect->connected && hConnect->cmd_peer != INVALID_SOCKET;
}

void FtpSetRetryCount(Connection *hConnect, int cn)
{
	Assert(hConnect && "FtpSetRetryCount");
	hConnect->RetryCount = cn;
}

BOOL FtpSetBreakable(Connection *hConnect, int cn)
{
	if (!hConnect)
		return FALSE;

	BOOL rc = hConnect->Breakable;

	if (cn != -1) {
		Log(("ESC: set brk %d->%d", hConnect->Breakable, cn));
		hConnect->Breakable = cn;
	}

	return rc;
}

int FtpGetRetryCount(Connection *hConnect)
{
	Assert(hConnect && "FtpGetRetryCount");
	return hConnect->RetryCount;
}

int FtpConnectMessage(Connection *hConnect, int Msg, LPCSTR HostName, int btn /*= MNone__*/,
		int btn1 /*= MNone__*/, int btn2 /*= MNone__*/)
{
	return hConnect ? hConnect->ConnectMessage(Msg, HostName, btn, btn1, btn2) : FALSE;
}

int FtpCmdBlock(Connection *hConnect, int block)
{
	int rc = -1;

	do {
		if (!hConnect)
			break;

		rc = ((Connection *)hConnect)->CmdVisible == FALSE;

		if (block != -1)
			((Connection *)hConnect)->CmdVisible = block == FALSE;
	} while (0);

	return rc;
}

BOOL FtpFindFirstFile(Connection *hConnect, LPCSTR lpszSearchFile, FTPFileInfo *lpFindFileData,
		BOOL *ResetCache)
{
	Assert(hConnect && "FtpFindFirstFile");
	String Command;
	int AllFiles = StrCmp(lpszSearchFile, "*") == 0 || StrCmp(lpszSearchFile, "*.*") == 0;
	int FromCache = 0;

	if (ResetCache && *ResetCache == TRUE) {
		hConnect->CacheReset();
		FtpGetFtpDirectory(hConnect);
		*ResetCache = FALSE;
	}

	if (AllFiles)
		Command = "dir";
	else {
		if (*lpszSearchFile == '/')
			lpszSearchFile = PointToName((char *)lpszSearchFile);

		Command.printf("dir \x1%s\x1", lpszSearchFile);
	}

	WINPORT(SetLastError)(ERROR_SUCCESS);

	if (!AllFiles || (FromCache = hConnect->CacheGet()) == 0) {
		if (AllFiles && !IS_SILENT(FP_LastOpMode) && hConnect->CmdVisible
				&& hConnect->CurrentState != fcsExpandList)
			hConnect->ConnectMessage(MRequestingFolder, hConnect->CurDir.c_str());
		//				hConnect->ToOEMDup(hConnect->CurDir.c_str()));

		int pc = hConnect->ProcessCommand(Command);

		if (!pc) {
			if (hConnect->Host.ServerType == FTP_TYPE_MVS) {
				if (hConnect->code == 550) {
					pc = 1;		// 550 No members found.
				}

				if (hConnect->code == 501 && AllFiles) {
					Command = "dir *";
					pc = hConnect->ProcessCommand(Command);
				}
			}

			if (!pc) {
				WINPORT(SetLastError)(hConnect->ErrorCode);
				return FALSE;
			}
		}
	}

	if (AllFiles && !FromCache)
		hConnect->CacheAdd();

	return ParseDirLine(hConnect, AllFiles, lpFindFileData);
}

BOOL FtpFindNextFile(Connection *hConnect, FTPFileInfo *lpFindFileData)
{
	Assert(hConnect && "FtpFindNextFile");
	return ParseDirLine(hConnect, TRUE, lpFindFileData);
}

BOOL FtpGetCurrentDirectory(Connection *hConnect, String &s)
{
	s.Null();

	if (!hConnect)
		return TRUE;

	if (!hConnect->CurDir.Length()) {
		if (!FtpGetFtpDirectory(hConnect))
			return FALSE;
	}

	s = hConnect->CurDir;
	//	hConnect->ToOEM(s);
	return s.Length() != 0;
}

BOOL FtpSetCurrentDirectory(Connection *hConnect, LPCSTR dir)
{
	String Command;
	Assert(hConnect && "FtpSetCurrentDirectory");

	if (*dir == 0)
		return FALSE;

	do {
		if (StrCmp(dir, "..") == 0) {
			Command.printf("cdup");
		} else {
			Command.printf("cd \x1%s\x1", dir);
		}

		if (hConnect->ProcessCommand(Command))
			break;

		return FALSE;
	} while (0);

	FtpGetFtpDirectory(hConnect);
	return TRUE;
}

BOOL FtpRemoveDirectory(Connection *hConnect, LPCSTR dir)
{
	String Command;
	Assert(hConnect && "FtpRemoveDirectory");

	if (StrCmp(dir, ".") == 0 || StrCmp(dir, "..") == 0)
		return TRUE;

	hConnect->CacheReset();
	// Dir
	Command.printf("rmdir \x1%s\x1", dir);

	if (hConnect->ProcessCommand(Command))
		return TRUE;

	// Dir+slash
	Command.printf("rmdir \x1%s/\x1", dir);

	if (hConnect->ProcessCommand(Command))
		return TRUE;

	// Full dir
	Command.printf("rmdir \x1%s/%s\x1", hConnect->CurDir.c_str(), dir + (dir[0] == '/'));
	//		hConnect->SToOEM(hConnect->CurDir.c_str()).c_str(), dir + (dir[0] == '/'));

	//??FixFTPSlash( Command );
	if (hConnect->ProcessCommand(Command))
		return TRUE;

	// Full dir+slash
	Command.printf("rmdir \"%s/%s/\"", hConnect->CurDir.c_str(), dir + (dir[0] == '/'));
	//		hConnect->SToOEM(hConnect->CurDir.c_str()).c_str(), dir + (dir[0] == '/'));

	if (hConnect->ProcessCommand(Command))
		return TRUE;

	return FALSE;
}

BOOL FtpRenameFile(Connection *Connect, LPCSTR lpszExisting, LPCSTR lpszNew)
{
	String Command;
	Assert(Connect && "FtpRenameFile");
	Connect->CacheReset();
	Command.printf("ren \x1%s\x1 \x1%s\x1", lpszExisting, lpszNew);
	return Connect->ProcessCommand(Command);
}

BOOL FtpDeleteFile(Connection *hConnect, LPCSTR lpszFileName)
{
	String Command;
	Assert(hConnect && "FtpDeleteFile");
	hConnect->CacheReset();
	Command.printf("del \x1%s\x1", lpszFileName);
	return hConnect->ProcessCommand(Command);
}

BOOL FtpChmod(Connection *hConnect, LPCSTR lpszFileName, DWORD Mode)
{
	Assert(hConnect && "FtpChmod");
	String Command;
	hConnect->CacheReset();
	Command.printf("chmod %o \x1%s\x1", Mode, lpszFileName);
	return hConnect->ProcessCommand(Command);
}

BOOL FtpGetFile(Connection *Connect, LPCSTR lpszRemoteFile, LPCSTR lpszNewFile, BOOL Reget, int AsciiMode)
{
	PROC(("FtpGetFile", "[%s]->[%s] %s %s", lpszRemoteFile, lpszNewFile, Reget ? "REGET" : "NEW",
			AsciiMode ? "ASCII" : "BIN"));
	String Command, full_name;
	int ExitCode;
	Assert(Connect && "FtpGetFile");

	// mode
	if (AsciiMode && !Connect->ProcessCommand("ascii")) {
		Log(("!ascii ascii:%d", AsciiMode));
		return FALSE;
	} else if (!AsciiMode && !Connect->ProcessCommand("bin")) {
		Log(("!bin ascii:%d", AsciiMode));
		return FALSE;
	}

	// Create directory
	Command = lpszNewFile;
	int m = Command.RChr('/');

	if (m != -1) {
		Command.SetLength(m);

		if (!DoCreateDirectory(Command.c_str())) {
			Log(("!CreateDirectory [%s]", Command.c_str()));
			return FALSE;
		}
	}

	// Remote file
	if (Connect->Host.ServerType != FTP_TYPE_MVS && *lpszRemoteFile != '/') {
		full_name = Connect->CurDir;	// Connect->ToOEMDup(Connect->CurDir.c_str());
		AddEndSlash(full_name, '/');
		full_name.Add(lpszRemoteFile);
		lpszRemoteFile = full_name.c_str();
	}

	// Get file
	Connect->IOCallback = TRUE;

	if (Reget && !Connect->ResumeSupport) {
		Connect->AddCmdLine(FMSG(MResumeRestart));
		Reget = FALSE;
	}

	Command.printf("%s \x1%s\x1 \x1%s\x1", Reget ? "reget" : "get", lpszRemoteFile, lpszNewFile);
	ExitCode = Connect->ProcessCommand(Command);
	Connect->IOCallback = FALSE;
	return ExitCode;
}

int64_t FtpFileSize(Connection *Connect, LPCSTR fnm)
{
	String Command;
	BYTE Line[20];

	if (!Connect)
		return -1;

	Command.printf("size \x1%s\x1", fnm);

	if (!Connect->ProcessCommand(Command)) {
		Log(("!size"));
		return -1;
	} else {
		Connect->GetReply(Line, sizeof(Line));
		return AtoI((LPCSTR)(Line + 4), (int64_t)-1);
	}
}

BOOL FtpPutFile(Connection *Connect, LPCSTR loc, LPCSTR rem, BOOL Reput, int AsciiMode)
{
	PROC(("FtpPutFile", "[%s]->[%s] %s %s", loc, rem, Reput ? "REGET" : "NEW", AsciiMode ? "ASCII" : "BIN"));
	String Command, full_name;
	int ExitCode;
	int64_t Position;
	Assert(Connect && "FtpPutFile");
	Connect->CacheReset();

	if ((AsciiMode && !Connect->ProcessCommand("ascii")) || (!AsciiMode && !Connect->ProcessCommand("bin"))) {
		Log(("!Set mode"));
		return FALSE;
	}

	if (AsciiMode)
		Reput = FALSE;

	// Remote file
	if (*rem == '\\' || (rem[0] && rem[1] == ':'))	// slash is really back, otherwise BUG
		rem = PointToName((char *)rem);

	if (Connect->Host.ServerType != FTP_TYPE_MVS) {
		if (*rem != '/') {
			//			full_name.printf("%s/%s", Connect->ToOEMDup(Connect->CurDir.c_str()), rem);
			full_name.printf("%s/%s", Connect->CurDir.c_str(), rem);
			rem = full_name.c_str();
		}
	}

	if (Reput) {
		Position = FtpFileSize(Connect, rem);

		if (Position == -1)
			return FALSE;
	} else
		Position = 0;

	Connect->IOCallback = TRUE;
	// Append
	Connect->restart_point = Position;
	Command.printf("%s \x1%s\x1 \x1%s\x1", Position ? "appe" : "put", loc, rem);
	Log(("%s upload", Position ? "Try APPE" : "Use PUT"));
	ExitCode = Connect->ProcessCommand(Command);

	// Error APPE, try to resume using REST
	if (!ExitCode && Position && Connect->ResumeSupport) {
		Log(("APPE fail, try REST upload"));
		Connect->restart_point = Position;
		Command.printf("put \x1%s\x1 \x1%s\x1", loc, rem);
		ExitCode = Connect->ProcessCommand(Command);
	}

	Connect->IOCallback = FALSE;
	return ExitCode;
}

BOOL FtpSystemInfo(Connection *Connect, char *Buffer, int MaxSize)
{
	char *ChPtr;
	Assert(Connect && "FtpSystemInfo");

	if (Buffer && MaxSize < 1)
		return FALSE;

	if (!Connect->SystemInfoFilled) {
		FP_Screen _scr;
		Connect->SystemInfoFilled = TRUE;

		if (Connect->ProcessCommand("syst")) {
			char tmp[200];	// Do not need to use String. Limit system info by 200 chars.
			Connect->GetReply((BYTE *)tmp, sizeof(tmp));

			if ((ChPtr = strchr(tmp, '\r')) != NULL)
				*ChPtr = 0;

			if ((ChPtr = strchr(tmp, '\n')) != NULL)
				*ChPtr = 0;

			if (isdigit(tmp[0]) && isdigit(tmp[1]) && isdigit(tmp[2]))
				StrCpy(Connect->SystemInfo, tmp + 4, ARRAYSIZE(Connect->SystemInfo));
			else
				StrCpy(Connect->SystemInfo, tmp, ARRAYSIZE(Connect->SystemInfo));
		} else {
			*Connect->SystemInfo = 0;
		}

		FTPDirList dl;
		FTPServerInfo si;
		String Line;
		si.ServerType = Connect->Host.ServerType;
		StrCpy(si.ServerInfo, Connect->SystemInfo, ARRAYSIZE(si.ServerInfo));
		WORD idx = dl.DetectStringType(&si, Line.c_str(), Line.Length());

		if (idx == FTP_TYPE_MVS) {
			Log(("site directorymode"));

			if (Connect->ProcessCommand("site directorymode")) {
				char tmp[200];	// Do not need to use String. Limit system info by 200 chars.
				Connect->GetReply((BYTE *)tmp, sizeof(tmp));
			}
		}
	}

	if (Buffer) {
		StrCpy(Buffer, Connect->SystemInfo, MaxSize);
		return *Buffer != 0;
	} else
		return TRUE;
}

BOOL FtpGetFtpDirectory(Connection *Connect)
{
	String s, s1;
	FTPDirList dl;
	char *m;
	FTPServerInfo si;
	WORD idx;
	// Exec
	{
		FP_Screen _scr;

		if (!Connect->ProcessCommand("pwd"))
			return FALSE;
	}
	Connect->GetReply(s);

	do {
		// Detect if unknown
		si.ServerType = Connect->Host.ServerType;
		StrCpy(si.ServerInfo, Connect->SystemInfo, ARRAYSIZE(si.ServerInfo));
		idx = Connect->Host.ServerType;

		if (idx == FTP_TYPE_DETECT || idx == FTP_TYPE_INVALID)
			idx = dl.DetectDirStringType(&si, s.c_str());

		// Parse
		FTPType *tp;
		char tmp[1024];		// There is not way to use String.

		if ((tp = dl.GetType(idx)) != NULL && tp->PWDParse
				&& tp->PWDParse(&si, s.c_str(), tmp, sizeof(tmp))) {
			Connect->CurDir = tmp;
			break;
		}

		// Del digits
		m = s.c_str();

		while (*m && (isdigit(*m) || strchr("\t\b ", *m) != NULL))
			m++;

		if (!m[0])
			return FALSE;

		s.Del(0, (int)(m - s.c_str()));

		// Decode FF
		if (Connect->Host.UndupFF) {
			for (m = s.c_str(); *m; m++) {
				if (m[0] == (char)0xFF && m[1] == (char)0xFF) {
					s1.Add((char)0xFF);
					m++;
				} else
					s1.Add(*m);
			}

			s = s1;
		}

		// Set classic path
		/* Unix:
			 - name enclosed with '"'
			 - if '"' is in name it doubles
		*/
		int b;

		if ((b = s.Chr('\"')) != -1) {
			s1.Null();

			for (int n = b + 1; n < s.Length(); n++)
				if (s[n] == '\"') {
					if (s[n + 1] == '\"') {
						s1.Add(s[n]);
						n++;
					} else
						break;
				} else
					s1.Add(s[n]);

			Connect->CurDir = s1;
		} else
			// Raw
			Connect->CurDir = s;
	} while (0);

	// Remove NL\CR
	int num;

	while ((num = Connect->CurDir.Chr("\r\n")) != -1)
		Connect->CurDir.SetLength(num);

	return TRUE;
}

//------------------------------------------------------------------------
void BadFormat(Connection *Connect, LPCSTR Line, BOOL inParce)
{
	Connect->AddCmdLine(Line);
	FtpConnectMessage(Connect, MNone__,
			inParce ? "Error parsing files list. Please read \"BugReport_*.txt\" and report to developer."
					: "Can not find listing parser. Please read \"BugReport_*.txt\" and report to developer.",
			-MOk);
}

LPCSTR Parser2Str(WORD sType, FTPDirList *dl /*=NULL*/)
{
	BOOL isNew = dl == NULL;
	LPCSTR rc;

	if (sType == FTP_TYPE_DETECT)
		return "Autodetect";
	else if (sType == FTP_TYPE_INVALID)
		return "Invalid";

	if (!dl)
		dl = new FTPDirList;

	FTPType *tp;

	if ((tp = dl->GetType(sType)) == NULL)
		rc = "Unknown";
	else
		rc = tp->TypeName;

	if (isNew)
		delete dl;

	return rc;
}

WORD FTP::SelectServerType(WORD Type)
{
	FarMenuItem MenuItems[50];
	FTPDirList dl;
	WORD n, cn;
	memset(MenuItems, 0, sizeof(MenuItems));
	StrCpy(MenuItems[0].Text, FP_GetMsg(MTableAuto), ARRAYSIZE(MenuItems[0].Text));
	MenuItems[1].Separator = TRUE;
	cn = dl.GetNumberOfSupportedTypes();

	for (n = 0; n < ARRAYSIZE(MenuItems) && n < cn; n++) {
		FTPType *tp = dl.GetType(n);
		snprintf(MenuItems[n + 2].Text, ARRAYSIZE(MenuItems[0].Text), "%s %c %s", tp->TypeName, FAR_VERT_CHAR,
				tp->TypeDescription);
	}

	if (Type >= n)
		MenuItems[0].Selected = TRUE;
	else
		MenuItems[Type + 2].Selected = TRUE;

	int rc = FP_Info->Menu(FP_Info->ModuleNumber, -1, -1, 0, FMENU_AUTOHIGHLIGHT, FP_GetMsg(MTableTitle),
			NULL, NULL, NULL, NULL, MenuItems, n + 2);

	if (rc == -1)
		return Type;
	else
		return ((WORD)rc) == 0 ? FTP_TYPE_DETECT : ((WORD)rc - 2);
}

BOOL ParseDirLine(Connection *Connect, BOOL AllFiles, FTPFileInfo *p)
{
	PROC(("ParseDirLine", "%p,%d", Connect, AllFiles))
	String Line, Line1;
	FTPDirList dl;
	FTPServerInfo si;

	while (true) {
		Connect->GetOutput(Line);

		if (!Line.Length())
			break;

		if (strstr(Line.c_str(), ": Permission denied")) {
			WINPORT(SetLastError)(ERROR_ACCESS_DENIED);
			return FALSE;
		}

		if (Line.Length() < 20)
			continue;

		// Check contains skipped text
		static LPCSTR FTPMsg[] = {"data connection", "transfer complete", "bytes received", "DEVICE:[",
				"Total of "};
		BOOL Found = FALSE;

		for (size_t n = 0; n < ARRAYSIZE(FTPMsg); n++)
			if (strstr(Line.c_str(), FTPMsg[n])) {
				Found = TRUE;
				break;
			}

		if (Found)
			continue;

		// Check special skip strings
		if (StrCmp(Line.c_str(), "Directory ", 10) == 0 && strchr(Line.c_str() + 10, '[') != NULL)
			continue;

		// Set start detect info
		memset(p, 0, sizeof(*p));
		si.ServerType = Connect->Host.ServerType;
		StrCpy(si.ServerInfo, Connect->SystemInfo, ARRAYSIZE(si.ServerInfo));
		// Use temp buffer
		Line1 = Line;
		// Detect
		WORD idx;
		FTPType *tp = dl.GetType(Connect->Host.ServerType);

		if (Connect->Host.ServerType == FTP_TYPE_DETECT || Connect->Host.ServerType == FTP_TYPE_INVALID
				|| tp == NULL) {
			idx = dl.DetectStringType(&si, Line.c_str(), Line.Length());

			if (idx == FTP_TYPE_INVALID || (tp = dl.GetType(idx)) == NULL) {
				LogCmd(Message("ParserDETECT: %s->%s [%s]", Parser2Str(Connect->Host.ServerType, &dl),
							Parser2Str(idx, &dl), Line1.c_str()),
						ldInt);

				if (Connect->Host.ServerType != FTP_TYPE_DETECT
						&& Connect->Host.ServerType != FTP_TYPE_INVALID) {
					LogCmd(Message("ParserIGNORE: [%s]", Line1.c_str()), ldInt);
					Connect->AddCmdLine(Message("ParserIGNORE: [%s]", Line1.c_str()));
					continue;
				}

				BadFormat(Connect, Line1.c_str(), FALSE);
				break;
			} else {
				Log(("ParserDETECTED: %s->%s [%s]", Parser2Str(Connect->Host.ServerType, &dl),
						Parser2Str(idx, &dl), Line1.c_str()));
				Connect->Host.ServerType = idx;
			}
		} else
			idx = Connect->Host.ServerType;

		// Use temp buffer
		Line = Line1;
		Log(("toParse: %d,[%s], %d", Line.Length(), Line.c_str()));

		// Parse
		if (!tp->Parser(&si, p, Line.c_str(), Line.Length())) {
			LogCmd(Message("ParserFAIL: %s->%s [%s]", Parser2Str(Connect->Host.ServerType, &dl),
						Parser2Str(idx, &dl), Line1.c_str()),
					ldInt);
			Connect->AddCmdLine(Message("ParserFAIL: (%s) [%s]", Parser2Str(idx, &dl), Line1.c_str()));
			continue;
		}

		// Skip entryes
		char *CurName = FTP_FILENAME(p);

		if (p->FileType == NET_SKIP || !CurName[0] || StrCmp(CurName, ".") == 0
				|| (!AllFiles && StrCmp(CurName, "..") == 0))
			continue;

		// Correct attrs
		if (p->FileType == NET_DIRECTORY || p->FileType == NET_SYM_LINK_TO_DIR)
			SET_FLAG(p->FindData.dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY);

		if (p->FileType == NET_SYM_LINK_TO_DIR || p->FileType == NET_SYM_LINK)
			SET_FLAG(p->FindData.dwFileAttributes, FILE_ATTRIBUTE_REPARSE_POINT);

		// Convert name text
		//		Connect->ToOEM(CurName);
		// OK
		return TRUE;
	}

	WINPORT(SetLastError)(ERROR_NO_MORE_FILES);
	return FALSE;
}
