#include "SourceTools.h"

#include "FileTools.h"

// scan a string from a starting position
// and find a #include statement
bool SourceTools::FindInclude(String s, int &startPos, int &endPos, String &incPath)
{
	// get the needed CodeEditor object
	s = s.Mid(startPos);

	CParser parser(s, "");
	while(parser)
	{
		CParser::Pos sPos = parser.GetPos();
		CParser::Pos ePos;
		if(parser.Id("#include"))
		{
			char c;
			if(parser.Char('<'))
				c = '>';
			else if(parser.Char('"'))
				c = '"';
			else
				c = 0;
			if(c)
			{
				// read the include path
				incPath.Clear();
				char c2 = 0;
				while(parser && (c2 = parser.GetChar()) != c && c2 != 0)
				{
					incPath << c2;
					ePos = parser.GetPos();
				}
				// skip if no ending delimiter found
				if(c2 != c)
					continue;
				
				// move past ending delimiter
				ePos.ptr++;
				
				// if include spans more lines, skip it
				if(sPos.line != ePos.line)
					continue;
				
				endPos = startPos + ePos.ptr - s.Begin();
				startPos += sPos.ptr - s.Begin();
				return true;
			}
		}
		else if(parser.IsId())
			parser.ReadId();
		else
			parser.GetChar();
	}
	return false;
}

// scan all includes, retrieving also positions in string
bool SourceTools::FindIncludes(String const &s, Vector<String> &paths, Vector<int> &starts, Vector<int> &ends)
{
	int sPos = 0;
	int ePos;
	String path;
	while(FindInclude(s, sPos, ePos, path))
	{
		paths.Add(path);
		starts.Add(sPos);
		ends.Add(ePos);
		sPos = ePos;
	}
	if(paths.GetCount())
		return true;
	return false;
}

// skip starting comments in string, return position of first non-comment line start
// return insert position for code - negative if we need a \n before
int SourceTools::SkipStartingComments(String const &s)
{
	// get the needed CodeEditor object
	const char *ptr = ~s;

	int line = 0;
	int inComment = 0;
	
	// skip starting empty spaces if any
	while(isspace(*ptr))
	{
		if(*ptr == '\n')
			line++;
		ptr++;
	}
	
	// skip comments
	bool fin = false;
	while(!fin && *ptr)
	{
		// empty line ?
		if(isspace(*ptr))
		{
			const char *p2 = ptr;
			while(*p2 && isspace(*p2) && *p2 != '\n')
				p2++;
			
			// stop on empty line
			if(*p2 == '\n')
			{
				ptr = p2 + 1;
				break;
			}
		}
		
		// standard comment ?
		if(*ptr == '/' && *(ptr + 1) == '*')
		{
			// standard comment, increment comment indent
			inComment++;
			ptr += 2;
			
			// skip a standard (maybe nested) comment
			while(inComment && *ptr)
			{
				if(*ptr == '*' && *(ptr + 1) == '/')
				{
					inComment--;
					ptr++;
				}
				else if(*ptr == '/' && *(ptr + 1) == '*')
				{
					inComment++;
					ptr++;
				}
				else if(*ptr == '\n')
					line++;
				ptr++;
			}
			
			// skip white chars after comment till EOL
			if(*ptr)
			{
				while(isspace(*ptr) && *ptr != '\n')
					ptr++;
				if(*ptr == '\n')
				{
					line++;
					ptr++;
				}
			}
			continue;
		}

		// single line comment
		if(*ptr == '/' && *(ptr + 1) == '/')
		{
			// single line comment, eat it up to end of line
			ptr += 2;
			while(*ptr && *ptr != '\n')
				ptr++;
			if(*ptr == '\n')
			{
				line++;
				ptr++;
			}
			continue;
		}
		
		// space char ?
		if(isspace(*ptr))
		{
			ptr++;
			continue;
		}
		
		// none of the above, we shall stop
		break;
	}
	
	// now find current line start
	const char *lineStart = ptr;
	while(lineStart > ~s && *lineStart != '\n')
		lineStart--;
	if(*lineStart == '\n')
		lineStart++;
	
	int cPos = ptr - ~s;

	// if identifier is not at line start, we shall insert anoter \n
	if(lineStart != ptr)
		cPos = -cPos;

	return cPos;
}

// insert some include files into a source file
bool SourceTools::InsertIncludes(String &s, Vector<String> const &includes)
{
	// first get all include files already in source
	Vector<String>srcIncludes = ScanStringIncludes(s);
	
	// remove them from provided includes
	Vector<String>addIncludes;
	for(int i = 0; i < includes.GetCount(); i++)
		if(FindIndex(srcIncludes, includes[i]) < 0)
			addIncludes.Add(includes[i]);
	if(addIncludes.IsEmpty())
		return true;
	
	// now find a location for newly added includes
	int pos;
	String incS;
	if(srcIncludes.GetCount())
	{
		// if file contains already some #include, put new ones before them
		String dummy;
		pos = 0;
		int endPos;
		FindInclude(s, pos, endPos, dummy);
	}
	else
	{
		// otherwise put them on first non-comment line
		pos = SkipStartingComments(s);
		if(pos < 0)
		{
			incS << "\n";
			pos = -pos;
		}
	}
	
	// build the include string
	for(int i = 0; i < addIncludes.GetCount(); i++)
		incS << "#include<" << addIncludes[i] << ">\n";
	
	// add to source
	s.Insert(pos, incS);
	return true;
}

