/*******************************************************************************
 * This file is part of GECAMed.
 * 
 * GECAMed is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License (L-GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GECAMed 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.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License (L-GPL)
 * along with GECAMed.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * GECAMed is Copyrighted by the Centre de Recherche Public Henri Tudor (http://www.tudor.lu)
 * (c) CRP Henri Tudor, Luxembourg, 2008
 *******************************************************************************/
package lu.tudor.santec.gecamed.core.gui.controller.document.word;

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import lu.tudor.santec.gecamed.core.gui.controller.data.HtmlTransferable;
import lu.tudor.santec.gecamed.core.gui.controller.document.DocumentController;
import lu.tudor.santec.gecamed.core.gui.controller.document.UnsupportedFileTypeException;
import lu.tudor.santec.gecamed.letter.gui.placeholders.PlaceholdersConfig;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jfree.util.Log;

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.ComFailException;
import com.jacob.com.Dispatch;
import com.jacob.com.DispatchEvents;
import com.jacob.com.Variant;

//A controller for Microsoft Office Word 97 - 2007
public class WordController extends DocumentController 
{
	/* ======================================== */
	// 		CONSTANTS
	/* ======================================== */
	
	private static final String		DOCUMENT_EXTENSION		= ".doc";
	
	private static final String		DOCUMENT_TEMPLATE_PATH	= "resources/template.doc";
	
	private static final String		HTML					= "<html>";
	
	/**
	 * SeekViews:
	 *  0 = MainDocument
	 *  1 = PrimaryHeader
	 *  2 = FirstPageHeader
	 *  3 = EvenPageHeader
	 *  4 = PrimaryFooter
	 *  5 = FirstPageFooter
	 *  6 = EvenPageFooter
	 *  7 = Footnotes
	 *  8 = Endnotes
	 *  9 = CurrentPageHeader
	 * 10 = CurrentPageFooter
	 */
	private static final int		MAX_VIEW_INDEX		= 10;
	
	private static final Variant	TRUE 				= new Variant(true);
	private static final Variant	FALSE 				= new Variant(false);
	private static final Variant	ZERO				= new Variant(0);
	private static final Variant	ONE					= new Variant(1);
	private static final Variant	SEARCH_NEW_STYLE	= new Variant("\\[[A-Z0-9_]@\\]");
	private static final Variant	SEARCH_OLD_STYLE	= new Variant("<[A-Z_][A-Z0-9_]@>");
	
	
	
	/* ======================================== */
	// 		MEMBERS
	/* ======================================== */
	
	private static Logger	logger		= Logger.getLogger(WordController.class.getName());
	
	
	private Dispatch			dDoc;
	
	private WordListener		wListener;
	
	private Dispatch			dWord;
	
	private ActiveXComponent	oWord;
	
	private Clipboard 			clipboard 	= Toolkit.getDefaultToolkit().getSystemClipboard();
	
	private HtmlTransferable 	transferable= new HtmlTransferable();
	
	
	
	/* ======================================== */
	// 		CONSTRUCTORS
	/* ======================================== */
	
	public WordController ()
	{
		super();
	}
	
	
	
	/* ======================================== */
	// 		IMPLEMENTED METHODS
	/* ======================================== */
	
