#include "FileFinder.h"

#include "Project.h"
#include "Settings.h"

static const char *FindInFilesDefaultTypes = "*.ino *.cpp *.h *.hpp *.c *.m *.C *.cxx *.cc *.S";

static void XmlizeDropChoice(XmlIO &xml, String name, WithDropChoice<EditString> &list)
{
	Vector<String>vals;
	if(xml.IsLoading())
	{
		xml(name + "List", vals);
		list.ClearList();
		for(int i = 0; i < vals.GetCount(); i++)
			list.AddList(vals[i]);
	}
	xml(name, list);
	if(xml.IsStoring())
	{
		vals.Clear();
		for(int i = 0; i < list.GetCount(); i++)
			vals.Add(list.Get(i));
		xml(name + "List", vals);
	}
}

void FileFinderClass::Xmlize(XmlIO &xml)
{
	CodeEditor::FindReplaceData d;
	if(xml.IsStoring())
		d = FishIDEEditor.GetFindReplaceData();
	
	xml
		("Find"			, d.find)
		("FindList"		, d.find_list)
		("Replace"		, d.replace)
		("ReplaceList"	, d.replace_list)
		("SameCase"		, d.samecase)
		("WholeWord"	, d.wholeword)
		("WildCards"	, d.wildcards)
		("IgnoreCase"	, d.ignorecase)
		("RegExp"		, d.regexp)
	;
	if(xml.IsLoading())
	{
		if(~ff.files == "")
			ff.files <<= String(FindInFilesDefaultTypes);
		if(!ff.files.GetCount())
		{
			ff.files.AddList((String)FindInFilesDefaultTypes);
			ff.files.AddList((String)"*.txt");
			ff.files.AddList((String)"*.*");
		}

		FishIDEEditor.SetFindReplaceData(d);
	}

	XmlizeDropChoice(xml, "Folder"	, ff.folder);
		
	xml
		("Style"			, ff.style)
//		("sSD"				, sSD)
		("Workspace"		, ff.workspace)
	;
/*
	s % ff.files;
	ff.files.SerializeList(s);
	s % ff.folder;
	ff.folder.SerializeList(s);
	s % ff.replace;
	ff.replace.SerializeList(s);
	s % ff.style;
	s % sSD();

	s % ff.samecase;
	s % ff.regexp;
	s % ff.workspace;
*/
}


FileFinderClass::FileFinderClass()
{
	proj = NULL;

	// initialize find in files layout
	sSD.AllFilesType();
	ConstructFindInFiles();
}

FileFinderClass::~FileFinderClass()
{
}

FileFinderClass &__GetFileFinder()
{
	static FileFinderClass finder;
	return finder;
}

void FileFinderClass::AddFoundFile(const String& fn, int ln, const String& line, int pos, int count)
{
	ErrorInfo f;
	f.file = fn;
	f.lineno = ln;
	f.linepos = pos + 1;
	f.len = count;
	f.kind = 0;
	String ext = GetFileExt(fn);
	if(ext == ".ino")
		ext = ".cpp";
		
	f.message = "\1" + EditorSyntax::GetSyntaxForFilename(ext) + "\1" +
				AsString(pos) + "\1" + AsString(count) + "\1" + line;
	proj->GetFindInFilesPane().Add(fn, ln, f.message, RawToValue(f));
}


void FileFinderClass::Renumber(void)
{
	for(int i = 0; i < filedata.GetCount(); i++)
		::Renumber(filedata[i].lineinfo);
	FishIDEEditor.Renumber();
}

void SearchForFiles(Index<String>& files, String dir, String mask, int readonly, Progress& pi)
{
	FindFile ff(AppendFileName(dir, "*.*"));
	while (ff)
	{
		if (ff.IsFolder() && *ff.GetName() != '.')
			SearchForFiles(files, AppendFileName(dir, ff.GetName()), mask, readonly, pi);
		else
			if (ff.IsFile() && PatternMatchMulti(mask, ff.GetName()))
			{
				if (IsNull(readonly) || !!readonly == !!ff.IsReadOnly())
				{
					if (pi.StepCanceled())
						return;
					files.FindAdd(AppendFileName(dir, ff.GetName()));
				}
			}
		ff.Next();
	}
}

