/** \file highlight.c
	Functions for syntax highlighting
*/
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <wchar.h>
#include <wctype.h>
#include <termios.h>
#include <signal.h>

#include "config.h"

#include "fallback.h"
#include "util.h"

#include "wutil.h"
#include "highlight.h"
#include "tokenizer.h"
#include "proc.h"
#include "parser.h"
#include "parse_util.h"
#include "builtin.h"
#include "function.h"
#include "env.h"
#include "expand.h"
#include "sanity.h"
#include "common.h"
#include "complete.h"
#include "output.h"
#include "halloc.h"
#include "halloc_util.h"

static void highlight_universal_internal( wchar_t * buff, 
										  int *color, 
										  int pos, 
										  array_list_t *error );


/**
   The environment variables used to specify the color of different tokens.
*/
static wchar_t *highlight_var[] = 
{
	L"fish_color_normal",
	L"fish_color_command",
	L"fish_color_redirection",
	L"fish_color_end",
	L"fish_color_error",
	L"fish_color_param",
	L"fish_color_comment",
	L"fish_color_match",
	L"fish_color_search_match",
	L"fish_color_operator",
	L"fish_color_escape",
	L"fish_color_quote"
}
	;

#define VAR_COUNT ( sizeof(highlight_var)/sizeof(wchar_t *) )

int highlight_get_color( int highlight )
{
	if( highlight < 0 )
		return FISH_COLOR_NORMAL;
	if( highlight >= VAR_COUNT )
		return FISH_COLOR_NORMAL;
	
	wchar_t *val = env_get( highlight_var[highlight]);
	if( val == 0 )
		val = env_get( highlight_var[HIGHLIGHT_NORMAL]);
	
	if( val == 0 )
	{
		return FISH_COLOR_NORMAL;
	}
	
	return output_color_code( val );
}