	protected void open()
	{
		if (super.type == DocumentController.TYPE_OPEN_TEMPLATE);
			// Initialise the manager, as it cannot be initialised from Word context
			PlaceholdersConfig.getManager();
		
		// Opens a document, first a document has to be set
		// Connecting Word over JACOB/COM an open it
		oWord = new ActiveXComponent("Word.Application");
		dWord = oWord.getObject();
		oWord.setProperty("Visible", openedHidden ? FALSE : TRUE);

		// Getting the path of the local file
		String file = this.file.getPath();

		// Getting the property "Documents", to add a new document
		Dispatch oDocuments = oWord.getProperty("Documents").toDispatch();

		// Opens the document
		if (!file.equalsIgnoreCase("")) {
			try 
			{
				dDoc = Dispatch.call(oDocuments, "Open", file).toDispatch();
			}
			catch (ComFailException e)
			{
				logger.log(Level.WARN, "Word may have problems to open this document.", e);
				throw new UnsupportedFileTypeException(e);
			}
		}

		// Setting listener for the state class and the quit event.
		wListener = new WordListener(getState(), dDoc, this);
		addListener(wListener);
	}
	
	
	public void print ()
	{
		Dispatch.call(dDoc, "PrintOut");
		
		if (openedHidden)
			closeAndDeleteFile();
	}
	
	
	@Override
	public void saveAsPDF (File pdfFile)
	{
//		Dispatch.call(dDoc, "ExportAsFixedFormat", 
//				pdfFile.getAbsolutePath(),
//				new Variant(17)); // PDF = 17, XPS = 18
		
		Dispatch.call(dDoc, "ExportAsFixedFormat", 
				pdfFile.getAbsolutePath(),	// OutputFileName
				new Variant(17),			// ExportFormat := wdExportFormatPDF
				FALSE,	// OpenAfterExport
				ZERO,	// OptimizeFor := wdExportOptimizeForPrint
				ZERO,	// Range := wdExportAllDocument
				ONE,	// From
				ONE,	// To
				ZERO,	// Item := wdExportDocumentContent
				TRUE,	// IncludeDocProps
				TRUE,	// KeepIRM
				ZERO,	// CreateBookmarks := wdExportCreateNoBookmarks
				TRUE,	// DocStructureTags
				TRUE,	// BitmapMissingFonts
				TRUE);	// UseISO19005_1
	}
	
	
	public void append (String text) 
	{
		Dispatch oSelection = oWord.getProperty("Selection").toDispatch();
		Dispatch.call(oSelection, "EndKey", 6);
		Dispatch.put(oSelection, "Text", text);
		Dispatch.call(oSelection, "Collapse");
	}
	
	
	public static boolean isWordInstalled () throws UnsatisfiedLinkError
	{
		ActiveXComponent axc = null;
		try {
			axc = new ActiveXComponent("Word.Application");
		} catch (Exception e) {}
		
		return axc != null;
	}
	
	
	public void checkIfContainsContactInfo ()
	{
		Dispatch		activeWin;
		Dispatch		selection;
		Dispatch		find;
		Dispatch		activePane;
		Dispatch		view;
		List<String>	contactKeys;
		String			placeholder;
		Variant			index;
		Dispatch		activeDoc;
		Dispatch		shapes;
		Dispatch		shape;
		int				shapeCount;
		int				shapeIndex;
		
		
		if (!isTemplate())
			return;
		
		activeWin	= this.oWord.getProperty("ActiveWindow").toDispatch();
		selection 	= this.oWord.getProperty("Selection").toDispatch();
		find		= Dispatch.call(selection,	"Find").toDispatch();
		activePane	= Dispatch.call(activeWin,	"ActivePane").toDispatch();
		view		= Dispatch.call(activePane,	"View").toDispatch();
		contactKeys	= getContactPlaceholders();
		
		
		// go through the views 
		index	= new Variant();
		for (int i = MAX_VIEW_INDEX; i >= 0; i--) 
		// !!! don't start with 0 and end with MAX_RANGE_INDEX !!!
		{
			try
			{
				index.putInt(i);
				Dispatch.put(view, "SeekView", index);
			}
			catch (Exception e)
			{
				continue;
			}

			// go on as long as you find something, that could fit
			while (findPlaceholder(find))
			{
				placeholder = Dispatch.get(selection, "Text").getString();
				if (contactKeys.contains(placeholder))
				{
					super.containsContactInfo	= Boolean.TRUE;
					Dispatch.call(selection, "Collapse", ZERO);
					return;
				}
			}
		}
		
		// show the main document
		Dispatch.call(selection, "Collapse", ZERO);
		
		// search the shapes
		activeDoc	= this.oWord.getProperty("ActiveDocument").toDispatch();
		shapes		= Dispatch.call(activeDoc, "Shapes").toDispatch();
		shapeCount	= Dispatch.call(shapes, "Count").getInt();
		shapeIndex	= 1;
		
		while (shapeIndex <= shapeCount)
		{
			try
			{
				shape = Dispatch.call(shapes, "Item", shapeIndex++).toDispatch();
			}
			catch (Exception e)
			{
				Log.warn(e.getMessage());
				continue;
			}
			
			Dispatch.call(shape, "Select");
			while (findPlaceholder(find))
			{
				placeholder = Dispatch.get(selection, "Text").getString();
				if (contactKeys.contains(placeholder))
				{
					super.containsContactInfo	= Boolean.TRUE;
					Dispatch.call(selection, "Collapse", ZERO);
					return;
				}
			}
		}
		
		// show the main document
		Dispatch.put(view, "SeekView", ZERO);
		
		super.containsContactInfo	= Boolean.FALSE;
	}
	
	
	
