/*
 * Copyright (C) 2014 PerfectCarl - https://github.com/PerfectCarl/vala-stacktrace
 *
 * 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.
 */

namespace Ivy {

	/**
	 * Extracts frames and builds a {@link Stacktrace}
	 *
	 */
	public class Extractor {

		private bool show_debug_frames = false;

		private string func = "";
		private string file_path = "";
		private string short_file_path = "";
		private string l = "";
		private string file_line = "";
		private string func_line = "";
		private string lib_address ="";

		private static Gee.List<string> libraries_with_no_info = new Gee.ArrayList<string>();

		private string get_module_name () {
			var path = new char[1024];
			Posix.readlink ("/proc/self/exe", path);
			string result = (string) path;
			return result;
		}

		// TODO CARL convert this piece of code to vala conventions
		private static string get_relative_path (string p_fullDestinationPath, string p_startPath) {

			string[] l_startPathParts = p_startPath.split ("/");
			string[] l_destinationPathParts = p_fullDestinationPath.split ("/");

			int l_sameCounter = 0;
			while ((l_sameCounter < l_startPathParts.length) &&
				   (l_sameCounter < l_destinationPathParts.length) &&
				   l_startPathParts[l_sameCounter] == l_destinationPathParts[l_sameCounter]) {
				l_sameCounter++;
			}

			if (l_sameCounter == 0) {
				return p_fullDestinationPath;             // There is no relative link.
			}

			StringBuilder l_builder = new StringBuilder ();
			for (int i = l_sameCounter ; i < l_startPathParts.length ; i++) {
				l_builder.append ("../");
			}

			for (int i = l_sameCounter ; i < l_destinationPathParts.length ; i++) {
				l_builder.append (l_destinationPathParts[i] + "/");
			}

			// CARL l_builder.Length--;
			// Remove the last /
			var result = l_builder.str;
			result = result.substring (0, result.length - 1);
			return result;
		}

		private string extract_short_file_path (string file_path) {
			var path = Environment.get_current_dir ();
			/*var i = file_path.index_of ( path );
			   if( i>=0 )
				return file_path.substring ( path.length, file_path.length - path.length );
			   return file_path; */
			var result = get_relative_path (file_path, path);
			return result;
		}

		// input : '/home/cran/Documents/Projects/elementary/noise/instant-beta/build/core/libnoise-core.so.0(noise_job_repository_create_job+0x309) [0x7ff60a021e69]'
		// ouput: 0x309
		private int extract_base_address (string line) {
			int result = 0;
			var start = line.last_index_of ("+");
			if (start >= 0) {
				var end = line.last_index_of (")");
				if( end > start ) {
					var text = line.substring (start+3,end-start-3);
					text.scanf("%x",  &result);
				}
			}
			return result;
		}

		private void process_info_for_file (string full_line, string str ) {
			func = "";
			file_path = "";
			short_file_path = "";
			l = "";
			file_line = "";
			func_line = "";
			if (full_line == "")
				return;

			var lines = full_line.split ("\n");

			if (lines.length > 0)
				func_line = lines[0];

			if (lines.length > 1)
				file_line = lines[1];
			if (file_line == "??:0" || file_line == "??:?")
				file_line = "";
			func = extract_function_name (str);

			file_path = "";
			short_file_path = "";
			l = "";
			if (file_line != "") {
				if (func == "")
					func = extract_function_name_from_line (func_line);
				file_path = extract_file_path (file_line);
				short_file_path = extract_short_file_path (file_path);
				l = extract_line (file_line);
			}
		}