enum
{
	WILDANY = 16,
	WILDONE,
	WILDSPACE,
	WILDNUMBER,
	WILDID,
};

bool Match(const char *f, const char *s, bool we, bool ignorecase, int& count)
{
	const char *b = s;
	while (*f)
	{
		if (*f == WILDANY)
		{
			f++;
			for (;;)
			{
				if (Match(f, s, we, ignorecase, count))
				{
					count += int(s - b);
					return true;
				}
				if (!*s++) break;
			}
			return false;
		}
		else
			if (*f == WILDONE)
			{
				if (!*s++) return false;
			}
			else
				if (*f == WILDSPACE)
				{
					if (*s != ' ' && *s != '\t') return false;
					s++;
					while (*s == ' ' || *s == '\t')
						s++;
				}
				else
					if (*f == WILDNUMBER)
					{
						if (*s < '0' || *s > '9') return false;
						s++;
						while (*s >= '0' && *s <= '9')
							s++;
					}
					else
						if (*f == WILDID)
						{
							if (!iscib(*s)) return false;
							s++;
							while (iscid(*s)) s++;
						}
						else
						{
							if (ignorecase ? ToUpper(*s) != ToUpper(*f) : *s != *f) return false;
							s++;
						}
		f++;
	}
	count = int(s - b);
	return we && iscid(*s) ? false : true;
}

bool FileFinderClass::SearchInFile(const String& fn, const String& pattern, bool wholeword, bool ignorecase, int& n, RegExp *regexp, int style)
{
	FileIn in(fn);
	if (!in) return true;
	int ln = 1;
	bool wb = wholeword ? iscid(*pattern) : false;
	bool we = wholeword ? iscid(*pattern.Last()) : false;
	int infile = 0;
	bool sync = false;
	while (!in.IsEof())
	{
		String line = in.GetLine();
		bool bw = true;
		int  count;
		if (regexp)
		{
			if (regexp->Match(line))
			{
				AddFoundFile(fn, ln, line, regexp->GetOffset(), regexp->GetLength());
				sync = true;
			}
		}
		else
			for (const char *s = line; *s; s++)
			{
				if (bw && Match(pattern, s, we, ignorecase, count))
				{
					AddFoundFile(fn, ln, line, int(s - line), count);
					sync = true;
					infile++;
					n++;
					break;
				}
				if (wb) bw = !iscid(*s);
			}
		ln++;
	}

	if (sync)
		proj->GetFindInFilesPane().Sync();

	in.Close();
	if (infile && style != STYLE_NO_REPLACE)
	{
		proj->ShowTab(fn);
		bool doit = true;
		if (style == STYLE_CONFIRM_REPLACE)
		{
			FishIDEEditor.SetCursor(0);
			FishIDEEditor.Find(false, true);
			switch (PromptYesNoCancel(NFormat("Replace %d lines in [* \1%s\1]?", infile, fn)))
			{
				case 1:
					break;
				case 0:
					doit = false;
					break;
				case - 1:
					return false;
			}
		}
		if (doit)
		{
			FishIDEEditor.SelectAll();
			FishIDEEditor.BlockReplace();
			FishIDEEditor.Flush();
			proj->GetFindInFilesPane().Add(fn, Null, AsString(infile) + " replacements made");
			proj->GetFindInFilesPane().Sync();
		}
	}

	return true;
}

