#include "expat.h"
#ifdef XML_UNICODE
#define UNICODE
#endif
#include <windows.h>
#include <urlmon.h>
#include <wininet.h>
#include <stdio.h>
#include <tchar.h>
#include "xmlurl.h"
#include "xmlmime.h"

static int
processURL(XML_Parser parser, IMoniker *baseMoniker, const XML_Char *url);

typedef void (*StopHandler)(void *, HRESULT);

class Callback : public IBindStatusCallback {
public:
  // IUnknown methods
  STDMETHODIMP QueryInterface(REFIID,void **);
  STDMETHODIMP_(ULONG) AddRef();
  STDMETHODIMP_(ULONG) Release();
  // IBindStatusCallback methods
  STDMETHODIMP OnStartBinding(DWORD, IBinding *);
  STDMETHODIMP GetPriority(LONG *);
  STDMETHODIMP OnLowResource(DWORD);
  STDMETHODIMP OnProgress(ULONG, ULONG, ULONG, LPCWSTR);
  STDMETHODIMP OnStopBinding(HRESULT, LPCWSTR);
  STDMETHODIMP GetBindInfo(DWORD *, BINDINFO *);
  STDMETHODIMP OnDataAvailable(DWORD, DWORD, FORMATETC *, STGMEDIUM *);
  STDMETHODIMP OnObjectAvailable(REFIID, IUnknown *);
  Callback(XML_Parser, IMoniker *, StopHandler, void *);
  ~Callback();
  int externalEntityRef(const XML_Char *context,
                        const XML_Char *systemId, const XML_Char *publicId);
private:
  XML_Parser parser_;
  IMoniker *baseMoniker_;
  DWORD totalRead_;
  ULONG ref_;
  IBinding *pBinding_;
  StopHandler stopHandler_;
  void *stopArg_;
};

STDMETHODIMP_(ULONG)
Callback::AddRef()
{ 
  return ref_++;
}

STDMETHODIMP_(ULONG)
Callback::Release()
{ 
  if (--ref_ == 0) {
    delete this;
    return 0;
  }
  return ref_;
}

STDMETHODIMP
Callback::QueryInterface(REFIID riid, void** ppv)
{ 
  if (IsEqualGUID(riid, IID_IUnknown))
    *ppv = (IUnknown *)this;
  else if (IsEqualGUID(riid, IID_IBindStatusCallback))
    *ppv = (IBindStatusCallback *)this;
  else
    return E_NOINTERFACE;
  ((LPUNKNOWN)*ppv)->AddRef();
  return S_OK;
}

STDMETHODIMP
Callback::OnStartBinding(DWORD, IBinding* pBinding)
{
  pBinding_ = pBinding;
  pBinding->AddRef();
  return S_OK;
}

STDMETHODIMP
Callback::GetPriority(LONG *)
{
  return E_NOTIMPL;
}

STDMETHODIMP
Callback::OnLowResource(DWORD)
{
  return E_NOTIMPL;
}

STDMETHODIMP
Callback::OnProgress(ULONG, ULONG, ULONG, LPCWSTR)
{
  return S_OK;
}

STDMETHODIMP
Callback::OnStopBinding(HRESULT hr, LPCWSTR szError)
{
  if (pBinding_) {
    pBinding_->Release();
    pBinding_ = 0;
  }
  if (baseMoniker_) {
    baseMoniker_->Release();
    baseMoniker_ = 0;
  }
  stopHandler_(stopArg_, hr);
  return S_OK;
}

STDMETHODIMP
Callback::GetBindInfo(DWORD* pgrfBINDF, BINDINFO* pbindinfo)
{
  *pgrfBINDF = BINDF_ASYNCHRONOUS;
  return S_OK;
}

