#include "stdafx.h"
#include <comdef.h>	// For _bstr_t
#include "VisVim.h"
#include "Commands.h"
#include "OleAut.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;

#endif


// Change directory before opening file?
#define CD_SOURCE		0	// Cd to source path
#define CD_SOURCE_PARENT	1	// Cd to parent directory of source path
#define CD_NONE			2	// No cd


static BOOL g_bEnableVim = TRUE;	// Vim enabled
static BOOL g_bDevStudioEditor = FALSE;	// Open file in Dev Studio editor simultaneously
static BOOL g_bNewTabs = FALSE;
static int g_ChangeDir = CD_NONE;	// CD after file open?

static void VimSetEnableState(BOOL bEnableState);
static BOOL VimOpenFile(BSTR& FileName, long LineNr);
static DISPID VimGetDispatchId(COleAutomationControl& VimOle, char* Method);
static void VimErrDiag(COleAutomationControl& VimOle);
static void VimChangeDir(COleAutomationControl& VimOle, DISPID DispatchId, BSTR& FileName);
static void DebugMsg(char* Msg, char* Arg = NULL);


/////////////////////////////////////////////////////////////////////////////
// CCommands

CCommands::CCommands()
{
	// m_pApplication == NULL; M$ Code generation bug!!!
	m_pApplication = NULL;
	m_pApplicationEventsObj = NULL;
	m_pDebuggerEventsObj = NULL;
}

CCommands::~CCommands()
{
	ASSERT(m_pApplication != NULL);
	if (m_pApplication)
	{
		m_pApplication->Release();
		m_pApplication = NULL;
	}
}

void CCommands::SetApplicationObject(IApplication * pApplication)
{
	// This function assumes pApplication has already been AddRef'd
	// for us, which CDSAddIn did in it's QueryInterface call
	// just before it called us.
	m_pApplication = pApplication;
	if (! m_pApplication)
		return;

	// Create Application event handlers
	XApplicationEventsObj::CreateInstance(&m_pApplicationEventsObj);
	if (! m_pApplicationEventsObj)
	{
		ReportInternalError("XApplicationEventsObj::CreateInstance");
		return;
	}
	m_pApplicationEventsObj->AddRef();
	m_pApplicationEventsObj->Connect(m_pApplication);
	m_pApplicationEventsObj->m_pCommands = this;

#ifdef NEVER
	// Create Debugger event handler
	CComPtr < IDispatch > pDebugger;
	if (SUCCEEDED(m_pApplication->get_Debugger(&pDebugger))
	    && pDebugger != NULL)
	{
		XDebuggerEventsObj::CreateInstance(&m_pDebuggerEventsObj);
		m_pDebuggerEventsObj->AddRef();
		m_pDebuggerEventsObj->Connect(pDebugger);
		m_pDebuggerEventsObj->m_pCommands = this;
	}
#endif

	// Get settings from registry HKEY_CURRENT_USER\Software\Vim\VisVim
	HKEY hAppKey = GetAppKey("Vim");
	if (hAppKey)
	{
		HKEY hSectionKey = GetSectionKey(hAppKey, "VisVim");
		if (hSectionKey)
		{
			g_bEnableVim = GetRegistryInt(hSectionKey, "EnableVim",
						       g_bEnableVim);
			g_bDevStudioEditor = GetRegistryInt(hSectionKey,
					"DevStudioEditor", g_bDevStudioEditor);
			g_bNewTabs = GetRegistryInt(hSectionKey, "NewTabs",
						    g_bNewTabs);
			g_ChangeDir = GetRegistryInt(hSectionKey, "ChangeDir",
						      g_ChangeDir);
			RegCloseKey(hSectionKey);
		}
		RegCloseKey(hAppKey);
	}
}