/**
   Highligt operators (such as $, ~, %, as well as escaped characters.
*/
static void highlight_param( const wchar_t * buff,
							 int *color,
							 int pos,
							 array_list_t *error )
{
	
	
	int mode = 0; 
	int in_pos, len = wcslen( buff );
	int bracket_count=0;
	wchar_t c;
	
	for( in_pos=0;
		 in_pos<len; 
		 in_pos++ )
	{
		c = buff[in_pos];
		switch( mode )
		{

			/*
			  Mode 0 means unquoted string
			*/
			case 0:
			{
				if( c == L'\\' )
				{
					int start_pos = in_pos;
					in_pos++;
					
					if( wcschr( L"~%", buff[in_pos] ) )
					{
						if( in_pos == 1 )
						{
							color[start_pos] = HIGHLIGHT_ESCAPE;
							color[in_pos+1] = HIGHLIGHT_NORMAL;
						}
					}
					else if( buff[in_pos]==L',' )
					{
						if( bracket_count )
						{
							color[start_pos] = HIGHLIGHT_ESCAPE;
							color[in_pos+1] = HIGHLIGHT_NORMAL;
						}
					}
					else if( wcschr( L"nrtbe*?$(){}'\"<>^ \\#;|&", buff[in_pos] ) )
					{
						color[start_pos]=HIGHLIGHT_ESCAPE;
						color[in_pos+1]=HIGHLIGHT_NORMAL;
					}
					else if( wcschr( L"uUxX01234567", buff[in_pos] ) )
					{
						int i;
						long long res=0;
						int chars=2;
						int base=16;
						
						int byte = 0;
						wchar_t max_val = ASCII_MAX;
						
						switch( buff[in_pos] )
						{
							case L'u':
							{
								chars=4;
								max_val = UCS2_MAX;
								break;
							}
							
							case L'U':
							{
								chars=8;
								max_val = WCHAR_MAX;
								break;
							}
							
							case L'x':
							{
								break;
							}
							
							case L'X':
							{
								byte=1;
								max_val = BYTE_MAX;
								break;
							}
							
							default:
							{
								base=8;
								chars=3;
								in_pos--;
								break;
							}								
						}
						
						for( i=0; i<chars; i++ )
						{
							int d = convert_digit( buff[++in_pos],base);
							
							if( d < 0 )
							{
								in_pos--;
								break;
							}
							
							res=(res*base)|d;
						}

						if( (res <= max_val) )
						{
							color[start_pos] = HIGHLIGHT_ESCAPE;
							color[in_pos+1] = HIGHLIGHT_NORMAL;								
						}
						else
						{	
							color[start_pos] = HIGHLIGHT_ERROR;
							color[in_pos+1] = HIGHLIGHT_NORMAL;								
						}
					}

				}
				else 
				{
					switch( buff[in_pos]){
						case L'~':
						case L'%':
						{
							if( in_pos == 0 )
							{
								color[in_pos] = HIGHLIGHT_OPERATOR;
								color[in_pos+1] = HIGHLIGHT_NORMAL;
							}
							break;
						}

						case L'$':
						{
							wchar_t n = buff[in_pos+1];							
							color[in_pos] = (n==L'$'||wcsvarchr(n))? HIGHLIGHT_OPERATOR:HIGHLIGHT_ERROR;
							color[in_pos+1] = HIGHLIGHT_NORMAL;								
							break;
						}


						case L'*':
						case L'?':
						case L'(':
						case L')':
						{
							color[in_pos] = HIGHLIGHT_OPERATOR;
							color[in_pos+1] = HIGHLIGHT_NORMAL;
							break;
						}
						
						case L'{':
						{
							color[in_pos] = HIGHLIGHT_OPERATOR;
							color[in_pos+1] = HIGHLIGHT_NORMAL;
							bracket_count++;
							break;					
						}
						
						case L'}':
						{
							color[in_pos] = HIGHLIGHT_OPERATOR;
							color[in_pos+1] = HIGHLIGHT_NORMAL;
							bracket_count--;
							break;						
						}

						case L',':
						{
							if( bracket_count )
							{
								color[in_pos] = HIGHLIGHT_OPERATOR;
								color[in_pos+1] = HIGHLIGHT_NORMAL;
							}

							break;					
						}
						
						case L'\'':
						{
							color[in_pos] = HIGHLIGHT_QUOTE;
							mode = 1;
							break;					
						}
				
						case L'\"':
						{
							color[in_pos] = HIGHLIGHT_QUOTE;
							mode = 2;
							break;
						}

					}
				}		
				break;
			}

			/*
			  Mode 1 means single quoted string, i.e 'foo'
			*/
			case 1:
			{
				if( c == L'\\' )
				{
					int start_pos = in_pos;
					switch( buff[++in_pos] )
					{
						case '\\':
						case L'\'':
						{
							color[start_pos] = HIGHLIGHT_ESCAPE;
							color[in_pos+1] = HIGHLIGHT_QUOTE;
							break;
						}
						
						case 0:
						{
							return;
						}
						
					}
					
				}
				if( c == L'\'' )
				{
					mode = 0;
					color[in_pos+1] = HIGHLIGHT_NORMAL;
				}
				
				break;
			}

			/*
			  Mode 2 means double quoted string, i.e. "foo"
			*/
			case 2:
			{
				switch( c )
				{
					case '"':
					{
						mode = 0;
						color[in_pos+1] = HIGHLIGHT_NORMAL;
						break;
					}
				
					case '\\':
					{
						int start_pos = in_pos;
						switch( buff[++in_pos] )
						{
							case L'\0':
							{
								return;
							}
							
							case '\\':
							case L'$':
							case '"':
							{
								color[start_pos] = HIGHLIGHT_ESCAPE;
								color[in_pos+1] = HIGHLIGHT_QUOTE;
								break;
							}
						}
						break;
					}
					
					case '$':
					{
						wchar_t n = buff[in_pos+1];
						color[in_pos] = (n==L'$'||wcsvarchr(n))? HIGHLIGHT_OPERATOR:HIGHLIGHT_ERROR;
						color[in_pos+1] = HIGHLIGHT_QUOTE;								
						break;
					}
				
				}						
				break;
			}
		}
	}
}