// remove some include files from a source file
bool SourceTools::RemoveIncludes(String &s, Vector<String> const &includes)
{
	
	// get all includes inside source, along with their positions
	Vector<String> srcIncludes;
	Vector<int> starts;
	Vector<int> ends;
	if(!FindIncludes(s, srcIncludes, starts, ends))
		return false;
	
	// from last one, scan all includes and remove requested ones
	for(int i = srcIncludes.GetCount() - 1; i >= 0; i--)
	{
		String inc = srcIncludes[i];
		if(FindIndex(includes, inc) >= 0)
		{
			int sPos = starts[i];
			int ePos = ends[i];
			
			int nlPos = ePos;
			while(isspace(s[nlPos]) && s[nlPos] != '\n')
				nlPos++;
			if(s[nlPos] == '\n')
				ePos = nlPos + 1;
			
			s.Remove(sPos, ePos - sPos);
		}
	}
	return true;
}

// scan a string for include files
// return an array of all #included files
Vector<String> SourceTools::ScanStringIncludes(String const &s)
{
	Index<String> includes;
	if(s.IsEmpty())
		return includes.PickKeys();

	CParser parser(s);
	while(parser)
	{
		CParser::Pos sPos = parser.GetPos();
		CParser::Pos ePos;
		if(parser.Id("#include"))
		{
			char c;
			if(parser.Char('<'))
				c = '>';
			else if(parser.Char('"'))
				c = '"';
			else
				c = 0;
			if(c)
			{
				// read the include path
				String path;
				
				char c2 = 0;
				while(parser && (c2 = parser.GetChar()) != c && c2 != 0)
				{
					path << c2;
					ePos = parser.GetPos();
				}
				// skip if no ending delimiter found
				if(c2 != c)
					continue;
				
				// move past ending delimiter
				ePos.ptr++;
				
				// if include spans more lines, skip it
				if(sPos.line != ePos.line)
					continue;
				
				// if path contains path delimiters or wrong chars, just skip it
/*
				if(path.Find('/') >= 0 || path.Find('\\') >= 0)
					continue;
*/
				
				// locate start of line... can't trust lineptr
				const char *lineStart = sPos.ptr;
				while(lineStart >= (const char *)s && *lineStart != '\n')
					lineStart--;
				lineStart++;
				includes.FindAdd(path);
			}
		}
		else
			parser.GetChar();
	}
	
	return includes.PickKeys();
}

////////////////////////////////////////////////////////////////////////////////////
// scan a file for include files
// return an array of all #included files
Vector<String> SourceTools::ScanIncludes(String const &path)
{
	String s = LoadFile(path);
	return ScanStringIncludes(s);

}

// scan an array of source files gathering all includes
Vector<String> SourceTools::ScanIncludes(Vector<String> const &files)
{
	Index<String> res;
	for(int i = 0; i < files.GetCount(); i++)
	{
		Vector<String> v = ScanIncludes(files[i]);
		for(int iv = 0; iv < v.GetCount(); iv++)
			res.FindAdd(v[iv]);
	}
	return res.PickKeys();
}

// check t files for timestamp - return true if first is newer
bool SourceTools::Newer(String const &str, String const &obj)
{
	return FileGetTime(str) > FileGetTime(obj);
}

// check first file against a list of files
// return true if first is older of any of the others
bool SourceTools::Older(String const &str, Vector<String> const &objs)
{
	if(!FileExists(str))
		return true;
	Time t = FileGetTime(str);
	for(int i = 0; i < objs.GetCount(); i++)
	{
		if(FileGetTime(objs[i]) > t)
			return true;
	}
	return false;
}

// get oldest filetime of a series of files
Time SourceTools::GetOldestTime(Vector<String> const &files)
{
	if(files.IsEmpty())
		return Time();
	Time time = FileGetTime(files[0]);
	for(int i = 1; i < files.GetCount(); i++)
	{
		Time tim2 = FileGetTime(files[i]);
		if(tim2 < time)
			time = tim2;
	}
	return time;
}

// get newest filetime of a series of files
Time SourceTools::GetNewestTime(Vector<String> const &files)
{
	if(files.IsEmpty())
		return Time();
	Time time = FileGetTime(files[0]);
	for(int i = 1; i < files.GetCount(); i++)
	{
		Time tim2 = FileGetTime(files[i]);
		if(tim2 > time)
			time = tim2;
	}
	return time;
}

// scan a file for function prototypes
Vector<String> SourceTools::scanPrototypes(String const &path)
{
	return Vector<String> ();
}