void CCommands::UnadviseFromEvents()
{
	ASSERT(m_pApplicationEventsObj != NULL);
	if (m_pApplicationEventsObj)
	{
		m_pApplicationEventsObj->Disconnect(m_pApplication);
		m_pApplicationEventsObj->Release();
		m_pApplicationEventsObj = NULL;
	}

#ifdef NEVER
	if (m_pDebuggerEventsObj)
	{
		// Since we were able to connect to the Debugger events, we
		// should be able to access the Debugger object again to
		// unadvise from its events (thus the VERIFY_OK below--see
		// stdafx.h).
		CComPtr < IDispatch > pDebugger;
		VERIFY_OK(m_pApplication->get_Debugger(&pDebugger));
		ASSERT(pDebugger != NULL);
		m_pDebuggerEventsObj->Disconnect(pDebugger);
		m_pDebuggerEventsObj->Release();
		m_pDebuggerEventsObj = NULL;
	}
#endif
}


/////////////////////////////////////////////////////////////////////////////
// Event handlers

// Application events

HRESULT CCommands::XApplicationEvents::BeforeBuildStart()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::BuildFinish(long nNumErrors, long nNumWarnings)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::BeforeApplicationShutDown()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

// The open document event handle is the place where the real interface work
// is done.
// Vim gets called from here.
//
HRESULT CCommands::XApplicationEvents::DocumentOpen(IDispatch * theDocument)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	if (! g_bEnableVim)
		// Vim not enabled or empty command line entered
		return S_OK;

	// First get the current file name and line number

	// Get the document object
	CComQIPtr < ITextDocument, &IID_ITextDocument > pDoc(theDocument);
	if (! pDoc)
		return S_OK;

	BSTR FileName;
	long LineNr = -1;

	// Get the document name
	if (FAILED(pDoc->get_FullName(&FileName)))
		return S_OK;

	LPDISPATCH pDispSel;

	// Get a selection object dispatch pointer
	if (SUCCEEDED(pDoc->get_Selection(&pDispSel)))
	{
		// Get the selection object
		CComQIPtr < ITextSelection, &IID_ITextSelection > pSel(pDispSel);

		if (pSel)
			// Get the selection line number
			pSel->get_CurrentLine(&LineNr);

		pDispSel->Release();
	}

	// Open the file in Vim and position to the current line
	if (VimOpenFile(FileName, LineNr))
	{
		if (! g_bDevStudioEditor)
		{
			// Close the document in developer studio
			CComVariant vSaveChanges = dsSaveChangesPrompt;
			DsSaveStatus Saved;

			pDoc->Close(vSaveChanges, &Saved);
		}
	}

	// We're done here
	SysFreeString(FileName);
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::BeforeDocumentClose(IDispatch * theDocument)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::DocumentSave(IDispatch * theDocument)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::NewDocument(IDispatch * theDocument)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	if (! g_bEnableVim)
		// Vim not enabled or empty command line entered
		return S_OK;

	// First get the current file name and line number

	CComQIPtr < ITextDocument, &IID_ITextDocument > pDoc(theDocument);
	if (! pDoc)
		return S_OK;

	BSTR FileName;
	HRESULT hr;

	hr = pDoc->get_FullName(&FileName);
	if (FAILED(hr))
		return S_OK;

	// Open the file in Vim and position to the current line
	if (VimOpenFile(FileName, 0))
	{
		if (! g_bDevStudioEditor)
		{
			// Close the document in developer studio
			CComVariant vSaveChanges = dsSaveChangesPrompt;
			DsSaveStatus Saved;

			pDoc->Close(vSaveChanges, &Saved);
		}
	}

	SysFreeString(FileName);
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::WindowActivate(IDispatch * theWindow)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::WindowDeactivate(IDispatch * theWindow)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::WorkspaceOpen()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::WorkspaceClose()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

HRESULT CCommands::XApplicationEvents::NewWorkspace()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}

// Debugger event

HRESULT CCommands::XDebuggerEvents::BreakpointHit(IDispatch * pBreakpoint)
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	return S_OK;
}


/////////////////////////////////////////////////////////////////////////////
// VisVim dialog

class CMainDialog : public CDialog
{
    public:
	CMainDialog(CWnd * pParent = NULL);	// Standard constructor

	//{{AFX_DATA(CMainDialog)
	enum { IDD = IDD_ADDINMAIN };
	int	m_ChangeDir;
	BOOL	m_bDevStudioEditor;
	BOOL	m_bNewTabs;
	//}}AFX_DATA