void FileFinderClass::FindInFiles(Project *project, bool replace)
{
	if(!project)
		return;
	proj = project;
	proj->ShowMessagePane(FINDFILESPANE);
	
	if (FishIDEEditor.IsReadOnly())
		replace = false;
	CodeEditor::FindReplaceData d = FishIDEEditor.GetFindReplaceData();
	CtrlRetriever rf;
	rf
		(ff.find, d.find)
		(ff.replace, d.replace)
		(ff.ignorecase, d.ignorecase)
		(ff.samecase, d.samecase)
		(ff.wholeword, d.wholeword)
		(ff.wildcards, d.wildcards)
		(ff.regexp, d.regexp)
	;
	WriteList(ff.find, d.find_list);
	WriteList(ff.replace, d.replace_list);
	ff.Sync();
	if(
		IsNull(~ff.folder) ||
		(
			String(~ff.folder).StartsWith(Settings.GetSketchFolder()) &&
			!String(~ff.folder).StartsWith(proj->GetFolder())
		)
	)
		ff.folder <<= proj->GetFolder();
	ff.style <<= STYLE_NO_REPLACE;
	ff.Sync();
	ff.itext = FishIDEEditor.GetI();
	ff.Setup(replace);

	int c = ff.Execute();

	ff.find.AddHistory();
	ff.replace.AddHistory();

	rf.Retrieve();
	d.find_list = ReadList(ff.find);
	d.replace_list = ReadList(ff.replace);
	FishIDEEditor.SetFindReplaceData(d);

	if (c == IDOK)
	{
		proj->GetFindInFilesPane().HeaderTab(2).SetText("Source line");
		Renumber();
		ff.find.AddHistory();
		ff.files.AddHistory();
		ff.folder.AddHistory();
		ff.replace.AddHistory();
		Progress pi("Found %d files to search.");
		pi.AlignText(ALIGN_LEFT);
		Index<String> files;
/*
		if (ff.workspace)
		{
			const Workspace& wspc = GetIdeWorkspace();
			for (int i = 0; i < wspc.GetCount(); i++)
				SearchForFiles(files, GetFileFolder(PackagePath(wspc[i])),
							   ~ff.files, ~ff.readonly, pi);
		}
		else
*/
//			SearchForFiles(files, NormalizePath(~~ff.folder, GetUppDir()), ~ff.files, ~ff.readonly, pi);
			SearchForFiles(files, NormalizePath(~~ff.folder, proj->GetFolder()), ~ff.files, ~ff.readonly, pi);
		if (!pi.Canceled())
		{
			String pattern;
			RegExp rx, *regexp = NULL;
			if (ff.regexp)
			{
				rx.SetPattern(~ff.find);
				regexp = &rx;
				pattern = "dummy";
			}
			else
				if (ff.wildcards)
				{
					String q = ~ff.find;
					for (const char *s = q; *s; s++)
						if (*s == '\\')
						{
							s++;
							if (*s == '\0') break;
							pattern.Cat(*s);
						}
						else
							switch (*s)
							{
								case '*':
									pattern.Cat(WILDANY);
									break;
								case '?':
									pattern.Cat(WILDONE);
									break;
								case '%':
									pattern.Cat(WILDSPACE);
									break;
								case '#':
									pattern.Cat(WILDNUMBER);
									break;
								case '$':
									pattern.Cat(WILDID);
									break;
								default:
									pattern.Cat(*s);
							}
				}
				else
					pattern = ~ff.find;
			pi.SetTotal(files.GetCount());
//@@			ShowConsole2();
			proj->GetFindInFilesPane().Clear();
			pi.SetPos(0);
			int n = 0;
			for (int i = 0; i < files.GetCount(); i++)
			{
				pi.SetText(files[i]);
				if (pi.StepCanceled()) break;
				if (!IsNull(pattern))
				{
					if (!SearchInFile(files[i], pattern, ff.wholeword, ff.ignorecase, n, regexp, ~ff.style))
						break;
				}
				else
				{
					ErrorInfo f;
					f.file = files[i];
					f.lineno = 1;
					f.linepos = 0;
					f.kind = 0;
					f.message = files[i];
					proj->GetFindInFilesPane().Add(f.file, 1, f.message, RawToValue(f));
					proj->GetFindInFilesPane().Sync();
					n++;
				}
			}
			if (!IsNull(pattern))
				proj->GetFindInFilesPane().Add(Null, Null, Format(t_("%d occurrence(s) have been found."), n));
			else
				proj->GetFindInFilesPane().Add(Null, Null, Format(t_("%d matching file(s) have been found."), n));
			proj->GetFindInFilesPane().HeaderTab(2).SetText(Format(t_("Source line (%d)"), proj->GetFindInFilesPane().GetCount()));
		}
	}
}


