/*
------------------------------------------------------------------------------
          Licensing information can be found at the end of the file.
------------------------------------------------------------------------------

dialog.hpp - v0.1 - Loading and management of dialog definitions.

The dialog class provides functionality to load and manage dialog 
definitions. Any number of dialog files can be loaded, and the Dialog
class provides interface for iterating through all the loaded conversations,
and finding conversations by id.

The dialog definition files contain any number of "conversations". Each
conversation is made up of any number of entries. An entry can be any of the
following: a line of speech, an option, a redirect command, or an event 
command.

A conversation is defined by giving it an ID within square brackets ( [, ] ).
Two conversation IDs should be considered reserved: EXIT and RETURN. The 
first of these means that the dialog is over for now and should be stopped,
and the second means to return to the conversation from which we came (if 
there is no conversation to return to, the dialog should be stopped).

A line of speech consists of an actor name within angled brackets ( <, > ) 
followed by the actual ine.

An option (as may be selected by a player) starts with the character ^. If it
starts with two of that character, ^^, it means that the option is persistent
and thus shall not be removed (the normal behaviour is that options are 
removed from the list after being chosen once). After the ^ or ^^ is the line
which will be spoken if the option is chosen. Last, the line has the 
character @ followed by the ID of the conversation to jump to should that 
option be chosen.

A redirect command is simply the @ character followed by the ID of the 
conversation to jump to when execution reaches this command.

Finally, an event command is any string surrounded by # characters. This can
be used to send a custom message to the game engine, to signal that something
should happen.

Here follows an example dialog file from "The Strangler":

	[TalkToBambi]
	^ Sorry to *bother* you, ma'am. I need to ask you some *questions*. @YouAcop
	^ Well well well... ain't you a *classy* one. @Classy
	^ Excuse me, do you know a woman by the name of Amber? @Amber
	^^ I don't have any more questions right now. @EXIT

	[YouAcop]
	<Bambi> You a *cop* or something? Fuck off.
	<O'Reilly> *Believe* me, I hate those *bastards* just as much as you do.
	@RETURN

	[Classy]
	<Bambi> Errr.. No *offense*, pal, but you're not exactly one to *talk*.
	@RETURN

	[Amber]
	<Bambi> I knew Amber. But Amber is *dead*. 
	@RETURN

	[ShowPicture]
	<O'Reilly> Have you seen this man around?
	<Bambi> Yeah, I've seen him with some of the *girls*.
	<O'Reilly> Did you see him with Amber last night?
	<Bambi> I *did*, now that you *mention* it...
	<O'Reilly> Any idea where he might have taken her?
	<Bambi> Probably to this *motel*... that's where he'd usually go....	
	#UnlockMotel#

*/

#ifndef dialog_hpp
#define dialog_hpp

#include <string.h>


struct conversation_line
    {
    string_id actor;
    string line;
        
	private:
		friend struct conversation_entry;

		inline bool parse_row( char* row )
			{
			// A Line is defined like this:
			//     <ActorName>LineText

			char* start = row;
		    
			// If the row doesn't start with '<', it's not a valid line
			if( *start != '<' ) return false;
			++start; // Skip leading bracket
		    
			// Find end bracket
			char* end = start;
			while( *end != '>' && *end != 0 ) ++end;

			// If there's no ending '>', it's not a valid line
			if( (*end) != '>' ) return false;

			*end = 0; // Get rid of trailing bracket
			actor = start; // Store actor name
		    
			// Parse line text
			start = end + 1; // Skip past the 0 terminator of the actor name    
			while( *start > 0 && *start <= ' ' ) ++start; // Skip leading whitespace

			char* str = start;
			while( *str )
				{
				if( *str == '\\' && *( str + 1 ) == 'n' )
					{
					*str++ = '\n';
					*str = '\r';
					}
				++str;
				}

			line = start; // Store line text
		    
			// If we don't have both a valid actor and a valid line, this row is invalid
			if( actor == "" || line == "" ) return false;

			// This is a valid row
			return true;
			}

    };