	//{{AFX_VIRTUAL(CMainDialog)
    protected:
	virtual void DoDataExchange(CDataExchange * pDX);	// DDX/DDV support
	//}}AFX_VIRTUAL

    protected:
	//{{AFX_MSG(CMainDialog)
	afx_msg void OnEnable();
	afx_msg void OnDisable();
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
};

CMainDialog::CMainDialog(CWnd * pParent /* =NULL */ )
	: CDialog(CMainDialog::IDD, pParent)
{
	//{{AFX_DATA_INIT(CMainDialog)
	m_ChangeDir = -1;
	m_bDevStudioEditor = FALSE;
	m_bNewTabs = FALSE;
	//}}AFX_DATA_INIT
}

void CMainDialog::DoDataExchange(CDataExchange * pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CMainDialog)
	DDX_Radio(pDX, IDC_CD_SOURCE_PATH, m_ChangeDir);
	DDX_Check(pDX, IDC_DEVSTUDIO_EDITOR, m_bDevStudioEditor);
	DDX_Check(pDX, IDC_NEW_TABS, m_bNewTabs);
	//}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CMainDialog, CDialog)
	//{{AFX_MSG_MAP(CMainDialog)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////
// CCommands methods

STDMETHODIMP CCommands::VisVimDialog()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	// Use m_pApplication to access the Developer Studio Application
	// object,
	// and VERIFY_OK to see error strings in DEBUG builds of your add-in
	// (see stdafx.h)

	VERIFY_OK(m_pApplication->EnableModeless(VARIANT_FALSE));

	CMainDialog Dlg;

	Dlg.m_bDevStudioEditor = g_bDevStudioEditor;
	Dlg.m_bNewTabs = g_bNewTabs;
	Dlg.m_ChangeDir = g_ChangeDir;
	if (Dlg.DoModal() == IDOK)
	{
		g_bDevStudioEditor = Dlg.m_bDevStudioEditor;
		g_bNewTabs = Dlg.m_bNewTabs;
		g_ChangeDir = Dlg.m_ChangeDir;

		// Save settings to registry HKEY_CURRENT_USER\Software\Vim\VisVim
		HKEY hAppKey = GetAppKey("Vim");
		if (hAppKey)
		{
			HKEY hSectionKey = GetSectionKey(hAppKey, "VisVim");
			if (hSectionKey)
			{
				WriteRegistryInt(hSectionKey, "DevStudioEditor",
						  g_bDevStudioEditor);
				WriteRegistryInt(hSectionKey, "NewTabs",
						  g_bNewTabs);
				WriteRegistryInt(hSectionKey, "ChangeDir", g_ChangeDir);
				RegCloseKey(hSectionKey);
			}
			RegCloseKey(hAppKey);
		}
	}

	VERIFY_OK(m_pApplication->EnableModeless(VARIANT_TRUE));
	return S_OK;
}

STDMETHODIMP CCommands::VisVimEnable()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	VimSetEnableState(true);
	return S_OK;
}

STDMETHODIMP CCommands::VisVimDisable()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	VimSetEnableState(false);
	return S_OK;
}

STDMETHODIMP CCommands::VisVimToggle()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());
	VimSetEnableState(! g_bEnableVim);
	return S_OK;
}

STDMETHODIMP CCommands::VisVimLoad()
{
	AFX_MANAGE_STATE(AfxGetStaticModuleState());

	// Use m_pApplication to access the Developer Studio Application object,
	// and VERIFY_OK to see error strings in DEBUG builds of your add-in
	// (see stdafx.h)

	CComBSTR bStr;
	// Define dispatch pointers for document and selection objects
	CComPtr < IDispatch > pDispDoc, pDispSel;

	// Get a document object dispatch pointer
	VERIFY_OK(m_pApplication->get_ActiveDocument(&pDispDoc));
	if (! pDispDoc)
		return S_OK;

	BSTR FileName;
	long LineNr = -1;

	// Get the document object
	CComQIPtr < ITextDocument, &IID_ITextDocument > pDoc(pDispDoc);

	if (! pDoc)
		return S_OK;

	// Get the document name
	if (FAILED(pDoc->get_FullName(&FileName)))
		return S_OK;

	// Get a selection object dispatch pointer
	if (SUCCEEDED(pDoc->get_Selection(&pDispSel)))
	{
		// Get the selection object
		CComQIPtr < ITextSelection, &IID_ITextSelection > pSel(pDispSel);

		if (pSel)
			// Get the selection line number
			pSel->get_CurrentLine(&LineNr);
	}

	// Open the file in Vim
	VimOpenFile(FileName, LineNr);

	SysFreeString(FileName);
	return S_OK;
}