static void
reportError(XML_Parser parser)
{
  int code = XML_GetErrorCode(parser);
  const XML_Char *message = XML_ErrorString(code);
  if (message)
    _ftprintf(stderr, wxT("%s:%d:%ld: %s\n"),
	     XML_GetBase(parser),
	     XML_GetErrorLineNumber(parser),
	     XML_GetErrorColumnNumber(parser),
	     message);
  else
    _ftprintf(stderr, wxT("%s: (unknown message %d)\n"),
              XML_GetBase(parser), code);
}

STDMETHODIMP
Callback::OnDataAvailable(DWORD grfBSCF,
                          DWORD dwSize,
                          FORMATETC *pfmtetc,
                          STGMEDIUM* pstgmed)
{
  if (grfBSCF & BSCF_FIRSTDATANOTIFICATION) {
    IWinInetHttpInfo *hp;
    HRESULT hr = pBinding_->QueryInterface(IID_IWinInetHttpInfo,
                                           (void **)&hp);
    if (SUCCEEDED(hr)) {
      char contentType[1024];
      DWORD bufSize = sizeof(contentType);
      DWORD flags = 0;
      contentType[0] = 0;
      hr = hp->QueryInfo(HTTP_QUERY_CONTENT_TYPE, contentType,
                         &bufSize, 0, NULL);
      if (SUCCEEDED(hr)) {
	char charset[CHARSET_MAX];
	getXMLCharset(contentType, charset);
	if (charset[0]) {
#ifdef XML_UNICODE
	  XML_Char wcharset[CHARSET_MAX];
	  XML_Char *p1 = wcharset;
	  const char *p2 = charset;
	  while ((*p1++ = (unsigned char)*p2++) != 0)
	    ;
	  XML_SetEncoding(parser_, wcharset);
#else
	  XML_SetEncoding(parser_, charset);
#endif
	}
      }
      hp->Release();
    }
  }
  if (!parser_)
    return E_ABORT;
  if (pstgmed->tymed == TYMED_ISTREAM) {
    while (totalRead_ < dwSize) {
#define READ_MAX (64*1024)
      DWORD nToRead = dwSize - totalRead_;
      if (nToRead > READ_MAX)
	nToRead = READ_MAX;
      void *buf = XML_GetBuffer(parser_, nToRead);
      if (!buf) {
	_ftprintf(stderr, wxT("out of memory\n"));
	return E_ABORT;
      }
      DWORD nRead;
      HRESULT hr = pstgmed->pstm->Read(buf, nToRead, &nRead);
      if (SUCCEEDED(hr)) {
	totalRead_ += nRead;
	if (!XML_ParseBuffer(parser_,
			     nRead,
			     (grfBSCF & BSCF_LASTDATANOTIFICATION) != 0
			     && totalRead_ == dwSize)) {
	  reportError(parser_);
	  return E_ABORT;
	}
      }
    }
  }
  return S_OK;
}

STDMETHODIMP
Callback::OnObjectAvailable(REFIID, IUnknown *)
{
  return S_OK;
}

int
Callback::externalEntityRef(const XML_Char *context,
                            const XML_Char *systemId,
                            const XML_Char *publicId)
{
  XML_Parser entParser = XML_ExternalEntityParserCreate(parser_, context, 0);
  XML_SetBase(entParser, systemId);
  int ret = processURL(entParser, baseMoniker_, systemId);
  XML_ParserFree(entParser);
  return ret;
}

Callback::Callback(XML_Parser parser, IMoniker *baseMoniker,
                   StopHandler stopHandler, void *stopArg)
: parser_(parser),
  baseMoniker_(baseMoniker),
  ref_(0),
  pBinding_(0),
  totalRead_(0),
  stopHandler_(stopHandler),
  stopArg_(stopArg)
{
  if (baseMoniker_)
    baseMoniker_->AddRef();
}

Callback::~Callback()
{
  if (pBinding_)
    pBinding_->Release();
  if (baseMoniker_)
    baseMoniker_->Release();
}