struct conversation_option
    {
    string line;
    string_id target;
	bool is_persistent;

	private:
		friend struct conversation_entry;

		inline bool parse_row( char* row )
			{
			// An Option is defined like this:
			//     ^OptionText@TargetId
			//
			// Or, for a persistent option:
			//     ^^OptionText@TargetId

			char* start = row;

			// If the row doesn't start with '^', it's not a valid option
			if( *start != '^' ) return false;
			++start; // Skip leading '^'

			// If the second character is also '^', this is a persistent option
			if( *start == '^' ) { is_persistent = true; ++start; } // Skip second '^'          
			else is_persistent = false; // This is not a persistent option

			// Skip leading whitespace
			while( *start > 0 && *start <= ' ' ) ++start;
			
			// If the rest of the row is empty, it is not a valid option
			if( *start == 0 ) return false;

			// Find the '@' character
			char* end = start;
			while( *end != '@' && *end !=0 ) ++end;

			// If there isn't an '@' character, this is not a valid option
			if( *end == 0 ) return false;
			*end = 0; // Terminate string at the '@' character
		    
			// Trim trailing whitespace
			char* trim = end - 1;
			while( *trim <= ' ' ) *trim-- = 0;

			line = start; // Store option text
		    
			// Parse target id
			start = end + 1; // Skip the '@' character
			
			// Skip leading whitespace
			while( *start > 0 && *start <= ' ' ) ++start;
		    
			target = start; // Store target id
		    
			// If option text or target id are empty, this is not a valid option
			if( line == "" || target == "" ) return false;

			return true;
			}
			
			
    };


struct conversation_redirect
    {
    string_id target;				

	private:
		friend struct conversation_entry;

		inline bool parse_row( char* row )
			{
			// A Redirect is defined like this:
			//     @TargetId

			char* start = row;

			// If row doesn't start with '@', it is not a valid redirect
			if( *start != '@' ) return false;
			++start; // Skip the '@' character

			// Skip leading whitespace
			while( *start > 0 && *start <= ' ' ) ++start;

			target = start; // Store target id

			// If we don't have a valid target id, this is not a valid redirect
			if( target == "" ) return false;

			return true;
			}
    };


struct conversation_event
    {
    string_id event;
		
	private:
		friend struct conversation_entry;

		inline bool parse_row( char* row )
			{
			// An Event is defined like this:
			//     #eventName#

			char* start = row;

			// If the row doesn't start with '#', it's not a valid event
			if( *start != '#' ) return false;
			++start; // Skip leading '#'

			// Find trailing #
			char* end = start;
			while( *end != '#' && *end !=0 ) ++end;

			// If there's no trailing '#', this is not a valid event
			if( *end != '#' ) return false;

			*end = 0; // Terminate string before trailing '#'
			event = start; // Store event string

			// If we don't have a valid event name, it's not a valid event
			if( event == "" ) return false;

			// Indicate that this is a valid event
			return true;			
			}
    };


struct conversation_entry
    {
    enum entry_type
	    {
        ENTRY_TYPE_LINE,
        ENTRY_TYPE_OPTION,
        ENTRY_TYPE_REDIRECT,
        ENTRY_TYPE_EVENT,
		};
		
    entry_type type; 
	string_id id;
	conversation_line line; 
	conversation_option option;
	conversation_redirect redirect;
	conversation_event event;

	private:
		friend struct conversation;

		inline bool parse_row( char* row )
			{
			size_t length = strlen( row );

			// Strip id (text defined {like_this} is an id) from the start of the row
			if( *row == '{' && strchr( row, '}' ) != 0 )
				{
				const char* str = ++row;
				while( *row && *row != '}' ) ++row;
				*row = 0;
				id = str;
				++row;
				while( *row && *row <= ' ' ) ++row;
				}

			// Check if this is a line
			if( *row == '<' )
				{
				type = ENTRY_TYPE_LINE;
				return line.parse_row( row );
				}

			// Check if this is an option
			else if( *row == '^' )
				{
				type = ENTRY_TYPE_OPTION;
				return option.parse_row( row );
				}

			// Check if this is a redirect
			else if( *row == '@' )
				{
				type = ENTRY_TYPE_REDIRECT;
				return redirect.parse_row( row );
				}

			// Check if this is an event
			else if( *row == '#' && row[ length -1 ] == '#' && length > 2 )
				{
				type = ENTRY_TYPE_EVENT;
				return event.parse_row( row );
				}

			// Not a valid row
			else
				{
				return false;
				}
			}
		
    };