//
// Here we do the actual processing and communication with Vim
//

// Set the enable state and save to registry
//
static void VimSetEnableState(BOOL bEnableState)
{
	g_bEnableVim = bEnableState;
	HKEY hAppKey = GetAppKey("Vim");
	if (hAppKey)
	{
		HKEY hSectionKey = GetSectionKey(hAppKey, "VisVim");
		if (hSectionKey)
			WriteRegistryInt(hSectionKey, "EnableVim", g_bEnableVim);
		RegCloseKey(hAppKey);
	}
}

// Open the file 'FileName' in Vim and goto line 'LineNr'
// 'FileName' is expected to contain an absolute DOS path including the drive
// letter.
// 'LineNr' must contain a valid line number or 0, e. g. for a new file
//
static BOOL VimOpenFile(BSTR& FileName, long LineNr)
{

	// OLE automation object for com. with Vim
	// When the object goes out of scope, it's destructor destroys the OLE
	// connection;
	// This is important to avoid blocking the object
	// (in this memory corruption would be likely when terminating Vim
	// while still running DevStudio).
	// So keep this object local!
	COleAutomationControl VimOle;

	// :cd D:/Src2/VisVim/
	//
	// Get a dispatch id for the SendKeys method of Vim;
	// enables connection to Vim if necessary
	DISPID DispatchId;
	DispatchId = VimGetDispatchId(VimOle, "SendKeys");
	if (! DispatchId)
		// OLE error, can't obtain dispatch id
		goto OleError;

	OLECHAR Buf[MAX_OLE_STR];
	char FileNameTmp[MAX_OLE_STR];
	char VimCmd[MAX_OLE_STR];
	char *s, *p;

	// Prepend CTRL-\ CTRL-N to exit insert mode
	VimCmd[0] = 0x1c;
	VimCmd[1] = 0x0e;
	VimCmd[2] = 0;

#ifdef SINGLE_WINDOW
	// Update the current file in Vim if it has been modified.
	// Disabled, because it could write the file when you don't want to.
	sprintf(VimCmd + 2, ":up\n");
#endif
	if (! VimOle.Method(DispatchId, "s", TO_OLE_STR_BUF(VimCmd, Buf)))
		goto OleError;

	// Change Vim working directory to where the file is if desired
	if (g_ChangeDir != CD_NONE)
		VimChangeDir(VimOle, DispatchId, FileName);

	// Make Vim open the file.
	// In the filename convert all \ to /, put a \ before a space.
	if (g_bNewTabs)
	{
		sprintf(VimCmd, ":tab drop ");
		s = VimCmd + 10;
	}
	else
	{
		sprintf(VimCmd, ":drop ");
		s = VimCmd + 6;
	}
	sprintf(FileNameTmp, "%S", (char *)FileName);
	for (p = FileNameTmp; *p != '\0' && s < VimCmd + MAX_OLE_STR - 4; ++p)
		if (*p == '\\')
			*s++ = '/';
		else
		{
			if (*p == ' ')
				*s++ = '\\';
			*s++ = *p;
		}
	*s++ = '\n';
	*s = '\0';

	if (! VimOle.Method(DispatchId, "s", TO_OLE_STR_BUF(VimCmd, Buf)))
		goto OleError;

	if (LineNr > 0)
	{
		// Goto line
		sprintf(VimCmd, ":%ld\n", LineNr);
		if (! VimOle.Method(DispatchId, "s", TO_OLE_STR_BUF(VimCmd, Buf)))
			goto OleError;
	}

	// Make Vim come to the foreground
	if (! VimOle.Method("SetForeground"))
		VimOle.ErrDiag();

	// We're done
	return true;

    OleError:
	// There was an OLE error
	// Check if it's the "unknown class string" error
	VimErrDiag(VimOle);
	return false;
}