static int
externalEntityRef(void *arg,
                  const XML_Char *context,
                  const XML_Char *base,
                  const XML_Char *systemId,
                  const XML_Char *publicId)
{
  return ((Callback *)arg)->externalEntityRef(context, systemId, publicId);
}


static HRESULT
openStream(XML_Parser parser,
           IMoniker *baseMoniker,
           const XML_Char *uri,
           StopHandler stopHandler, void *stopArg)
{
  if (!XML_SetBase(parser, uri))
    return E_OUTOFMEMORY;
  HRESULT hr;
  IMoniker *m;
#ifdef XML_UNICODE
  hr = CreateURLMoniker(0, uri, &m);
#else
  LPWSTR uriw = new wchar_t[strlen(uri) + 1];
  for (int i = 0;; i++) {
    uriw[i] = uri[i];
    if (uriw[i] == 0)
      break;
  }
  hr = CreateURLMoniker(baseMoniker, uriw, &m);
  delete [] uriw;
#endif
  if (FAILED(hr))
    return hr;
  IBindStatusCallback *cb = new Callback(parser, m, stopHandler, stopArg);
  XML_SetExternalEntityRefHandler(parser, externalEntityRef);
  XML_SetExternalEntityRefHandlerArg(parser, cb);
  cb->AddRef();
  IBindCtx *b;
  if (FAILED(hr = CreateAsyncBindCtx(0, cb, 0, &b))) {
    cb->Release();
    m->Release();
    return hr;
  }
  cb->Release();
  IStream *pStream;
  hr = m->BindToStorage(b, 0, IID_IStream, (void **)&pStream);
  if (SUCCEEDED(hr)) {
    if (pStream)
      pStream->Release();
  }
  if (hr == MK_S_ASYNCHRONOUS)
    hr = S_OK;
  m->Release();
  b->Release();
  return hr;
}

struct QuitInfo {
  const XML_Char *url;
  HRESULT hr;
  int stop;
};

static void
winPerror(const XML_Char *url, HRESULT hr)
{
  LPVOID buf;
  if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
		    | FORMAT_MESSAGE_FROM_HMODULE,
		    GetModuleHandleA("urlmon.dll"),
		    hr,
		    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		    (LPTSTR) &buf,
		    0,
		    NULL)
      || FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
		      | FORMAT_MESSAGE_FROM_SYSTEM,
		      0,
		      hr,
		      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		      (LPTSTR) &buf,
		      0,
		      NULL)) {
    /* The system error messages seem to end with a newline. */
    _ftprintf(stderr, wxT("%s: %s"), url, buf);
    fflush(stderr);
    LocalFree(buf);
  }
  else
    _ftprintf(stderr, wxT("%s: error %x\n"), url, hr);
}

static void
threadQuit(void *p, HRESULT hr)
{
  QuitInfo *qi = (QuitInfo *)p;
  qi->hr = hr;
  qi->stop = 1;
}

extern "C"
int
XML_URLInit(void)
{
  return SUCCEEDED(CoInitialize(0));
}

extern "C"
void
XML_URLUninit(void)
{
  CoUninitialize();
}

static int
processURL(XML_Parser parser, IMoniker *baseMoniker,
           const XML_Char *url)
{
  QuitInfo qi;
  qi.stop = 0;
  qi.url = url;

  XML_SetBase(parser, url);
  HRESULT hr = openStream(parser, baseMoniker, url, threadQuit, &qi);
  if (FAILED(hr)) {
    winPerror(url, hr);
    return 0;
  }
  else if (FAILED(qi.hr)) {
    winPerror(url, qi.hr);
    return 0;
  }
  MSG msg;
  while (!qi.stop && GetMessage (&msg, NULL, 0, 0)) {
    TranslateMessage (&msg);
    DispatchMessage (&msg);
  }
  return 1;
}

extern "C"
int
XML_ProcessURL(XML_Parser parser,
               const XML_Char *url,
               unsigned flags)
{
  return processURL(parser, 0, url);
}