		private void process_info_from_lib (string file_path, string str) {
			//stdout.printf( "process_info_from_lib('%s', '%s') func: '%s'\n", file_path, str, func);
			var has_info = true;
			var addr1_s = "";
			var lib_addr = "";
			var cmd2 = "";
			lib_address ="";
			lock( libraries_with_no_info)
			 {
				if( libraries_with_no_info.index_of (file_path) == -1 ){
					 // The library is not on the black list
					cmd2 = "nm %s".printf(file_path);

					 addr1_s = execute_command_sync_get_output (cmd2);
					 if( addr1_s == null || addr1_s == "" )
					 {
						// stdout.printf( "ADDED TO NO INFO: '%s'\n", file_path);
						libraries_with_no_info.add (file_path);
						has_info = false;

					 }
				}
				else
					has_info = false;
			}
			if( has_info && func != "" )
			{
				MatchInfo info;
				var expression = "\\n[^ ]* T "+func;
				try {

					Regex regex = new Regex (expression);
					int count = 0;
					string matches = "";
					if( regex.match (addr1_s, 0, out info) )
					 {
						while( info.matches() ){
							var lll = info.fetch(0);
							// stdout.printf ( "lll '%s'\n", lll );
							lib_addr = lll.substring(0, lll.index_of(" "));
							matches += lib_addr + "\n";
							info.next();
							count++;
						}
						if( count >1 )
						{
						   stdout.printf ("  XX %d matches for '%s'. Command: '%s'. Matches: '%s'\n", count, func, cmd2, matches);
						}
						 // stdout.printf ("  YY %d matches for '%s'. Command: '%s'. Matches: '%s'\n", count, func, cmd2, matches);
					}

				} catch (RegexError e)
				{
					critical( "Error while processing regex '%s. Err: '%s", expression, e.message );
				}
				//stdout.printf ("addr1_s %s\n", addr1_s);
				int addr1 = 0;
				lib_addr.scanf("%x",  &addr1);
				if( addr1 != 0 ) {
					int addr2 = extract_base_address (str);
					string addr3 = "%#08x".printf (addr1+addr2);
					lib_address = addr3;
					// stdout.printf ("lib_address : %s\n", lib_address);
					var new_full_line = process_line (file_path, addr3);
					//stdout.printf ("STR : %s\n", str);
					// stdout.printf ("AD1 : %s\n", addr1_s);
					//stdout.printf ("AD2 : %#08x\n", addr2);
					//stdout.printf ("AD3 : %s\n", addr3);
					//stdout.printf ("LIB : %s\n", file_path);
					//stdout.printf ("RES : %s\n", new_full_line);

					process_info_for_file (new_full_line, str );
				}
				else
					stdout.printf ("NULL\n");
			}

		}

		private string extract_function_name (string line) {
			if (line == "")
				return "";
			var start = line.index_of ("(");
			if (start >= 0) {
				var end = line.index_of ("+", start);
				if (end >= 0) {
					var result = line.substring (start + 1, end - start - 1);
					return result.strip ();
				}
			}
			return "";
		}

		private string extract_function_name_from_line (string line) {
			return line.strip ();
		}

		private string extract_file_path_from (string str) {
			if (str == "")
				return "";
			/*if( str.index_of("??") >= 0)
				//result = result.substring (4, line.length - 4 );
				stdout.printf ("ERR2?? : %s\n", str ) ; */
			var start = str.index_of ("(");
			if (start >= 0) {
				return str.substring (0, start).strip ();
			}
			return str.strip ();
		}

		private string extract_file_path (string line) {
			var result = line;
			if (result == "")
				return "";
			if (result == "??:0??:0")
				return "";
			// For some reason, the file name can starts with ??:0
			if (result.has_prefix ("??:0"))
				result = result.substring (4, line.length - 4);
			// stdout.printf ("ERR1?? : %s\n", line );
			var start = result.index_of (":");
			if (start >= 0) {
				result = result.substring (0, start);
				return result.strip ();
			}
			return "";
		}

		private static string extract_line (string line) {
			var result = line;
			if (result == "")
				return "";
			if (result.has_prefix ("??:0"))
				result = result.substring (4, line.length - 4);
			var start = result.index_of (":");
			if (start >= 0) {
				result = result.substring (start + 1, line.length - start - 1);
				var end = result.index_of ("(");
				if (end >= 0) {
					result = result.substring (0, end);
				}
				return result.strip ();
			}
			return "";
		}

		private string extract_address (string line) {
			if (line == "")
				return "";
			var start = line.index_of ("[");
			if (start >= 0) {
				var end = line.index_of ("]", start);
				if (end >= 0) {
					var result = line.substring (start + 1, end - start - 1);
					return result.strip ();
				}
			}
			return "";
		}

		private string execute_command_sync_get_output (string cmd) {
			try {
				int exitCode;
				string std_out;
				string std_err;
				Process.spawn_command_line_sync (cmd, out std_out, out std_err, out exitCode);
				if( exitCode == 0)
					return std_out;
				else
					print ("Error while executing '%s'. Exit code '%d'\n".printf(cmd, exitCode));

			}
			catch (Error e) {
				print ("Error while executing '%s': %s\n".printf(cmd,e.message));
			}
			return "";
		}