	/* ======================================== */
	// 		OVERRIDDEN METHODS
	/* ======================================== */
	
	@Override
	protected void close ()
	{
		Dispatch.call(this.dWord, "Quit");
	}
	
	
	@Override
	protected String getDocumentExtension()
	{
		return DOCUMENT_EXTENSION;
	}
	
	
	@Override
	protected InputStream getDefaultTemplate()
	{
		return WordController.class.getResourceAsStream(DOCUMENT_TEMPLATE_PATH);
	}
	
	
	@Override
	protected boolean isDocumentClosed()
	{
		return wListener.isBQuit();
	}
	
	
	@Override
	protected int replaceAll (Map<String, String> data)
	{
		Dispatch	activeWin	= this.oWord.getProperty("ActiveWindow").toDispatch();
		Dispatch	selection 	= this.oWord.getProperty("Selection").toDispatch();
		Dispatch	find		= Dispatch.call(selection,	"Find").toDispatch();
		Dispatch	activePane	= Dispatch.call(activeWin,	"ActivePane").toDispatch();
		Dispatch	view		= Dispatch.call(activePane,	"View").toDispatch();
		int			founds		= 0;
		Dispatch	activeDoc	= this.oWord.getProperty("ActiveDocument").toDispatch();
		Dispatch	shapes;
		Dispatch	shape;
		int			shapeCount;
		int			shapeIndex;
		
		
//		String[]	views	= new String[] {
//				"wdSeekMainDocument", "wdSeekCurrentPageFooter", "wdSeekCurrentPageHeader", 
//				"wdSeekEndnotes", "wdSeekEvenPagesFooter", "wdSeekEvenPagesHeader", "wdSeekFirstPageFooter", 
//				"wdSeekFirstPageHeader", "wdSeekFootnotes", "wdSeekPrimaryFooter", "wdSeekPrimaryHeader"};
		// go through the views
		for (int index = MAX_VIEW_INDEX; index >= 0; index--) 
		// !!! don't start with 0 and end with MAX_RANGE_INDEX !!!
		{
			try
			{
				Dispatch.put(view, "SeekView", index);
			}
			catch (Exception e)
			{
				Log.warn(e.getMessage());
				continue;
			}
			
			// go on as long as you find something, that could fit
			while (findPlaceholder(find))
				if (replaceSelection(selection, data))
					founds++;
		}
		
		// show the main document
		Dispatch.put(view, "SeekView", ZERO);
		
		// search the shapes
		shapes		= Dispatch.call(activeDoc, "Shapes").toDispatch();
		shapeCount	= Dispatch.call(shapes, "Count").getInt();
		shapeIndex	= 1;
		
		while (shapeIndex <= shapeCount)
		{
			try
			{
				shape = Dispatch.call(shapes, "Item", shapeIndex++).toDispatch();
			}
			catch (Exception e)
			{
				Log.warn(e.getMessage());
				continue;
			}
			
			Dispatch.call(shape, "Select");
			while (findPlaceholder(find))
				if (replaceSelection(selection, data))
					founds++;
		}
		
		// show the main document
		Dispatch.put(view, "SeekView", ZERO);
		
		return founds;
	}
	
	
	@Override
	protected HashSet<String> scanForPlaceholders()
	{
		HashSet<String>	placeholders	= new HashSet<String>();
		Dispatch		activeWin		= this.oWord.getProperty("ActiveWindow").toDispatch();
		Dispatch		selection 		= this.oWord.getProperty("Selection").toDispatch();
		Dispatch		find			= Dispatch.call(selection,	"Find").toDispatch();
		Dispatch		activePane		= Dispatch.call(activeWin,	"ActivePane").toDispatch();
		Dispatch		view			= Dispatch.call(activePane,	"View").toDispatch();
		String			key;
		Dispatch		activeDoc		= this.oWord.getProperty("ActiveDocument").toDispatch();
		Dispatch		shapes;
		Dispatch		shape;
		int				shapeCount;
		int				shapeIndex;
		
		
		// go through the views 
		for (int index = MAX_VIEW_INDEX; index >= 0; index--) 
		// !!! don't start with 0 and end with MAX_RANGE_INDEX !!!
		{
			try
			{
				Dispatch.put(view, "SeekView", index);
			}
			catch (Exception e)
			{
				Log.warn(e.getMessage());
				continue;
			}
			
			// go on as long as you find something, that could fit
			while (findPlaceholder(find))
			{
				key	= Dispatch.get(selection, "Text").getString();
				placeholders.add(key);
			}
				
		}
		
		// show the main document
		Dispatch.put(view, "SeekView", ZERO);
		
		// search the shapes
		shapes		= Dispatch.call(activeDoc, "Shapes").toDispatch();
		shapeCount	= Dispatch.call(shapes, "Count").getInt();
		shapeIndex	= 1;
		
		while (shapeIndex <= shapeCount)
		{
			try
			{
				shape = Dispatch.call(shapes, "Item", shapeIndex++).toDispatch();
			}
			catch (Exception e)
			{
				Log.warn(e.getMessage());
				continue;
			}
			
			Dispatch.call(shape, "Select");
			while (findPlaceholder(find))
			{
				key	= Dispatch.get(selection, "Text").getString();
				placeholders.add(key);
			}
		}
		
		// show the main document
		Dispatch.put(view, "SeekView", ZERO);
		
		return placeholders;
	}
	
	
	