void highlight_shell( wchar_t * buff, 
					  int *color, 
					  int pos, 
					  array_list_t *error )
{
	tokenizer tok;
	int had_cmd=0;
	int i;
	int last_val;
	wchar_t *last_cmd=0;
	int len;

	void *context;
	
	if( !buff || !color )
	{
		debug( 0, L"%s called with null input", __func__ );
		return;
	}

	

	len = wcslen(buff);
	
	if( !len )
		return;
	
	context = halloc( 0, 0 );
	
	for( i=0; buff[i] != 0; i++ )
		color[i] = -1;
	
	for( tok_init( &tok, buff, TOK_SHOW_COMMENTS );
		 tok_has_next( &tok );
		 tok_next( &tok ) )
	{	
		int last_type = tok_last_type( &tok );
		int prev_argc=0;
		
		switch( last_type )
		{
			case TOK_STRING:
			{
				if( had_cmd )
				{
					
					/*Parameter */
					wchar_t *param = tok_last( &tok );
					if( param[0] == L'-' )
					{
						if( complete_is_valid_option( last_cmd, param, error ))
							color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM;
						else
							color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
					}
					else
					{
						color[ tok_get_pos( &tok ) ] = HIGHLIGHT_PARAM;
					}					

					highlight_param( param,
									 &color[tok_get_pos( &tok )],
									 pos-tok_get_pos( &tok ), 
									 error );

				}
				else
				{ 
					prev_argc=0;
					
					/*
					  Command. First check that the command actually exists.
					*/
					wchar_t *cmd = expand_one( context, 
											   wcsdup(tok_last( &tok )),
											   EXPAND_SKIP_SUBSHELL | EXPAND_SKIP_VARIABLES);
					
					if( cmd == 0 )
					{
						color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
					}
					else
					{
						int is_cmd = 0;
						int is_subcommand = 0;
						int mark = tok_get_pos( &tok );
						color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMAND;
						
						if( parser_is_subcommand( cmd ) )
						{
							tok_next( &tok );
							if(( wcscmp( L"-h", tok_last( &tok ) ) == 0 ) ||
							   ( wcscmp( L"--help", tok_last( &tok ) ) == 0 ) )							
							{
								/* 
								   The builtin and command builtins
								   are normally followed by another
								   command, but if they are invoked
								   with the -h option, their help text
								   is displayed instead
								*/
							}
							else
							{
								is_subcommand = 1;
							}
							tok_set_pos( &tok, mark );
						}

						if( !is_subcommand )
						{
							wchar_t *tmp;
							/*
							  OK, this is a command, it has been
							  successfully expanded and everything
							  looks ok. Lets check if the command
							  exists.
							*/

							/*
							  First check if it is a builtin or
							  function, since we don't have to stat
							  any files for that
							*/
							is_cmd |= builtin_exists( cmd );
							is_cmd |= function_exists( cmd );

							/*
							  Moving on to expensive tests
							*/

							/*
							  Check if this is a regular command
							*/
							is_cmd |= !!(tmp=parser_get_filename( context, cmd ));

							/* 
							   Could not find the command. Maybe it is
							   a path for a implicit cd command.
							*/
							is_cmd |= !!(tmp=parser_cdpath_get( context, cmd ));
														
							if( is_cmd )
							{								
								color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMAND;
							}
							else
							{
								if( error )
									al_push( error, wcsdupcat2 ( L"Unknown command \'", cmd, L"\'", 0 ));
								color[ tok_get_pos( &tok ) ] = (HIGHLIGHT_ERROR);
							}
							had_cmd = 1;
						}

						if( had_cmd )
						{
							last_cmd = halloc_wcsdup( context, tok_last( &tok ) );						
						}
					}

				}
				break;
			}
		
			case TOK_REDIRECT_OUT:
			case TOK_REDIRECT_IN:
			case TOK_REDIRECT_APPEND:
			case TOK_REDIRECT_FD:
			{
				if( !had_cmd )
				{
					color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
					if( error )
						al_push( error, wcsdup ( L"Redirection without a command" ) );
					break;
				}
				
				wchar_t *target=0;
				
				color[ tok_get_pos( &tok ) ] = HIGHLIGHT_REDIRECTION;
				tok_next( &tok );

				/*
				  Check that we are redirecting into a file
				*/

				switch( tok_last_type( &tok ) )
				{
					case TOK_STRING:
					{
						target = expand_one( context, wcsdup( tok_last( &tok ) ), EXPAND_SKIP_SUBSHELL);
						/*
						  Redirect filename may contain a subshell. 
						  If so, it will be ignored/not flagged.
						*/
					}
					break;
					default:
					{
						color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
						if( error )
							al_push( error, wcsdup ( L"Invalid redirection" ) );
					}
					
				}

				if( target != 0 )
				{
					wchar_t *dir = halloc_wcsdup( context, target );
					wchar_t *dir_end = wcsrchr( dir, L'/' );
					struct stat buff;
					/* 
					   If file is in directory other than '.', check
					   that the directory exists.
					*/
					if( dir_end != 0 )
					{
						*dir_end = 0;
						if( wstat( dir, &buff ) == -1 )
						{
							color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
							if( error )
								al_push( error, wcsdupcat2( L"Directory \'", dir, L"\' does not exist", 0 ) );
							
						}
					}
					
					/*
					  If the file is read from or appended to, check
					  if it exists.
					*/
					if( last_type == TOK_REDIRECT_IN || 
						last_type == TOK_REDIRECT_APPEND )
					{
						if( wstat( target, &buff ) == -1 )
						{
							color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
							if( error )
								al_push( error, wcsdupcat2( L"File \'", target, L"\' does not exist", 0 ) );
						}
					}
				}
				break;
			}
			
			case TOK_PIPE:
			case TOK_BACKGROUND:
			{
				if( had_cmd )
				{
					color[ tok_get_pos( &tok ) ] = HIGHLIGHT_END;
					had_cmd = 0;
				}
				else
				{
					color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;					
					if( error )
						al_push( error, wcsdup ( L"No job to put in background" ) );
				}
				
				break;
			}
			
			case TOK_END:
			{
				color[ tok_get_pos( &tok ) ] = HIGHLIGHT_END;
				had_cmd = 0;
				break;
			}
			
			case TOK_COMMENT:
			{
				color[ tok_get_pos( &tok ) ] = HIGHLIGHT_COMMENT;
				break;
			}
			
			case TOK_ERROR:
			default:
			{
				/*
				  If the tokenizer reports an error, highlight it as such.
				*/
				if( error )
					al_push( error, wcsdup ( tok_last( &tok) ) );
				color[ tok_get_pos( &tok ) ] = HIGHLIGHT_ERROR;
				break;				
			}			
		}
	}

	tok_destroy( &tok );	
			 
	/*
	  Locate and syntax highlight subshells recursively
	*/

	wchar_t *buffcpy = halloc_wcsdup( context, buff );
	wchar_t *subpos=buffcpy;
	int done=0;
	
	while( 1 )
	{
		wchar_t *begin, *end;
	
		if( parse_util_locate_cmdsubst( subpos, 
										(const wchar_t **)&begin, 
										(const wchar_t **)&end,
										1) <= 0)
		{
			break;
		}
		
		if( !*end )
			done=1;
		else
			*end=0;
		
		highlight_shell( begin+1, color +(begin-buffcpy)+1, -1, error );
		color[end-buffcpy]=HIGHLIGHT_OPERATOR;
		
		if( done )
			break;
		
		subpos = end+1;		
		
	}

	last_val=0;
	for( i=0; buff[i] != 0; i++ )
	{
		if( color[i] >= 0 )
			last_val = color[i];
		else
			color[i] = last_val;
	}


	highlight_universal_internal( buff, color, pos, error );

	/*
	  Spaces should not be highlighted at all, since it makes cursor look funky in some terminals
	*/
	for( i=0; buff[i]; i++ )
	{
		if( iswspace(buff[i]) )
		{
			color[i]=0;
		}
	}

	halloc_free( context );
	
}