// Return the dispatch id for the Vim method 'Method'
// Create the Vim OLE object if necessary
// Returns a valid dispatch id or null on error
//
static DISPID VimGetDispatchId(COleAutomationControl& VimOle, char* Method)
{
	// Initialize Vim OLE connection if not already done
	if (! VimOle.IsCreated())
	{
		if (! VimOle.CreateObject("Vim.Application"))
			return NULL;
	}

	// Get the dispatch id for the SendKeys method.
	// By doing this, we are checking if Vim is still there...
	DISPID DispatchId = VimOle.GetDispatchId("SendKeys");
	if (! DispatchId)
	{
		// We can't get a dispatch id.
		// This means that probably Vim has been terminated.
		// Don't issue an error message here, instead
		// destroy the OLE object and try to connect once more
		//
		// In fact, this should never happen, because the OLE aut. object
		// should not be kept long enough to allow the user to terminate Vim
		// to avoid memory corruption (why the heck is there no system garbage
		// collection for those damned OLE memory chunks???).
		VimOle.DeleteObject();
		if (! VimOle.CreateObject("Vim.Application"))
			// If this create fails, it's time for an error msg
			return NULL;

		if (! (DispatchId = VimOle.GetDispatchId("SendKeys")))
			// There is something wrong...
			return NULL;
	}

	return DispatchId;
}

// Output an error message for an OLE error
// Check on the classstring error, which probably means Vim wasn't registered.
//
static void VimErrDiag(COleAutomationControl& VimOle)
{
	SCODE sc = GetScode(VimOle.GetResult());
	if (sc == CO_E_CLASSSTRING)
	{
		char Buf[256];
		sprintf(Buf, "There is no registered OLE automation server named "
			 "\"Vim.Application\".\n"
			 "Use the OLE-enabled version of Vim with VisVim and "
			 "make sure to register Vim by running \"vim -register\".");
		MessageBox(NULL, Buf, "OLE Error", MB_OK);
	}
	else
		VimOle.ErrDiag();
}

// Change directory to the directory the file 'FileName' is in or it's parent
// directory according to the setting of the global 'g_ChangeDir':
// 'FileName' is expected to contain an absolute DOS path including the drive
// letter.
//	CD_NONE
//	CD_SOURCE_PATH
//	CD_SOURCE_PARENT
//
static void VimChangeDir(COleAutomationControl& VimOle, DISPID DispatchId, BSTR& FileName)
{
	// Do a :cd first

	// Get the path name of the file ("dir/")
	CString StrFileName = FileName;
	char Drive[_MAX_DRIVE];
	char Dir[_MAX_DIR];
	char DirUnix[_MAX_DIR * 2];
	char *s, *t;

	_splitpath(StrFileName, Drive, Dir, NULL, NULL);

	// Convert to Unix path name format, escape spaces.
	t = DirUnix;
	for (s = Dir; *s; ++s)
		if (*s == '\\')
			*t++ = '/';
		else
		{
			if (*s == ' ')
				*t++ = '\\';
			*t++ = *s;
		}
	*t = '\0';


	// Construct the cd command; append /.. if cd to parent
	// directory and not in root directory
	OLECHAR Buf[MAX_OLE_STR];
	char VimCmd[MAX_OLE_STR];

	sprintf(VimCmd, ":cd %s%s%s\n", Drive, DirUnix,
		 g_ChangeDir == CD_SOURCE_PARENT && DirUnix[1] ? ".." : "");
	VimOle.Method(DispatchId, "s", TO_OLE_STR_BUF(VimCmd, Buf));
}

#ifdef _DEBUG
// Print out a debug message
//
static void DebugMsg(char* Msg, char* Arg)
{
	char Buf[400];
	sprintf(Buf, Msg, Arg);
	AfxMessageBox(Buf);
}
#endif
