/*

args_4: Command line arguments parsing library
Yauheni A. Akhotnikau (C) 2002-2003
eao197@yahoo.com
-------------------------------------------------

Permission is granted to anyone to use this software for any purpose on any
computer system, and to redistribute it freely, subject to the following
restrictions:

1. This software is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

2. The origin of this software must not be misrepresented, either by
   explicit claim or by omission.

3. Altered versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

-------------------------------------------------

*/
/*!
	Args-4
*/

#include <cstring>
#include <cctype>
#include <cassert>
#include <cerrno>
#include <cstdarg>

#include <args_4/h/args.hpp>

namespace args_4
{

static char	g_chars_to_ignore[] = "\r";
static char g_eol_chars[] = "\n";

const int	c_initial_word_capacity = 32;

static bool
is_eol_( char ch ) {
	return ( 0 != strchr( g_eol_chars, ch ) );
}

static bool
is_char_must_be_ignored_( char ch ) {
	return ( 0 != strchr( g_chars_to_ignore, ch ) );
}

/*
** error_stream_t
*/
error_stream_t::error_stream_t() {
}

error_stream_t::~error_stream_t() {
}

error_stream_t &
error_stream_t::out( const char * /*str*/ ) {
	return *this;
}

/*
** word_container_t
*/
word_container_t::word_container_t( void ) :
	m_word( 0 ),
	m_word_capacity( c_initial_word_capacity ),
	m_word_len( 0 ),
	m_prev_word_returned( false ) {
	m_word = new char[ m_word_capacity ];
	m_word[ m_word_len ] = 0;
}

word_container_t::~word_container_t( void ) {
	if( m_word )
		delete[] m_word;
}

const char *	word_container_t::next(
	int & error_code,
	error_stream_t * error_stream ) {
	assert( 0 != m_word );

	if( m_prev_word_returned ) {
		error_code = c_ok;
		m_prev_word_returned = false;
		return m_word;
	}

	m_error_stream = error_stream;

	// Clear last word
	m_word[ m_word_len = 0 ] = 0;

	char ch;
	bool is_quote_found = false;
	while( 1 ) {
		if( c_ok == ( error_code = getch( ch ) ) ) {
			if( isspace( ch ) ) {
				if( m_word_len ) {
					if( !is_quote_found )
						// We can return word
						return m_word;
					if( is_eol_( ch ) )
						// String cannot be continued on new line
						return err_new_line_in_string( error_code );
					// Space must be added to string
					expand( ch );
				}
				// Waiting for start word or string
			}
			else {
				if( '\"' == ch ) {
					if( is_quote_found )
						// We must return string
						return ( m_word_len ? m_word :
							err_empty_string( error_code ) );
					else
						if( m_word_len )
							return err_quote_in_word( error_code );
						else
							is_quote_found = true;
				}
				else
					if( '\\' == ch ) {
						// We must look next char
						char ch_next;
						if( c_ok == ( error_code =
							getch( ch_next ) ) ) {
							if( '\\' == ch_next ||
								'\"' == ch_next )
								expand( ch_next );
							else if( !is_eol_( ch_next ) ) {
								// This is unrecognized combination
								// Simple copy it
								expand( ch );
								expand( ch_next );
							}
						}
						else
							if( c_eof == error_code )
								return err_invalid_back_slash_seq(
									error_code );
							else
								break;
					}
					else
						expand( ch );
			}
		}
		else {
			if( c_eof == error_code && m_word_len ) {
				error_code = c_ok;
				return m_word;
			}
			break;
		}
	}

	return 0;
}

void	word_container_t::back( void ) {
	m_prev_word_returned = true;
}

// Expand m_word if needed
void	word_container_t::expand( char ch ) {
	if( m_word_len + 2 > m_word_capacity ) {
		m_word_capacity <<= 1;
		char * new_word = new char[ m_word_capacity ];
		strcpy( new_word, m_word );
		delete[] m_word;
		m_word = new_word;
	}
	register char * p = m_word + (m_word_len++);
	*(p++) = ch;
	*p = 0;
}

// Pop-up next char. If no char available,
// then c_eof must be returned
int	word_container_t::getch( char & /*ch*/ ) {
	return c_eof;
}

const char *
word_container_t::err_new_line_in_string(
	int & error_code ) {
	error_code = c_new_line_in_string;

	if( m_error_stream ) {
		m_error_stream->out(
			"string cannot be continued on next line\n" );
	}

	return 0;
}

const char *
word_container_t::err_empty_string(
	int & error_code ) {
	error_code = c_empty_string;

	if( m_error_stream ) {
		m_error_stream->out( "string cannot be empty\n" );
	}

	return 0;
}

const char *
word_container_t::err_quote_in_word(
	int & error_code ) {
	error_code = c_quote_in_word;

	if( m_error_stream ) {
		m_error_stream->out(
			"symbol '\"' cannot appears in word\n" );
	}

	return 0;
}

const char *
word_container_t::err_invalid_back_slash_seq(
	int & error_code ) {
	error_code = c_invalid_back_slash_seq;

	if( m_error_stream ) {
		m_error_stream->out(
			"there isn't any symbol after back "
			"slash ('\\')\n" );
	}

	return 0;
}

/*
** words_from_str_t
*/
words_from_str_t::words_from_str_t(
	const char * str ) :
	m_cur( str ) {
	assert( m_cur );
}

words_from_str_t::~words_from_str_t() {
}

int	words_from_str_t::getch( char & ch ) {
	while( *m_cur ) {
		if( !is_char_must_be_ignored_( ch = *(m_cur++) ) )
			return c_ok;
	}
	return c_eof;
}

/*
** words_from_file_t
*/
const size_t	c_file_buffer_capacity = 2048;
words_from_file_t::words_from_file_t(
	const char * file_name ) :
	m_file_name( 0 ),
	m_file( 0 ),
	m_buf( 0 ),
	m_size( 0 ),
	m_pos( 0 ),
	m_line( 1 ),
	m_errno( 0 ) {
	assert( file_name );

	m_file_name = new char[ strlen( file_name ) + 1 ];
	assert( m_file_name );
	strcpy( m_file_name, file_name );

	m_file = fopen( m_file_name, "r" );
	if( 0 != m_file ) {
		m_buf = new char[ c_file_buffer_capacity ];
		if( 0 == m_buf ) {
			fclose( m_file );
			m_file = 0;
		}
	}
	else
		m_errno = errno;
}

words_from_file_t::~words_from_file_t( void ) {
	if( 0 != m_buf )
		delete[] m_buf;
	if( 0 != m_file )
		fclose( m_file );
	if( 0 != m_file_name )
		delete[] m_file_name;
}

int	words_from_file_t::query_last_io_result() const {
	return m_errno;
}

const char *
words_from_file_t::query_file_name() const {
	return m_file_name;
}

unsigned int
words_from_file_t::query_line() const {
	return m_line;
}

int	words_from_file_t::getch( char & ch ) {
	register char cur = 0;
	register int err;
	while( 1 ) {
		if( c_ok == ( err = lo_get_ch( cur ) ) ) {
			if( cur && !is_char_must_be_ignored_( cur ) ) {
				ch = cur;
				return c_ok;
			}
		}
		else
			return err;
	}
}

int	words_from_file_t::lo_get_ch( char & ch ) {
	if( m_pos == m_size ) {
		m_pos = 0;
		m_size = 0;

		if( 0 == m_file || ferror( m_file ) )
			return c_io_error;
		else if( feof( m_file ) )
			return c_eof;
		m_size = fread( m_buf, 1, c_file_buffer_capacity, m_file );
		if( !m_size ) {
			m_errno = errno;
			return c_eof;
		}
	}
	ch = *(m_buf + m_pos++);

	if( is_eol_( ch ) )
		++m_line;

	return c_ok;
}

void	words_from_file_t::lo_back_ch( void ) {
	if( is_eol_( m_buf[ m_pos ] ) )
		--m_line;
	m_pos--;
}

/*
** words_from_file_with_simple_comment_t
*/
words_from_file_with_simple_comment_t::
words_from_file_with_simple_comment_t(
	const char * file_name,
	char comment_char ) :
	words_from_file_t( file_name ),
	m_state( e_new_line ),
	m_comment_char_set( 0 ) {

	m_comment_char_set = new char[ 2 ];
	m_comment_char_set[ 0 ] = comment_char;
	m_comment_char_set[ 1 ] = 0;
}

words_from_file_with_simple_comment_t::
words_from_file_with_simple_comment_t(
	const char * file_name,
	const char * comment_char_set ) :
	words_from_file_t( file_name ),
	m_state( e_new_line ),
	m_comment_char_set( 0 ) {
	assert( comment_char_set );

	m_comment_char_set = new char[ 1 + strlen( comment_char_set ) ];
	strcpy( m_comment_char_set, comment_char_set );
}

words_from_file_with_simple_comment_t::
~words_from_file_with_simple_comment_t() {
}

int
words_from_file_with_simple_comment_t::
getch( char & ch ) {
	register int result = words_from_file_t::getch( ch );
	while( c_ok == result ) {
		if( is_eol_( ch ) ) {
			m_state = e_new_line;
			break;
		}
		else if( e_normal == m_state )
			break;
		else if( e_new_line == m_state ) {
			if( strchr( m_comment_char_set, ch ) )
				m_state = e_comment;
			else if( !isspace( ch ) ) {
				m_state = e_normal;
				break;
			}
		}

		result = words_from_file_t::getch( ch );
	}

	return result;
}

/*
** words_from_argv_t
*/
words_from_argv_t::words_from_argv_t(
	int argc, char ** argv ) :
	m_argc( argc ),
	m_argv( argv ),
	m_cur( 1 ) {
}

words_from_argv_t::~words_from_argv_t( void ) {
}

const char *	words_from_argv_t::next(
	int & error_code,
	error_stream_t * error_stream ) {

	if( m_prev_word_returned ) {
		m_cur--;
		m_prev_word_returned = false;
	}

	if( m_cur < m_argc ) {
		error_code = c_ok;
		return m_argv[ m_cur++ ];
	}

	error_code = c_eof;
	return 0;
}

int	words_from_argv_t::query_argc( void ) const {
	return m_argc;
}

/*
** cmd_t
*/
cmd_t::cmd_t() :
	m_name( 0 ) {
}

cmd_t::cmd_t( const char * name ) {
	if( 0 != name ) {
		m_name = new char[ strlen( name ) + 1 ];
		strcpy( m_name, name );
	}
}

cmd_t::~cmd_t( void ) {
	if( 0 != m_name ) {
		delete[] m_name;
	}
}

bool	cmd_t::compare_name( const char * name ) const {
	if( 0 != m_name && 0 != name )
		return ( 0 == strcmp( m_name, name ) );
	else
		return false;
}

int	cmd_t::process_params(
	word_container_t & /*words*/,
	error_stream_t * /*error_stream*/ ) {
	return c_ok;
}

const char *	cmd_t::query_name() const {
	return ( m_name ? m_name : "<no_name>" );
}

// Command is not mandatory by default
bool	cmd_t::is_mandatory( void ) const {
	return false;
}

// Command is not defined by default
bool	cmd_t::is_defined( void ) const {
	return true;
}

// Pop-up next mandatory parameter
const char *	cmd_t::next_mandatory_word(
	word_container_t & words,
	error_stream_t * error_stream,
	int & error_code ) {

	const char * pcsz = words.next( error_code,
		error_stream );
	if( ! pcsz ) {
		if( c_eof == error_code ) {
			if( error_stream ) {
				error_stream->out( "missed value for \"" ).
					out( query_name() ).
					out( "\"\n" );
			}
			error_code = c_value_missed;
		}
	}
	else
		error_code = c_ok;

	return pcsz;
}

/*
** cmd_parser_t
*/
typedef cmd_t * cmd_ptr_t;
cmd_parser_t::cmd_parser_t(
	bool is_word_backing_enabled ) :
	m_list_cmd( 0 ),
	m_list_len( 0 ),
	m_list_capacity( 8 ),
	m_is_word_backing_enabled( is_word_backing_enabled ),
	m_error_stream( 0 ) {
	m_list_cmd = new cmd_ptr_t[ m_list_capacity ];
}

cmd_parser_t::~cmd_parser_t() {
	if( 0 != m_list_cmd ) {
		delete[] m_list_cmd;
	}
}

void	cmd_parser_t::add_cmd( cmd_t * pcmd ) {
	if( m_list_len + 1 >= m_list_capacity ) {
		cmd_t ** new_list = new cmd_ptr_t[
			m_list_capacity <<= 1 ];
		memcpy( new_list, m_list_cmd, sizeof( cmd_t* ) *
			m_list_len );
		delete[] m_list_cmd;
		m_list_cmd = new_list;
	}
	m_list_cmd[ m_list_len++ ] = pcmd;
}

typedef cmd_t * cmd_ptr_t;
void	cmd_parser_t::add_cmds( cmd_t * p1, ... ) {
	va_list	l;
	va_start( l, p1 );
	add_cmd( p1 );

	while( true ) {
		cmd_ptr_t ptr = va_arg( l, cmd_ptr_t );
		if( ptr )
			add_cmd( ptr );
		else
			break;
	}
	va_end( l );
}

int	cmd_parser_t::parse(
	word_container_t & words,
	error_stream_t * error_stream ) {
	assert( 0 != m_list_cmd );

	m_error_stream = error_stream;
	int error;

	do
	{
		const char * cmd_name = words.next( error,
			m_error_stream );
		if( cmd_name ) {
			if( c_ok != ( error = process_cmd( cmd_name, words ) ) )
				return error;
		}
		else
			if( c_eof != error )
				return error;
	} while ( c_eof != error );

	return check_mandatory_args();
}

// Search handler for specified command
cmd_t *	cmd_parser_t::find_cmd_by_name(
	const char * cmd_name ) {

	for( register size_t i = 0; i < m_list_len; ++i )
		if( m_list_cmd[ i ]->compare_name( cmd_name ) )
			return m_list_cmd[ i ];

	return 0;
}

// Try to find and process specified command
int	cmd_parser_t::process_cmd(
	const char * cmd_name,
	word_container_t & words ) {

	cmd_t * pcmd = find_cmd_by_name( cmd_name );
	if( 0 != pcmd )
		return pcmd->process_params( words, m_error_stream );
	else {
		if( m_is_word_backing_enabled ) {
			words.back();
		}
		else if( m_error_stream ) {
			m_error_stream->out( "unknown cmd \"" ).
				out( cmd_name ).out( "\"\n" );
		}
		return c_unknown_cmd;
	}
}

// All mandatory args must be defined
int	cmd_parser_t::check_mandatory_args( void ) {
	bool header_printed = false;
	for( register size_t i = 0; i < m_list_len; ++i ) {
		register cmd_t * pcmd = m_list_cmd[ i ];
		if( pcmd->is_mandatory() && !pcmd->is_defined() ) {
			// Mandatory argument is not defined
			if( m_error_stream ) {
				if( !header_printed )
					m_error_stream->out(
						"\tfollowing mandatory "
						"argument(s) is not present:\n" );
				m_error_stream->out( "\t\t" ).
					out( pcmd->query_name() ).
					out( "\n" );
			}
			header_printed = true;
		}
	}

	if( header_printed ) {
		return c_mandatory_cmd_missed;
	}
	else
		return c_ok;
}

/*
** cmd_str_value_t
*/
cmd_str_value_t::cmd_str_value_t(
	const char * name,
	bool mandatory ) :
	cmd_t( name ),
	m_is_mandatory( mandatory ),
	m_is_defined( false ),
	m_value( 0 ) {
}

cmd_str_value_t::~cmd_str_value_t() {
	if( 0 != m_value ) {
		delete[] m_value;
	}
}

int	cmd_str_value_t::process_params(
	word_container_t & words,
	error_stream_t * error_stream ) {
	if( m_is_defined ) {
		if( error_stream ) {
			error_stream->out( "\"" ).out( query_name() ).
				out( "\" is already defined\n" );
		}
		return c_cmd_already_defined;
	}

	int error_code;
	const char * pcsz = next_mandatory_word(
		words, error_stream, error_code );
	if( ! pcsz )
		return error_code;

	m_value = new char[ strlen( pcsz ) + 1 ];
	strcpy( m_value, pcsz );
	m_is_defined = true;
	return c_ok;
}

bool	cmd_str_value_t::is_mandatory( void ) const {
	return m_is_mandatory;
}

bool	cmd_str_value_t::is_defined( void ) const {
	return m_is_defined;
}

const char *
cmd_str_value_t::query_value( void ) const {
	return m_value;
}

static bool
check_hex_digits( const char * p ) {
	static char digits[] = "0123456789abcdefABCDEF";
	while( *p ) {
		if( !strchr( digits, *p ) )
			return false;
		++p;
	}

	return true;
}

static bool
check_oct_digits( const char * p ) {
	static char digits[] = "01234567";
	while( *p ) {
		if( !strchr( digits, *p ) )
			return false;
		++p;
	}

	return true;
}

static bool
check_dec_digits( const char * p ) {
	static char digits[] = "0123456789";
	while( *p ) {
		if( !strchr( digits, *p ) )
			return false;
		++p;
	}

	return true;
}

/*
** cmd_uint_value_t
*/
cmd_uint_value_t::cmd_uint_value_t(
	const char * name,
	bool mandatory ) :
	cmd_t( name ),
	m_value( 0 ),
	m_is_mandatory( mandatory ),
	m_is_defined( false ) {
}
cmd_uint_value_t::~cmd_uint_value_t() {
}

int	cmd_uint_value_t::process_params(
	word_container_t & words,
	error_stream_t * error_stream ) {
	if( m_is_defined ) {
		if( error_stream ) {
			error_stream->out( "\"" ).out( query_name() ).
				out( "\" is already defined\n" );
		}
		return c_cmd_already_defined;
	}

	int error_code;
	const char * pcsz = next_mandatory_word(
		words, error_stream, error_code );
	if( !pcsz )
		return error_code;

	if( '0' == *pcsz ) {
		// This is hexdecimal or octal value
		if( 'X' == toupper( pcsz[ 1 ] ) ) {
			// This is hexdecimal value
			if( check_hex_digits( pcsz + 2 ) &&
				1 == sscanf( pcsz + 2, "%x", &m_value ) )
				m_is_defined = true;
		}
		else
			if( check_oct_digits( pcsz ) &&
				1 == sscanf( pcsz, "%o", &m_value ) )
				m_is_defined = true;
	}
	else
		if( check_dec_digits( pcsz ) &&
			1 == sscanf( pcsz, "%u", &m_value ) )
			m_is_defined = true;

	if( ! m_is_defined ) {
		if( error_stream ) {
			error_stream->out( "invalid value for \"" ).
				out( query_name() ).out( "\"\n" );
		}
		return c_invalid_value;
	}
	return c_ok;
}

bool	cmd_uint_value_t::is_mandatory( void ) const {
	return m_is_mandatory;
}

bool	cmd_uint_value_t::is_defined( void ) const {
	return m_is_defined;
}

unsigned int
cmd_uint_value_t::query_value( void ) const {
	return m_value;
}

/*
** cmd_on_off_switch_t
*/
cmd_on_off_switch_t::cmd_on_off_switch_t(
	const char * name,
	bool mandatory ) :
	cmd_t( name ),
	m_is_mandatory( mandatory ),
	m_is_defined( false ) {
}
cmd_on_off_switch_t::~cmd_on_off_switch_t( void ) {
}

int	cmd_on_off_switch_t::process_params(
	word_container_t & words,
	error_stream_t * error_stream ) {
	if( m_is_defined ) {
		if( error_stream ) {
			error_stream->out( "\"" ).out( query_name() ).
				out( "\" is already defined\n" );
		}
		return c_cmd_already_defined;
	}

	m_is_defined = true;
	return c_ok;
}

bool	cmd_on_off_switch_t::is_mandatory( void ) const {
	return m_is_mandatory;
}

bool	cmd_on_off_switch_t::is_defined( void ) const {
	return m_is_defined;
}

/*
** cmd_strvector_value_t
*/
typedef char * char_ptr_t;
cmd_strvector_value_t::cmd_strvector_value_t(
	const char * name,
	bool mandatory ) :
	cmd_t( name ),
	m_vector_len( 0 ),
	m_vector_capacity( 8 ),
	m_is_mandatory( mandatory ),
	m_values( 0 ) {
	m_values = new char_ptr_t[ m_vector_capacity ];
}

cmd_strvector_value_t::~cmd_strvector_value_t() {
	if( m_values ) {
		for( register size_t i = 0; i < m_vector_len; ++i ) {
			delete[] m_values[ i ];
		}
		delete[] m_values;
	}
}

int	cmd_strvector_value_t::process_params(
	word_container_t & words,
	error_stream_t * error_stream ) {

	int error_code;
	const char * pcsz = next_mandatory_word(
		words, error_stream, error_code );
	if( !pcsz )
		return error_code;

	char * value = new char[ strlen( pcsz ) + 1 ];
	strcpy( value, pcsz );
	if( m_vector_len >= m_vector_capacity ) {
		char ** new_vector = new char_ptr_t[
			m_vector_capacity <<= 1 ];
		memcpy( new_vector, m_values, m_vector_len *
			sizeof( char_ptr_t ) );
		delete[] m_values;
		m_values = new_vector;
	}
	m_values[ m_vector_len++ ] = value;

	return c_ok;
}

bool	cmd_strvector_value_t::is_mandatory() const {
	return m_is_mandatory;
}

bool	cmd_strvector_value_t::is_defined() const {
	return ( m_vector_len > 0 );
}

size_t	cmd_strvector_value_t::query_value_count() const {
	return m_vector_len;
}

const char *
cmd_strvector_value_t::query_value(
	size_t index ) const {
	assert( index < m_vector_len );

	return m_values[ index ];
}

/*
** cmd_response_file_t
*/
cmd_response_file_t::cmd_response_file_t(
	char first_char ) :
	cmd_strvector_value_t( "<response file>", false ),
	m_first_char( first_char ) {
}

cmd_response_file_t::~cmd_response_file_t() {
}

bool
cmd_response_file_t::compare_name(
	const char * name ) const {
	return ( m_first_char == *name );
}

int
cmd_response_file_t::process_params(
	word_container_t & words,
	error_stream_t * error_stream ) {
	int error_code;

	words.back();
	const char * word = words.next( error_code, error_stream );
	words_from_str_t name( word + 1 );
	return cmd_strvector_value_t::process_params( name,
		error_stream );
}

} /* namespace args_4 */