/**
   Perform quote and parenthesis highlighting on the specified string.
*/
static void highlight_universal_internal( wchar_t * buff, 
										  int *color, 
										  int pos, 
										  array_list_t *error )
{
	
	if( (pos >= 0) && (pos < wcslen(buff)) )
	{
		
		/*
		  Highlight matching quotes
		*/
		if( (buff[pos] == L'\'') || (buff[pos] == L'\"') )
		{

			array_list_t l;
			al_init( &l );
		
			int level=0;
			wchar_t prev_q=0;
		
			wchar_t *str=buff;

			int match_found=0;
		
			while(*str)
			{
				switch( *str )
				{
					case L'\\':
						str++;
						break;
					case L'\"':
					case L'\'':
						if( level == 0 )
						{
							level++;
							al_push( &l, (void *)(str-buff) );
							prev_q = *str;
						}
						else
						{
							if( prev_q == *str )
							{
								long pos1, pos2;
							
								level--;
								pos1 = (long)al_pop( &l );
								pos2 = str-buff;
								if( pos1==pos || pos2==pos )
								{
									color[pos1]|=HIGHLIGHT_MATCH<<8;
									color[pos2]|=HIGHLIGHT_MATCH<<8;
									match_found = 1;
									
								}
								prev_q = *str==L'\"'?L'\'':L'\"';
							}
							else
							{
								level++;
								al_push( &l, (void *)(str-buff) );
								prev_q = *str;
							}
						}
					
						break;
				}
				if( (*str == L'\0'))
					break;

				str++;
			}

			al_destroy( &l );
		
			if( !match_found )
				color[pos] = HIGHLIGHT_ERROR<<8;
		}

		/*
		  Highlight matching parenthesis
		*/
		if( wcschr( L"()[]{}", buff[pos] ) )
		{
			int step = wcschr(L"({[", buff[pos])?1:-1;
			wchar_t dec_char = *(wcschr( L"()[]{}", buff[pos] ) + step);
			wchar_t inc_char = buff[pos];
			int level = 0;
			wchar_t *str = &buff[pos];
			int match_found=0;			

			while( (str >= buff) && *str)
			{
				if( *str == inc_char )
					level++;
				if( *str == dec_char )
					level--;
				if( level == 0 )
				{
					int pos2 = str-buff;
					color[pos]|=HIGHLIGHT_MATCH<<8;
					color[pos2]|=HIGHLIGHT_MATCH<<8;
					match_found=1;
					break;
				}
				str+= step;
			}
			
			if( !match_found )
				color[pos] = HIGHLIGHT_ERROR<<8;
		}
	}
}

void highlight_universal( wchar_t * buff, 
						  int *color, 
						  int pos, 
						  array_list_t *error )
{
	int i;
	
	for( i=0; buff[i]; i++ )
		color[i] = 0;
	
	highlight_universal_internal( buff, color, pos, error );
}