		// Poor's man demangler. libunwind is another dep
		// TODO : Optimize this
		// module : app
		// address : 0x007f80
		// output : /home/cran/Projects/noise/noise-perf-instant-search/tests/errors.vala:87
		private string process_line (string module, string address) {
			var cmd = "addr2line -f -e %s %s".printf (module, address);
			var result = execute_command_sync_get_output (cmd);
			//stdout.printf( "CMD %s\n", cmd);
			return result;
		}

	/**
	 * Populates the stacktrace with frames
	 *
	 * The frames are extracted from ``Linux.Backtrace`` and enriched
	 * via calls to unix tools ``nm`` and ``addr2line``.
	 *
	 * ''Warning:'' because this methods calls synchronously other applications (nm and addr2line), it
	 * can have a significant impact on performance.
	 *
	 * @param trace the stacktrace
	 */
		public void create_stacktrace (Stacktrace trace) {
			int frame_count = 100;
			int skipped_frames_count = 5;
			// Stacktrace not due to a crash
			if (trace.is_custom)
				skipped_frames_count = 3;

			void *[] array = new void *[frame_count];

			trace.frames.clear ();
			trace.first_vala = null;
			trace.max_file_name_length = 0;
			trace.is_all_function_name_blank = true;
			trace.is_all_file_name_blank = true;

			// TODO fix that > 0.26
			#if VALA_0_26 || VALA_0_28
			var size = Linux.Backtrace.@get (array);
			var strings = Linux.Backtrace.symbols (array);
			#else
			int size = Linux.backtrace (array, frame_count);
			unowned string[] strings = Linux.backtrace_symbols (array, size);
			// Needed because of some weird bug
			strings.length = size;
			#endif

			int[] addresses = (int[])array;
			string module = get_module_name ();
			// First ones are the handler
			for (int i = skipped_frames_count ; i < size ; i++) {
				int address = addresses[i];
				string str = strings[i];
				var addr = extract_address (str);
				lib_address ="";
				//stdout.printf ("9 '%s'. Addr: '%s' \n", func, addr);
				var full_line = process_line (module, addr);
				//stdout.printf ("10 '%s'\n", func);
				if( full_line == "" ) {
					// Happens when the process memory is going up and up
					// Likely a memory leak
					// Like in the test suite for echo
					// ** (/home/cran/Documents/Projects/i-hate-farms/ide/echo/build/test:2859):
					// CRITICAL **: vala_data_type_copy: assertion 'self != NULL' failed
					// Error while executing 'addr2line -f -e /home/cran/Documents/Projects/i-
					// hate-farms/ide/echo/build/test 0x2afe3b5beb32': Failed to fork (Cannot allocate memory)

					print ("Something went very wrong. Your stacktrace cannot be displayed\n");
					break;
				}
				process_info_for_file( full_line, str);
				//stdout.printf ("11 '%s'\n", func);
				if (file_line == "") {
					file_path = extract_file_path_from (str);

				}
				//stdout.printf ("12 '%s'\n", func);
				// The file name may ends with .so or .so.0 ...
				if( ".so" in file_path ) {
					process_info_from_lib (file_path, str);
				}
				//stdout.printf ("14 '%s'\n", func);
				if( show_debug_frames )
				{
					stdout.printf ("\nFrame %d \n--------\n  . addr: [%s]\n  . full_line: '%s'\n  . file_line: '%s'\n  . func_line: '%s'\n  . str : '%s'\n  . func: '%s'\n  . file: '%s'\n  . line: '%s'\n  . address: '%#08x'\n  . lib_address: '%s'\n",
					i, addr, full_line, file_line, func_line, str, func, file_path, l, address, lib_address);
				}
				if (func != "" && file_path.has_suffix (".vala") && trace.is_all_function_name_blank)
					trace.is_all_function_name_blank = false;

				if (short_file_path != "" && trace.is_all_file_name_blank)
					trace.is_all_file_name_blank = false;

				var line_number = extract_line (file_line);
				var frame = new Frame (addr, file_line, func, file_path, short_file_path, line_number);

				if (trace.first_vala == null && file_path.has_suffix (".vala"))
					trace.first_vala = frame;

				if (short_file_path.length > trace.max_file_name_length)
					trace.max_file_name_length = short_file_path.length;
				if (l.length > trace.max_line_number_length)
					trace.max_line_number_length = l.length;
				trace.frames.add (frame);
			}
		}
	}
}
