// -- FILE ------------------------------------------------------------------
// name       : RtfInterpreter.cs
// project    : RTF Framelet
// created    : Leon Poyyayil - 2008.05.20
// language   : c#
// environment: .NET 2.0
// copyright  : (c) 2004-2008 by Itenso GmbH, Switzerland
// --------------------------------------------------------------------------
using System;
using Itenso.Rtf.Model;
using Itenso.Rtf.Support;

namespace Itenso.Rtf.Interpreter
{

	// ------------------------------------------------------------------------
	public sealed class RtfInterpreter : RtfInterpreterBase, IRtfElementVisitor
	{

		// ----------------------------------------------------------------------
		public RtfInterpreter( params IRtfInterpreterListener[] listeners )
			: base( listeners )
		{
			this.fontTableBuilder = new RtfFontTableBuilder( Context.WritableFontTable );
			this.colorTableBuilder = new RtfColorTableBuilder( Context.WritableColorTable );
			this.documentInfoBuilder = new RtfDocumentInfoBuilder( Context.WritableDocumentInfo );
			this.userPropertyBuilder = new RtfUserPropertyBuilder( Context.WritableUserProperties );
			this.imageBuilder = new RtfImageBuilder();
		} // RtfInterpreter

		// ----------------------------------------------------------------------
		public bool IsSupportedDocument( IRtfGroup rtfDocument )
		{
			try
			{
				GetSupportedDocument( rtfDocument );
			}
			catch ( RtfException )
			{
				return false;
			}
			return true;
		} // IsSupportedDocument

		// ----------------------------------------------------------------------
		public IRtfGroup GetSupportedDocument( IRtfGroup rtfDocument )
		{
			if ( rtfDocument == null )
			{
				throw new RtfException( "document is null" );
			}
			if ( rtfDocument.Contents.Count == 0 )
			{
				throw new RtfException( "document has not contents" );
			}
			IRtfElement firstElement = rtfDocument.Contents[ 0 ];
			if ( firstElement.Kind != RtfElementKind.Tag )
			{
				throw new RtfException( "first element in document is not a tag" );
			}
			IRtfTag firstTag = (IRtfTag)firstElement;
			if ( !RtfSpec.TagRtf.Equals( firstTag.Name ) )
			{
				throw new RtfException( "first tag in document is not " + RtfSpec.TagRtf );
			}
			if ( !firstTag.HasValue )
			{
				throw new RtfException( "unspecified RTF version" );
			}
			if ( firstTag.ValueAsNumber != RtfSpec.RtfVersion1 )
			{
				throw new RtfException( "unsupported RTF version: " + firstTag.ValueAsText );
			}
			return rtfDocument;
		} // GetSupportedDocument

		// ----------------------------------------------------------------------
		protected override void DoInterpret( IRtfGroup rtfDocument )
		{
			InterpretContents( GetSupportedDocument( rtfDocument ) );
		} // DoInterpret

		// ----------------------------------------------------------------------
		private void InterpretContents( IRtfGroup rtfDocument )
		{
			// by getting here we already know that the given document is supported, and hence
			// we know it has version 1
			Context.Reset(); // clears all previous content and sets the version to 1
			NotifyBeginDocument();
			VisitChildrenOf( rtfDocument );
			Context.State = RtfInterpreterState.Ended;
			NotifyEndDocument();
		} // InterpretContents

		// ----------------------------------------------------------------------
		private void VisitChildrenOf( IRtfGroup group )
		{
			bool pushedTextFormat = false;
			if ( Context.State == RtfInterpreterState.InDocument )
			{
				Context.PushCurrentTextFormat();
				pushedTextFormat = true;
			}
			try
			{
				foreach ( IRtfElement child in group.Contents )
				{
					child.Visit( this );
				}
			}
			finally
			{
				if ( pushedTextFormat )
				{
					Context.PopCurrentTextFormat();
				}
			}
		} // VisitChildrenOf