struct conversation
    {
    string_id id;
    array<conversation_entry> entries;
	
	private:
		friend struct dialog;

		inline bool parse_row( char* row )
			{
			// Block definition?
			size_t length = strlen( row );
			if( *row == '[' && row[ length - 1 ] == ']' )
				{        
				if( id == "" ) // We only deal with the id if this is a new block
					{
					row[ length - 1] = 0; // Get rid of trailing bracket
					char* id_str = row + 1; // Get rid of leading bracket
					id = id_str; // Store id
					return true; // Indicate that row was parsed ok
					}

				// We have reached the end of the conversation 
				return false; // Indicate that row has not been parsed (new conversation)
				}

			// Entry
			conversation_entry entry;
			if( entry.parse_row( row ) ) // Parse the row as an entry
				entries.add( entry ); // If row was successfully parsed, add it to entry list

			return true; // Indicate that row was parsed ok
			}
    };


struct dialog
    {
    inline int conversation_count() const
		{
		return conversations_.count();
		}


	inline conversation const& conversation_item( int index ) const
		{
		return conversations_.item( index );
		}


	inline conversation* find_conversation( string_id const& id )
		{
		return conversations_.find( id );
		}


	inline void load_dialog( string const& filename )
		{
		ref<binary> bin = bload( filename );
		PIXIE_ASSERT( bin, "Failed to load dialog" );
		if( bin )
			{
			parse( (char*)bin->data, (int)bin->size );
			}
		}


	inline void remove_all_conversations()
		{
		conversations_.clear();
		}


	private:
        inline void parse( char* buffer, int size )
			{
			conversation current;

			// extract rows
			for( char* ptr = buffer; ptr - buffer < size; ++ptr )
				{
				char* row_start = ptr;
				
				// skip forward until next line break
				while( *ptr != '\n' && ptr - buffer < size ) ++ptr;

				// only bother with non-zero length rows
				if( ptr - row_start > 1 )
					{
					// add null terminator at the end of the row
					char* row = row_start;
					row[ ptr - row_start - 1 ] = 0; 

					// skip leading whitespace
					char* start = row;
					while ( *start > 0 && *start <= ' ' )  ++start;					
					row = start;

					// skip trailing whitespace
					start = row;
					char* end = row + strlen( row );
					while( *end <= ' ' && end > start ) *end-- = 0; 
					++end;
					
					// only bother with non-zero length rows
					if( end - start > 0 )
						{
						// parse row
						bool parse_result = current.parse_row( row );
						if( !parse_result ) // if we're on a new conversation
							{
							// new conversation, so insert the current one into the list if it is valid
							if( current.id != "" ) conversations_[ current.id ] = current;

							// and then start a new conversation
							current = conversation();
							current.parse_row( row );
							}
						}
					}
				}
			
			// if the current conversation is valid, add it to the list
			if( current.id != "" ) conversations_[ current.id ] = current;
			}


        dictionary<string_id, conversation> conversations_;
    };


#endif /* dialog.hpp */

/*
------------------------------------------------------------------------------

This software is available under 2 licenses - you may choose the one you like.

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

ALTERNATIVE A - MIT License

Copyright (c) 2015 Mattias Gustavsson

Permission is hereby granted, free of charge, to any person obtaining a copy of 
this software and associated documentation files (the "Software"), to deal in 
the Software without restriction, including without limitation the rights to 
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do 
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.

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

ALTERNATIVE B - Public Domain (www.unlicense.org)

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 
software, either in source code form or as a compiled binary, for any purpose, 
commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this 
software dedicate any and all copyright interest in the software to the public 
domain. We make this dedication for the benefit of the public at large and to 
the detriment of our heirs and successors. We intend this dedication to be an 
overt act of relinquishment in perpetuity of all present and future rights to 
this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

------------------------------------------------------------------------------
*/