void FileFinderClass::FindFileAll(const Vector < Tuple < int, int >> & f)
{
//@@	ShowConsole2();
	proj->GetFindInFilesPane().Clear();
for (auto pos : f)
	{
		FishIDEEditor.CachePos(pos.a);
		int linei = FishIDEEditor.GetLinePos(pos.a);
		WString ln = FishIDEEditor.GetWLine(linei);
		AddFoundFile(FishIDEEditor.GetPath(), linei + 1, ln.ToString(), lenAsUtf8(~ln, pos.a), lenAsUtf8(~ln + pos.a, pos.b));
	}
	proj->GetFindInFilesPane().HeaderTab(2).SetText(Format(t_("Source line (%d)"), proj->GetFindInFilesPane().GetCount()));
	proj->GetFindInFilesPane().Add(Null, Null, Format(t_("%d occurrence(s) have been found."), f.GetCount()));
};

void FileFinderClass::FindString(bool back)
{
	if (!FishIDEEditor.FindString(back))
		BeepExclamation();
}

void FileFinderClass::InsertWildcard(const char *s)
{
	iwc = s;
}

void FileFinderClass::FindWildcard()
{
	int l, h;
	ff.find.GetSelection(l, h);
	iwc = 0;
	FindWildcardMenu(THISBACK(InsertWildcard), ff.find.GetPushScreenRect().TopRight(), false, NULL, ff.regexp);
	if (iwc.GetCount())
	{
		ff.wildcards = true;
		ff.find.SetFocus();
		ff.find.SetSelection(l, h);
		ff.find.RemoveSelection();
		ff.find.Insert(iwc);
	}
}

void FileFinderClass::FindSetStdDir(String n)
{
	ff.folder <<= n;
}

void FileFinderClass::FindStdDir()
{
	String n = GetFileFolder(FishIDEEditor.GetPath());
	MenuBar menu;
	if (!IsNull(n))
		menu.Add(n, THISBACK1(FindSetStdDir, n));
//@@	Vector<String> d = GetUppDirs();
Vector<String> d;
d << proj->GetFolder();

	for (int i = 0; i < d.GetCount(); i++)
		menu.Add(d[i], THISBACK1(FindSetStdDir, d[i]));
	menu.Execute(&ff.folder, ff.folder.GetPushScreenRect().BottomLeft());
}

void FileFinderClass::FindFolder()
{
	if (!sSD.ExecuteSelectDir()) return;
	ff.folder <<= ~sSD;
}

void FileFinderClass::SyncFindInFiles()
{
	ff.samecase.Enable(ff.ignorecase);
}

void FileFinderClass::ConstructFindInFiles()
{
	ff.find.AddButton().SetMonoImage(CtrlImg::smallright()).Tip("Wildcard") <<= THISBACK(FindWildcard);
	ff.files <<= String(FindInFilesDefaultTypes);
	ff.files.AddList((String)FindInFilesDefaultTypes);
	ff.files.AddList((String)"*.txt");
	ff.files.AddList((String)"*.*");
	ff.folder.AddButton().SetMonoImage(CtrlImg::smalldown()).Tip("Related folders") <<= THISBACK(FindStdDir);
	ff.folder.AddButton().SetMonoImage(CtrlImg::smallright()).Tip("Select folder") <<= THISBACK(FindFolder);
	FishIDEEditor.PutI(ff.find);
	FishIDEEditor.PutI(ff.replace);
	CtrlLayoutOKCancel(ff, "Find In Files");
	ff.ignorecase <<= THISBACK(SyncFindInFiles);
	ff.samecase <<= true;
	SyncFindInFiles();
}