		// ----------------------------------------------------------------------
		void IRtfElementVisitor.VisitTag( IRtfTag tag )
		{
			if ( Context.State != RtfInterpreterState.InDocument )
			{
				if ( Context.FontTable.Count > 0 )
				{
					// somewhat of a hack to detect state change from header to in-document for
					// rtf-docs which do neither have a generator group nor encapsulate the
					// actual document content in a group.
					if ( Context.ColorTable.Count > 0 || RtfSpec.TagViewKind.Equals( tag.Name ) )
					{
						Context.State = RtfInterpreterState.InDocument;
					}
				}
			}

			switch ( Context.State )
			{
				case RtfInterpreterState.Init:
					if ( RtfSpec.TagRtf.Equals( tag.Name ) )
					{
						Context.State = RtfInterpreterState.InHeader;
						Context.RtfVersion = tag.ValueAsNumber;
					}
					else
					{
						throw new RtfException( "Init: illegal state for tag '" + tag + "'" );
					}
					break;
				case RtfInterpreterState.InHeader:
					switch ( tag.Name )
					{
						case RtfSpec.TagDefaultFont:
							Context.DefaultFontId = RtfSpec.TagFont + tag.ValueAsNumber.ToString();
							break;
					}
					break;
				case RtfInterpreterState.InDocument:
					switch ( tag.Name )
					{
						case RtfSpec.TagPlain:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveNormal();
							break;
						case RtfSpec.TagParagraphDefaults:
						case RtfSpec.TagSectionDefaults:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithAlignment( RtfTextAlignment.Left );
							break;
						case RtfSpec.TagBold:
							bool bold = !tag.HasValue || tag.ValueAsNumber != 0;
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithBold( bold );
							break;
						case RtfSpec.TagItalic:
							bool italic = !tag.HasValue || tag.ValueAsNumber != 0;
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithItalic( italic );
							break;
						case RtfSpec.TagUnderLine:
							bool underline = !tag.HasValue || tag.ValueAsNumber != 0;
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithUnderline( underline );
							break;
						case RtfSpec.TagUnderLineNone:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithUnderline( false );
							break;
						case RtfSpec.TagStrikeThrough:
							bool strikeThrough = !tag.HasValue || tag.ValueAsNumber != 0;
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithStrikeThrough( strikeThrough );
							break;
						case RtfSpec.TagHidden:
							bool hidden = !tag.HasValue || tag.ValueAsNumber != 0;
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithHidden( hidden );
							break;
						case RtfSpec.TagFont:
							string fontId = tag.FullName;
							if ( Context.FontTable.ContainsFontWithId( fontId ) )
							{
								Context.WritableCurrentTextFormat =
									Context.WritableCurrentTextFormat.DeriveWithFont(
										Context.FontTable[ fontId ] );
							}
							else
							{
								throw new RtfException( "undefined font: " + fontId );
							}
							break;
						case RtfSpec.TagFontSize:
							int fontSize = tag.ValueAsNumber;
							if ( fontSize > 0 )
							{
								Context.WritableCurrentTextFormat =
									Context.WritableCurrentTextFormat.DeriveWithFontSize( fontSize );
							}
							else
							{
								throw new RtfException( "invalid font size: " + fontSize );
							}
							break;
						case RtfSpec.TagFontSubscript:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithSuperScript( false );
							break;
						case RtfSpec.TagFontSuperscript:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithSuperScript( true );
							break;
						case RtfSpec.TagFontNoSuperSub:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithSuperScript( 0 );
							break;
						case RtfSpec.TagFontDown:
							int moveDown = tag.ValueAsNumber;
							if ( moveDown == -1 )
							{
								moveDown = 6; // the default value according to rtf spec
							}
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithSuperScript( -moveDown );
							break;
						case RtfSpec.TagFontUp:
							int moveUp = tag.ValueAsNumber;
							if ( moveUp == -1 )
							{
							  moveUp = 6; // the default value according to rtf spec
							}
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithSuperScript( moveUp );
							break;
						case RtfSpec.TagAlignLeft:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithAlignment( RtfTextAlignment.Left );
							break;
						case RtfSpec.TagAlignCenter:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithAlignment( RtfTextAlignment.Center );
							break;
						case RtfSpec.TagAlignRight:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithAlignment( RtfTextAlignment.Right );
							break;
						case RtfSpec.TagAlignJustify:
							Context.WritableCurrentTextFormat =
								Context.WritableCurrentTextFormat.DeriveWithAlignment( RtfTextAlignment.Justify );
							break;
						case RtfSpec.TagColorBackground:
						case RtfSpec.TagColorBackgroundWord:
						case RtfSpec.TagColorForeground:
							int colorIndex = tag.ValueAsNumber;
							if ( colorIndex >= 0 && colorIndex < Context.ColorTable.Count )
							{
								IRtfColor newColor = Context.ColorTable[ colorIndex ];
								bool isForeground = RtfSpec.TagColorForeground.Equals( tag.Name );
								Context.WritableCurrentTextFormat = isForeground ?
									Context.WritableCurrentTextFormat.DeriveWithForegroundColor( newColor ) :
									Context.WritableCurrentTextFormat.DeriveWithBackgroundColor( newColor );
							}
							else
							{
								throw new RtfException( "undefined color index: " + colorIndex );
							}
							break;
						case RtfSpec.TagSection:
							NotifyInsertBreak( RtfVisualBreakKind.Section );
							break;
						case RtfSpec.TagParagraph:
							NotifyInsertBreak( RtfVisualBreakKind.Paragraph );
							break;
						case RtfSpec.TagLine:
							NotifyInsertBreak( RtfVisualBreakKind.Line );
							break;
						case RtfSpec.TagPage:
							NotifyInsertBreak( RtfVisualBreakKind.Page );
							break;
						case RtfSpec.TagTabulator:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.Tabulator );
							break;
						case RtfSpec.TagTilde:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.NonBreakingSpace );
							break;
						case RtfSpec.TagEmDash:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.EmDash );
							break;
						case RtfSpec.TagEnDash:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.EnDash );
							break;
						case RtfSpec.TagEmSpace:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.EmSpace );
							break;
						case RtfSpec.TagEnSpace:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.EnSpace );
							break;
						case RtfSpec.TagQmSpace:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.QmSpace );
							break;
						case RtfSpec.TagBulltet:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.Bullet );
							break;
						case RtfSpec.TagLeftSingleQuote:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.LeftSingleQuote );
							break;
						case RtfSpec.TagRightSingleQuote:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.RightSingleQuote );
							break;
						case RtfSpec.TagLeftDoubleQuote:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.LeftDoubleQuote );
							break;
						case RtfSpec.TagRightDoubleQuote:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.RightDoubleQuote );
							break;
						case RtfSpec.TagHyphen:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.OptionalHyphen );
							break;
						case RtfSpec.TagUnderscore:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.NonBreakingHyphen );
							break;
					}
					break;
			}
		} // IRtfElementVisitor.VisitTag

		// ----------------------------------------------------------------------
		void IRtfElementVisitor.VisitGroup( IRtfGroup group )
		{
			string groupDestination = group.Destination;
			switch ( Context.State )
			{
				case RtfInterpreterState.Init:
					if ( RtfSpec.TagRtf.Equals( groupDestination ) )
					{
						VisitChildrenOf( group );
					}
					else
					{
						throw new RtfException( "Init: illegal state for group '" + groupDestination + "'" );
					}
					break;
				case RtfInterpreterState.InHeader:
					switch ( groupDestination )
					{
						case RtfSpec.TagFontTable:
							this.fontTableBuilder.VisitGroup( group );
							break;
						case RtfSpec.TagColorTable:
							this.colorTableBuilder.VisitGroup( group );
							break;
						case RtfSpec.TagGenerator:
							// last group with a destination in header, but no need to process its contents
							Context.State = RtfInterpreterState.InDocument;
							IRtfText generator = group.Contents.Count == 3 ? group.Contents[ 2 ] as IRtfText : null;
							if ( generator != null )
							{
								string generatorName = generator.Text;
								Context.Generator = generatorName.EndsWith( ";" ) ?
									generatorName.Substring( 0, generatorName.Length - 1 ) : generatorName;
							}
							else
							{
								throw new RtfException( "invalid generator group: " + group );
							}
							break;
						case null:
							// group without destination cannot be part of header, but need to process its contents
							Context.State = RtfInterpreterState.InDocument;
							if ( !group.IsExtensionDestination )
							{
								VisitChildrenOf( group );
							}
							break;
					}
					break;
				case RtfInterpreterState.InDocument:
					switch ( groupDestination )
					{
						case RtfSpec.TagUserProperties:
							this.userPropertyBuilder.VisitGroup( group );
							break;
						case RtfSpec.TagInfo:
							this.documentInfoBuilder.VisitGroup( group );
							break;
						case RtfSpec.TagUnicodeAlternativeChoices:
							IRtfGroup alternativeWithUnicodeSupport =
								group.SelectChildGroupWithDestination( RtfSpec.TagUnicodeAlternativeUnicode );
							if ( alternativeWithUnicodeSupport != null )
							{
								// there is an alternative with unicode formatted content -> use this
								VisitChildrenOf( alternativeWithUnicodeSupport );
							}
							else
							{
								// try to locate the alternative without unicode -> only ANSI fallbacks
								IRtfGroup alternativeWithoutUnicode = // must be the third element if present
									group.Contents.Count > 2 ? group.Contents[ 2 ] as IRtfGroup : null;
								if ( alternativeWithoutUnicode != null )
								{
									VisitChildrenOf( alternativeWithoutUnicode );
								}
							}
							break;
						case RtfSpec.TagHeader:
						case RtfSpec.TagHeaderFirst:
						case RtfSpec.TagHeaderLeft:
						case RtfSpec.TagHeaderRight:
						case RtfSpec.TagFooter:
						case RtfSpec.TagFooterFirst:
						case RtfSpec.TagFooterLeft:
						case RtfSpec.TagFooterRight:
						case RtfSpec.TagFootnote:
						case RtfSpec.TagStyleSheet:
							// groups we currently ignore, so their content doesn't intermix with
							// the actual document content
							break;
						case RtfSpec.TagPictureWrapper:
							VisitChildrenOf( group );
							break;
						case RtfSpec.TagPicture:
							this.imageBuilder.VisitGroup( group );
							NotifyInsertImage(
								this.imageBuilder.Format,
								this.imageBuilder.Width,
								this.imageBuilder.Height,
								this.imageBuilder.DesiredWidth,
								this.imageBuilder.DesiredHeight,
								this.imageBuilder.ScaleWidthPercent,
								this.imageBuilder.ScaleHeightPercent,
								this.imageBuilder.ImageDataHex );
							break;
						case RtfSpec.TagParagraphNumberText:
						case RtfSpec.TagListNumberText:
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.ParagraphNumberBegin );
							VisitChildrenOf( group );
							NotifyInsertSpecialChar( RtfVisualSpecialCharKind.ParagraphNumberEnd );
							break;
						default:
							if ( !group.IsExtensionDestination )
							{
								// nested text group
								VisitChildrenOf( group );
							}
							break;
					}
					break;
			}
		} // IRtfElementVisitor.VisitGroup

		// ----------------------------------------------------------------------
		void IRtfElementVisitor.VisitText( IRtfText text )
		{
			switch ( Context.State )
			{
				case RtfInterpreterState.Init:
					throw new RtfException( "Init: illegal state for text '" + text.Text + "'" );
				case RtfInterpreterState.InHeader:
					Context.State = RtfInterpreterState.InDocument;
					break;
				case RtfInterpreterState.InDocument:
					break;
			}
			NotifyInsertText( text.Text );
		} // IRtfElementVisitor.VisitText

		// ----------------------------------------------------------------------
		// members
		private readonly RtfFontTableBuilder fontTableBuilder;
		private readonly RtfColorTableBuilder colorTableBuilder;
		private readonly RtfDocumentInfoBuilder documentInfoBuilder;
		private readonly RtfUserPropertyBuilder userPropertyBuilder;
		private readonly RtfImageBuilder imageBuilder;

	} // class RtfInterpreter

} // namespace Itenso.Rtf.Interpreter
// -- EOF -------------------------------------------------------------------