	/* ======================================== */
	// 		HELP METHODS
	/* ======================================== */
	
	private void addListener(WordListener listener) 
	{
		// Setting a listener for the close of a document implement
		// DocumentListener dListener = new DocumentListener();
		// new DispatchEvents(dDoc, dListener, dDoc.getProgramId());
//		new DispatchEvents(dWord, listener, dWord.getProgramId());
		new DispatchEvents(oWord, listener, oWord.getProgramId());
	}
	
	
	private boolean findPlaceholder (Dispatch find)
	{
		try
		{
			Dispatch.callSub(find, "ClearFormatting");
			Variant pattern = useNewPlaceholderStyle() ? SEARCH_NEW_STYLE : SEARCH_OLD_STYLE;
//			logger.info("Used pattern to find Placeholders: "+pattern.getString());
			return Dispatch.call(find, "Execute", 
					pattern, 
					FALSE,	// match case
					FALSE,	// match whole word
					TRUE,	// match wild cards
					FALSE,	// match sounds like
					FALSE,	// match all word forms
					TRUE,	// forward
					ZERO)	// wrap
					.getBoolean();
		}
		catch (Exception e)
		{
			logger.log(Level.ERROR, "Error while trying to find next potential placeholder.", e);
			return false;
		}
	}
	
	
	private boolean replaceSelection (Dispatch selection, Map<String, String> data)
	{
		Variant	key		= Dispatch.get(selection, "Text");
		String	value	= data.get(key.getString());
		
		
		if (value != null)
		{
			// data contains the key
			try
			{
				if (value.startsWith(HTML))
				{
					// paste HTML
					transferable.setHtml(value);
					clipboard.setContents(transferable, null);
					
					try 
					{
						Dispatch.call(selection, "Paste");
					}
					catch (Exception e)
					{
						logger.info("Pasting HTML content failed, waiting and retrying ... ");
						try
						{
							Thread.sleep(1000);
						}
						catch (InterruptedException e1)
						{
							logger.log(Level.ERROR, e1.getMessage(), e1);
						}
						Dispatch.call(selection, "Paste");
					}
					
					Dispatch.call(selection, "TypeBackspace");
				}
				else
				{
					Dispatch.put(selection, "Text", value);
					// collapse selection at the end of the selection
					Dispatch.call(selection, "Collapse", ZERO); 
				}
			}
			catch (Exception e)
			{
				logger.log(Level.ERROR, "Error while trying to replace  '"+key+"'.", e);
				return false;
			}
				
			return true;
		}
		else
			return false;
	}
	
	
	
	/* ======================================== */
	// 		MAIN FOR TESTING
	/* ======================================== */
	
	public static void main(String[] args) 
	{
		Map<String, String> oldStyleData	= new HashMap<String, String>();
		Map<String, String> newStyleData	= new HashMap<String, String>();
		WordController	con;
		
		
		oldStyleData.put("hTML",	"<html><h1>DÜFTE NET DA SENN</h1></html>");
		oldStyleData.put("HTML",	"<html><h1>headline ...</h1><br>... and body</html>");
		oldStyleData.put("TE_XT1",	"Test für Klammernersetzung bestanden");
		oldStyleData.put("tEXt",	"FAILD!");
		oldStyleData.put("TABELLE",	"1\t2\t3\t4\t5\t6");
		oldStyleData.put("HEADER",	"Kopfzeile");
		oldStyleData.put("FOOTER",	"Fußeile");
		
		newStyleData.put("[hTML]",		"<html><h1>DÜFTE NET DA SENN</h1></html>");
		newStyleData.put("[HTML]",		"<html><h1>headline ...</h1><br>... and body</html>");
		newStyleData.put("[TEXT]",		"Test für Klammernersetzung bestanden");
		newStyleData.put("[TE_XT1]",	"Auch mit Zahlen und _ klappt es!");
		newStyleData.put("[tEXt]",		"FAILD!");
		newStyleData.put("[TABELLE]",	"1\t2\t3\t4\t5\t6");
		newStyleData.put("[HEADER]",	"Kopfzeile");
		newStyleData.put("[FOOTER]",	"Fußeile");
		newStyleData.put("[OFFICE_NAME]", "Demo Office");
		newStyleData.put("[OFFICE_INFORMATIONS]", "This is a medical office");
		newStyleData.put("[OFFICE_FULL_ADDRESS]", "In der Kuhle 1,\n1234 Letzebourg");
		newStyleData.put("[OFFICE_ADDRESS_COUNTRY]", "In der Kuhle 1,\nL-1234 Letzebourg");
		
		
		
		
		try 
		{
			con = new WordController();
			con.file = new File("c:\\guido_test.doc");
//			con.file = new File("c:\\test.odt");
//			con.file = new File("c:\\test.docx");
//			con.file = new File("c:\\test.doc");
			con.setPlaceholderVersion(PH_VERSION_NEW);
//			con.setVersion(OLD_STYLE);
			con.open();
			con.startReplacing(con.useNewPlaceholderStyle() ? newStyleData : oldStyleData);
			con.waitForUserInput();
		}
		catch (Exception e) 
		{
			logger.log(Level.ERROR, e.getMessage(), e);
		}
	}
}