//
// FLTK native OS file chooser widget
//
// Copyright 1998-2021 by Bill Spitzak and others.
// Copyright 2004 Greg Ercolano.
// API changes + filter improvements by Nathan Vander Wilt 2005
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file.  If this
// file is missing or damaged, see the license at:
//
//     https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
//     https://www.fltk.org/bugs.php
//

// Any application to multi-folder implementation:
//     http://www.codeproject.com/dialog/selectfolder.asp
//

#ifndef FL_DOXYGEN              // PREVENT DOXYGEN'S USE OF THIS FILE
#include <FL/Enumerations.H>

#include <stdlib.h>             // malloc
#include <stdio.h>              // snprintf
#include <wchar.h>

#define FNFC_MAX_PATH 32768     // XXX: MAX_PATH under win32 is 260, too small for modern use

#include <FL/fl_string_functions.h>       // fl_strdup()
#include "flstring.h"           // fl_strlcpy()/cat()
#include <FL/Fl_Native_File_Chooser.H>
#  include <windows.h>
#  include <commdlg.h>          // OPENFILENAMEW, GetOpenFileName()
#  include <shlobj.h>           // BROWSEINFOW, SHBrowseForFolder()
#  include <FL/filename.H>      // FL_EXPORT
#include <FL/platform.H>        // fl_open_display


class Fl_WinAPI_Native_File_Chooser_Driver : public Fl_Native_File_Chooser_Driver {
private:
  int  _btype;                  // kind-of browser to show()
  int  _options;                // general options
  OPENFILENAMEW *_ofn_ptr;      // GetOpenFileName() & GetSaveFileName() struct
  BROWSEINFOW   *_binf_ptr;     // SHBrowseForFolder() struct
  WCHAR         *_wpattern;     // pattern buffer for filter
  char  **_pathnames;           // array of pathnames
  int     _tpathnames;          // total pathnames
  char   *_directory;           // default pathname to use
  char   *_title;               // title for window
  char   *_filter;              // user-side search filter
  char   *_parsedfilt;          // filter parsed for Windows dialog
  int     _nfilters;            // number of filters parse_filter counted
  char   *_preset_file;         // the file to preselect
  char   *_errmsg;              // error message

  // Private methods
  void errmsg(const char *msg);

  void clear_pathnames();
  void set_single_pathname(const char *s);
  void add_pathname(const char *s);

  void ClearOFN();
  void ClearBINF();
  void Win2Unix(char *s);
  void Unix2Win(char *s);
  bool IsUnixPath(const char *s);
  int showfile();
  int showdir();

  void parse_filter(const char *);
  void clear_filters();
  void add_filter(const char *, const char *);
public:
  Fl_WinAPI_Native_File_Chooser_Driver(int val);
  ~Fl_WinAPI_Native_File_Chooser_Driver();
  void type(int t) FL_OVERRIDE;
  int type() const FL_OVERRIDE;
  void options(int o) FL_OVERRIDE;
  int options() const FL_OVERRIDE;
  int count() const FL_OVERRIDE;
  const char *filename() const FL_OVERRIDE;
  const char *filename(int i) const FL_OVERRIDE;
  void directory(const char *val) FL_OVERRIDE;
  const char *directory() const FL_OVERRIDE;
  void title(const char *t) FL_OVERRIDE;
  const char* title() const FL_OVERRIDE;
  const char *filter() const FL_OVERRIDE;
  void filter(const char *f) FL_OVERRIDE;
  int filters() const FL_OVERRIDE;
  void filter_value(int i) FL_OVERRIDE;
  int filter_value() const FL_OVERRIDE;
  void preset_file(const char*f) FL_OVERRIDE;
  const char* preset_file() const FL_OVERRIDE;
  const char *errmsg() const FL_OVERRIDE;
  int show() FL_OVERRIDE;
};


Fl_Native_File_Chooser::Fl_Native_File_Chooser(int val) {
  platform_fnfc = new Fl_WinAPI_Native_File_Chooser_Driver(val);
}


static LPCWSTR utf8towchar(const char *in);
static char *wchartoutf8(LPCWSTR in);


#define LCURLY_CHR      '{'
#define RCURLY_CHR      '}'
#define LBRACKET_CHR    '['
#define RBRACKET_CHR    ']'

// STATIC: Print Windows 'double-null' string (debug)
#ifdef DEBUG
#include <stdio.h>
static void dnullprint(char *wp) {
  if ( ! wp ) return;
  for ( int t=0; true; t++ ) {
    if ( wp[t] == '\0' && wp[t+1] == '\0' ) {
      printf("\\0\\0");
      fflush(stdout);
      return;
    } else if ( wp[t] == '\0' ) {
      printf("\\0");
    } else {
      printf("%c",wp[t]);
    }
  }
}
#endif

// Return length of double-null string
//    Includes single nulls in count, excludes trailing double-null.
//
//         1234 567
//         |||/\|||
//    IN: "one\0two\0\0"
//   OUT: 7
//
static int dnulllen(const char *wp) {
  int len = 0;
  while ( ! ( *(wp+0) == 0 && *(wp+1) == 0 ) ) {
    ++wp;
    ++len;
  }
  return(len);
}

// STATIC: Append a string to another, leaving terminated with DOUBLE NULL.
//     Automatically handles extending length of string.
//     wp can be NULL (a new wp will be allocated and initialized).
//     string must be NULL terminated.
//     The pointer wp may be modified on return.
//
static void dnullcat(char*&wp, const char *string, int n = -1 ) {
  //DEBUG printf("DEBUG: dnullcat IN: <"); dnullprint(wp); printf(">\n");
  size_t inlen = ( n < 0 ) ? strlen(string) : n;
  char *wp2 = 0; // used to point at end of last string
  if ( ! wp ) {
    wp = new char[inlen + 4];
    *(wp+0) = '\0';
    *(wp+1) = '\0';
    wp2 = wp; // no "last string", point at begin of buffer
  } else {
    int wplen = dnulllen(wp);
    // Make copy of wp into larger buffer
    char *tmp = new char[wplen + inlen + 4];
    memcpy(tmp, wp, wplen+2);   // copy of wp plus doublenull
    delete[] wp;                // delete old wp
    wp = tmp;                   // use new copy
    wp2 = wp + wplen + 1;       // point at second null
    //DEBUG printf("DEBUG: dnullcat COPY: <"); dnullprint(wp); printf("> (wplen=%d)\n", wplen);
  }

  // *wp2 points at second null; the buffer is large enough to copy the string!
  strcpy(wp2, string);
  *(wp2+inlen+1) = '\0'; // Leave string double-null terminated
  //DEBUG printf("DEBUG: dnullcat OUT: <"); dnullprint(wp); printf(">\n\n");
}

// CTOR
Fl_WinAPI_Native_File_Chooser_Driver::Fl_WinAPI_Native_File_Chooser_Driver(int val) :
  Fl_Native_File_Chooser_Driver(val) {
  _btype           = val;
  _options         = Fl_Native_File_Chooser::NO_OPTIONS;
  _ofn_ptr         = new OPENFILENAMEW;
  _binf_ptr        = new BROWSEINFOW;
  _wpattern        = 0;
  memset((void*)_ofn_ptr, 0, sizeof(OPENFILENAMEW));
  _ofn_ptr->lStructSize = sizeof(OPENFILENAMEW);
  _ofn_ptr->hwndOwner = 0L;
  memset((void*)_binf_ptr, 0, sizeof(BROWSEINFOW));
  _pathnames       = NULL;
  _tpathnames      = 0;
  _directory       = NULL;
  _title           = NULL;
  _filter          = NULL;
  _parsedfilt      = NULL;
  _nfilters        = 0;
  _preset_file     = NULL;
  _errmsg          = NULL;
}

// DTOR
Fl_WinAPI_Native_File_Chooser_Driver::~Fl_WinAPI_Native_File_Chooser_Driver() {
  //_pathnames                // managed by clear_pathnames()
  //_tpathnames               // managed by clear_pathnames()
  _directory   = strfree(_directory);
  _title       = strfree(_title);
  _filter      = strfree(_filter);
  //_parsedfilt               // managed by clear_filters()
  //_nfilters                 // managed by clear_filters()
  _preset_file = strfree(_preset_file);
  _errmsg      = strfree(_errmsg);
  clear_filters();
  clear_pathnames();
  ClearOFN();
  ClearBINF();
  delete _binf_ptr;
  delete _ofn_ptr;
  if ( _wpattern ) delete[] _wpattern;
}

// SET TYPE OF BROWSER
void Fl_WinAPI_Native_File_Chooser_Driver::type(int val) {
  _btype = val;
}

// GET TYPE OF BROWSER
int Fl_WinAPI_Native_File_Chooser_Driver::type() const {
  return( _btype );
}

// SET OPTIONS
void Fl_WinAPI_Native_File_Chooser_Driver::options(int val) {
  _options = val;
}

// GET OPTIONS
int Fl_WinAPI_Native_File_Chooser_Driver::options() const {
  return(_options);
}

// PRIVATE: SET ERROR MESSAGE
void Fl_WinAPI_Native_File_Chooser_Driver::errmsg(const char *val) {
  _errmsg = strfree(_errmsg);
  _errmsg = strnew(val);
}

// FREE PATHNAMES ARRAY, IF IT HAS ANY CONTENTS
void Fl_WinAPI_Native_File_Chooser_Driver::clear_pathnames() {
  if ( _pathnames ) {
    while ( --_tpathnames >= 0 ) {
      _pathnames[_tpathnames] = strfree(_pathnames[_tpathnames]);
    }
    delete[] _pathnames;
    _pathnames = NULL;
  }
  _tpathnames = 0;
}

// SET A SINGLE PATHNAME
void Fl_WinAPI_Native_File_Chooser_Driver::set_single_pathname(const char *s) {
  clear_pathnames();
  _pathnames = new char*[1];
  _pathnames[0] = strnew(s);
  _tpathnames = 1;
}

// ADD PATHNAME TO EXISTING ARRAY
void Fl_WinAPI_Native_File_Chooser_Driver::add_pathname(const char *s) {
  if ( ! _pathnames ) {
    // Create first element in array
    ++_tpathnames;
    _pathnames = new char*[_tpathnames];
  } else {
    // Grow array by 1
    char **tmp = new char*[_tpathnames+1];              // create new buffer
    memcpy((void*)tmp, (void*)_pathnames,
                       sizeof(char*)*_tpathnames);      // copy old
    delete[] _pathnames;                                // delete old
    _pathnames = tmp;                                   // use new
    ++_tpathnames;
  }
  _pathnames[_tpathnames-1] = strnew(s);
}

// FREE A PIDL (Pointer to IDentity List)
static void FreePIDL(LPITEMIDLIST pidl) {
  IMalloc *imalloc = NULL;
  if ( SUCCEEDED(SHGetMalloc(&imalloc)) ) {
    imalloc->Free(pidl);
    imalloc->Release();
    imalloc = NULL;
  }
}

// CLEAR MICROSOFT OFN (OPEN FILE NAME) CLASS
void Fl_WinAPI_Native_File_Chooser_Driver::ClearOFN() {
  // Free any previously allocated lpstrFile before zeroing out _ofn_ptr
  if ( _ofn_ptr->lpstrFile ) {
    delete[] _ofn_ptr->lpstrFile;
    _ofn_ptr->lpstrFile = NULL;
  }
  if ( _ofn_ptr->lpstrInitialDir ) {
    delete[] (TCHAR*) _ofn_ptr->lpstrInitialDir; //msvc6 compilation fix
    _ofn_ptr->lpstrInitialDir = NULL;
  }
  _ofn_ptr->lpstrFilter = NULL;         // (deleted elsewhere)
  int temp = _ofn_ptr->nFilterIndex;            // keep the filter_value
  memset((void*)_ofn_ptr, 0, sizeof(OPENFILENAMEW));
  _ofn_ptr->lStructSize  = sizeof(OPENFILENAMEW);
  _ofn_ptr->nFilterIndex = temp;
}

// CLEAR MICROSOFT BINF (BROWSER INFO) CLASS
void Fl_WinAPI_Native_File_Chooser_Driver::ClearBINF() {
  if ( _binf_ptr->pidlRoot ) {
    FreePIDL((ITEMIDLIST*)_binf_ptr->pidlRoot);
    _binf_ptr->pidlRoot = NULL;
  }
  memset((void*)_binf_ptr, 0, sizeof(BROWSEINFOW));
}

// CONVERT WINDOWS BACKSLASHES TO UNIX FRONTSLASHES
void Fl_WinAPI_Native_File_Chooser_Driver::Win2Unix(char *s) {
  while ( (s=strchr(s,'\\')) ) *s = '/';
}

// CONVERT UNIX FRONTSLASHES TO WINDOWS BACKSLASHES
void Fl_WinAPI_Native_File_Chooser_Driver::Unix2Win(char *s) {
  while ( (s=strchr(s,'/')) ) *s = '\\';
}

// SEE IF PATH IS FRONT SLASH OR BACKSLASH STYLE
//    Use this to preserve path style after windows dialog appears.
//    If no slashes are specified, windows is assumed.
//    If a mix of both path styles is used, windows is assumed.
//
bool Fl_WinAPI_Native_File_Chooser_Driver::IsUnixPath(const char *s) {
  if ( !s ) return false;               // NULL?
  if ( strchr(s, '\\') ) return false;  // windows style?
  if ( strchr(s, '/')  ) return true;   // unix style?
  return false;                         // no slashes? assume native windows
}

// SAVE THE CURRENT WORKING DIRECTORY
//     Returns a malloc()ed copy of the cwd that can
//     later be freed with RestoreCWD(). May return 0 on error.
//
static char *SaveCWD() {
  char *thecwd = 0;
  DWORD thecwdsz = GetCurrentDirectory(0,0);
  if ( thecwdsz > 0 ) {
    thecwd = (char*)malloc(thecwdsz);
    if (GetCurrentDirectory(thecwdsz, thecwd) == 0 ) {
      free(thecwd); thecwd = 0;
    }
  }
  return thecwd;
}

// RESTORES THE CWD SAVED BY SaveCWD(), FREES STRING
//    Always returns NULL (string was freed).
//
static void RestoreCWD(char *thecwd) {
  if ( !thecwd ) return;
  SetCurrentDirectory(thecwd);
  free(thecwd);
}

// SHOW FILE BROWSER
int Fl_WinAPI_Native_File_Chooser_Driver::showfile() {
  bool unixpath = IsUnixPath(_directory) || IsUnixPath(_preset_file);    // caller uses unix paths?
  ClearOFN();
  clear_pathnames();
  size_t fsize = FNFC_MAX_PATH;
  _ofn_ptr->Flags |= OFN_NOVALIDATE;    // prevent disabling of front slashes
  _ofn_ptr->Flags |= OFN_HIDEREADONLY;  // hide goofy readonly flag
  // USE NEW BROWSER
  _ofn_ptr->Flags |= OFN_EXPLORER;      // use newer explorer windows
  _ofn_ptr->Flags |= OFN_ENABLESIZING;  // allow window to be resized (hey, why not?)
  _ofn_ptr->Flags |= OFN_NOCHANGEDIR;   // XXX: docs say ineffective on XP/2K/NT, but set it anyway..

  switch ( _btype ) {
    case Fl_Native_File_Chooser::BROWSE_DIRECTORY:
    case Fl_Native_File_Chooser::BROWSE_MULTI_DIRECTORY:
    case Fl_Native_File_Chooser::BROWSE_SAVE_DIRECTORY:
      abort();                          // never happens: handled by showdir()
    case Fl_Native_File_Chooser::BROWSE_FILE:
      break;
    case Fl_Native_File_Chooser::BROWSE_MULTI_FILE:
      _ofn_ptr->Flags |= OFN_ALLOWMULTISELECT;
      break;
    case Fl_Native_File_Chooser::BROWSE_SAVE_FILE:
      if ( options() & Fl_Native_File_Chooser::SAVEAS_CONFIRM && type() == Fl_Native_File_Chooser::BROWSE_SAVE_FILE ) {
          _ofn_ptr->Flags |= OFN_OVERWRITEPROMPT;
      }
      break;
  }
  // SPACE FOR RETURNED FILENAME
  _ofn_ptr->lpstrFile    = new WCHAR[fsize];
  _ofn_ptr->nMaxFile     = (DWORD)(fsize-1);
  _ofn_ptr->lpstrFile[0] = 0;
  _ofn_ptr->lpstrFile[1] = 0;           // dnull
  // PARENT WINDOW
  _ofn_ptr->hwndOwner = GetForegroundWindow();
  // DIALOG TITLE
  if (_title) {
    static WCHAR wtitle[200];
    wcsncpy(wtitle, utf8towchar(_title), 200);
    wtitle[200-1] = 0;
    _ofn_ptr->lpstrTitle =  wtitle;
  } else {
    _ofn_ptr->lpstrTitle = NULL;
  }
  // FILTER
  if (_parsedfilt != NULL) {    // to convert a null-containing char string into a widechar string
    // NEW
    if ( !_wpattern ) _wpattern = new WCHAR[FNFC_MAX_PATH];
    const char *p = _parsedfilt;
    while(*(p + strlen(p) + 1) != 0) p += strlen(p) + 1;
    p += strlen(p) + 2;
    MultiByteToWideChar(CP_UTF8, 0, _parsedfilt, (int) (p - _parsedfilt), _wpattern, FNFC_MAX_PATH);
    _ofn_ptr->lpstrFilter = _wpattern;
  } else {
    _ofn_ptr->lpstrFilter = NULL;
  }
  // PRESET FILE
  //     If set, supercedes _directory. See KB Q86920 for details
  //     XXX: this doesn't preselect the item in the listview.. why?
  //
  if ( _preset_file ) {
    // Temp copy of _dirname we can convert to windows path if needed
    char *winpath = fl_strdup(_preset_file);
    if ( unixpath ) Unix2Win(winpath);
    size_t len = strlen(winpath);
    if ( len >= _ofn_ptr->nMaxFile ) {
      char msg[80];
      snprintf(msg, 80, "preset_file() filename is too long: %ld is >=%ld", (long)len, (long)fsize);
      errmsg(msg);
      return(-1);
    }
    wcscpy(_ofn_ptr->lpstrFile, utf8towchar(winpath));
    len = wcslen(_ofn_ptr->lpstrFile);
    _ofn_ptr->lpstrFile[len+0] = 0;     // multiselect needs dnull
    _ofn_ptr->lpstrFile[len+1] = 0;
    free(winpath);  // free temp copy now that we have a new wchar
    //wprintf(L"lpstrFile is '%ls'\n", (WCHAR*)(_ofn_ptr->lpstrFile));
  }
  // PRESET DIR
  //     XXX: See KB Q86920 for doc bug:
  //     http://support.microsoft.com/default.aspx?scid=kb;en-us;86920
  //
  if ( _directory ) {
    // Temp copy of _dirname we can convert to windows path if needed
    char *winpath = fl_strdup(_directory);
    // Caller specified unix front slash path?
    //     If so, convert to backslashes; windows native browser mishandles unix style paths.
    //     We'll convert back to unix style when dialog completes.
    //
    if ( unixpath ) Unix2Win(winpath);
    // Make a wide char version of potentially utf8 string
    _ofn_ptr->lpstrInitialDir = new WCHAR[FNFC_MAX_PATH];
    wcscpy((WCHAR *)_ofn_ptr->lpstrInitialDir, utf8towchar(winpath));
    free(winpath);  // free temp copy now that we have a new wchar
    //wprintf(L"lpstrInitialDir is '%ls'\n", (WCHAR*)(_ofn_ptr->lpstrInitialDir));
  }
  // SAVE THE CURRENT DIRECTORY
  //     See above warning (XXX) for OFN_NOCHANGEDIR
  //
  char *save_cwd = SaveCWD();           // must be freed with RestoreCWD()
  // OPEN THE DIALOG WINDOW
  int err;
  if ( _btype == Fl_Native_File_Chooser::BROWSE_SAVE_FILE ) {
    err = GetSaveFileNameW(_ofn_ptr);
  } else {
    err = GetOpenFileNameW(_ofn_ptr);
  }
  // GET EXTENDED ERROR
  int exterr = CommDlgExtendedError();
  // RESTORE CURRENT DIRECTORY
  RestoreCWD(save_cwd); save_cwd = 0;   // also frees save_cwd
  // ERROR OR CANCEL?
  if ( err == 0 ) {
    if ( exterr == 0 ) return(1);       // user hit cancel
    // Otherwise, an error occurred..
    char msg[80];
    snprintf(msg, 80, "CommDlgExtendedError() code=%d", exterr);
    errmsg(msg);
    return(-1);
  }
  // PREPARE PATHNAMES FOR RETURN
  switch ( _btype ) {
    case Fl_Native_File_Chooser::BROWSE_FILE:
    case Fl_Native_File_Chooser::BROWSE_SAVE_FILE:
      set_single_pathname(wchartoutf8(_ofn_ptr->lpstrFile));
      if ( unixpath ) Win2Unix(_pathnames[_tpathnames-1]); // preserve unix style path
      break;
    case Fl_Native_File_Chooser::BROWSE_MULTI_FILE: {
      // EXTRACT MULTIPLE FILENAMES
      const WCHAR *dirname = _ofn_ptr->lpstrFile;
      size_t dirlen = wcslen(dirname);
      if ( dirlen > 0 ) {
        // WALK STRING SEARCHING FOR 'DOUBLE-NULL'
        //     eg. "/dir/name\0foo1\0foo2\0foo3\0\0"
        //
        char pathname[FNFC_MAX_PATH];
        for ( const WCHAR *s = dirname + dirlen + 1; *s; s += (wcslen(s)+1)) {
          // ISSUE #206: replace strncpy/cat with fl_strlcpy/cat
          fl_strlcpy(pathname, wchartoutf8(dirname), FNFC_MAX_PATH);
          fl_strlcat(pathname, "\\",                 FNFC_MAX_PATH);
          fl_strlcat(pathname, wchartoutf8(s),       FNFC_MAX_PATH);
          add_pathname(pathname);
        }
      }
      // XXX
      //    Work around problem where pasted forward-slash pathname
      //    into the file browser causes new "Explorer" interface
      //    not to grok forward slashes, passing back as a 'filename'..!
      //
      if ( _tpathnames == 0 ) {
        add_pathname(wchartoutf8(dirname));
      }
      // Caller specified unix path? Return unix paths
      if ( unixpath ) {
        for ( int t=0; t<_tpathnames; t++ ) {
          Win2Unix(_pathnames[t]);
        }
      }
      break;
    }
    case Fl_Native_File_Chooser::BROWSE_DIRECTORY:
    case Fl_Native_File_Chooser::BROWSE_MULTI_DIRECTORY:
    case Fl_Native_File_Chooser::BROWSE_SAVE_DIRECTORY:
      abort();                  // never happens: handled by showdir()
  }
  return(0);
}

// Used by SHBrowseForFolder(), sets initial selected dir.
// Ref: Usenet: microsoft.public.vc.mfc, Dec 8 2000, 1:38p David Lowndes
//              Subject: How to specify to select an initial folder .."
//
static int CALLBACK Dir_CB(HWND win, UINT msg, LPARAM param, LPARAM data) {
  switch (msg) {
    case BFFM_INITIALIZED:
      if (data) ::SendMessageW(win, BFFM_SETSELECTIONW, TRUE, data);
      break;
    case BFFM_SELCHANGED:
      TCHAR path[FNFC_MAX_PATH];
      if ( SHGetPathFromIDList((ITEMIDLIST*)param, path) ) {
        ::SendMessage(win, BFFM_ENABLEOK, 0, 1);
      } else {
        // disable ok button if not a path
        ::SendMessage(win, BFFM_ENABLEOK, 0, 0);
      }
      break;
    case BFFM_VALIDATEFAILED:
      // we could pop up an annoying message here.
      // also needs set ulFlags |= BIF_VALIDATE
      break;
    default:
      break;
  }
  return(0);
}

// SHOW DIRECTORY BROWSER
int Fl_WinAPI_Native_File_Chooser_Driver::showdir() {
  bool unixpath = IsUnixPath(_directory);      // caller uses unix paths?
  // initialize OLE only once
  fl_open_display();            // init needed by BIF_USENEWUI
  ClearBINF();
  clear_pathnames();
  // PARENT WINDOW
  _binf_ptr->hwndOwner = GetForegroundWindow();
  // DIALOG TITLE
  //_binf_ptr->lpszTitle = _title ? _title : NULL;
  if (_title) {
    static WCHAR wtitle[256];
    wcsncpy(wtitle, utf8towchar(_title), 256);
    wtitle[255] = 0;
    _binf_ptr->lpszTitle =  wtitle;
  } else {
    _binf_ptr->lpszTitle = NULL;
  }

  // FLAGS
  _binf_ptr->ulFlags = 0;               // initialize

  // TBD: make sure matches to runtime system, if need be.
  //(what if _WIN32_IE doesn't match system? does the program not run?)
  //
  // TBD: match all 3 types of directories
  //
  // NOTE: *Don't* use BIF_SHAREABLE. It /disables/ mapped network shares
  //       from being visible in BROWSE_DIRECTORY mode. Walter Garm's comments:
  //
  //       --- Garms, Walter (GE EntSol, Security) wrote:
  //       With your help I was able to solve the problem of the network drives.
  //       For Version 6.0, at least, the BIF_SHAREABLE flag seems to have the
  //       opposite sense:  With BIF_SHAREABLE not set I see the mapped network
  //       drives, and with BIF_SHAREABLE set I do not.
  //       ---

#if defined(BIF_NONEWFOLDERBUTTON)                              // Version 6.0
  if ( _btype == Fl_Native_File_Chooser::BROWSE_DIRECTORY ) _binf_ptr->ulFlags |= BIF_NONEWFOLDERBUTTON;
  _binf_ptr->ulFlags |= BIF_USENEWUI | BIF_RETURNONLYFSDIRS;
#elif defined(BIF_USENEWUI)                                     // Version 5.0
  if ( _btype == Fl_Native_File_Chooser::BROWSE_DIRECTORY ) _binf_ptr->ulFlags |= BIF_EDITBOX;
  else if ( _btype == Fl_Native_File_Chooser::BROWSE_SAVE_DIRECTORY ) _binf_ptr->ulFlags |= BIF_USENEWUI;
  _binf_ptr->ulFlags |= BIF_RETURNONLYFSDIRS;
#elif defined(BIF_EDITBOX)                                      // Version 4.71
  _binf_ptr->ulFlags |= BIF_RETURNONLYFSDIRS | BIF_EDITBOX;
#else                                                           // Version Old
  _binf_ptr->ulFlags |= BIF_RETURNONLYFSDIRS;
#endif

  // BUFFER
  //char displayname[FNFC_MAX_PATH];
  WCHAR displayname[FNFC_MAX_PATH];
  _binf_ptr->pszDisplayName = displayname;

  // PRESET DIR
  WCHAR presetname[FNFC_MAX_PATH];
  if ( _directory ) {
    // Temp copy of _dirname we can convert to windows path if needed
    char *winpath = fl_strdup(_directory);
    // Caller specified unix front slash path?
    //     If so, convert to backslashes; windows native browser mishandles unix style paths.
    //     We'll convert back to unix style when dialog completes.
    //
    if ( unixpath ) Unix2Win(winpath);
    // Wide char version of potentially utf8 string
    wcsncpy(presetname, utf8towchar(winpath), FNFC_MAX_PATH);
    free(winpath);  // free temp copy now that we have a new wchar
    presetname[FNFC_MAX_PATH-1] = 0; // dnull
    presetname[FNFC_MAX_PATH-2] = 0;
    _binf_ptr->lParam = (LPARAM)presetname;
    //wprintf(L"presetname is '%ls'\n", (WCHAR*)(presetname));
  }
  else _binf_ptr->lParam = 0;
  _binf_ptr->lpfn = Dir_CB;
  // OPEN BROWSER
  LPITEMIDLIST pidl = SHBrowseForFolderW(_binf_ptr);
  // CANCEL?
  if ( pidl == NULL ) return(1);

  // GET THE PATHNAME(S) THE USER SELECTED
  // TBD: expand NetHood shortcuts from this PIDL??
  // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/functions/shbrowseforfolder.asp

  WCHAR path[FNFC_MAX_PATH];
  if ( SHGetPathFromIDListW(pidl, path) ) {
    add_pathname(wchartoutf8(path));
    if ( unixpath ) Win2Unix(_pathnames[_tpathnames-1]); // preserve unix style path
  }
  FreePIDL(pidl);
  if ( !wcslen(path) ) return(1);             // don't return empty pathnames
  return(0);
}

// RETURNS:
//    0 - user picked a file
//    1 - user cancelled
//   -1 - failed; errmsg() has reason
//
int Fl_WinAPI_Native_File_Chooser_Driver::show() {
  int retval;
  if ( _btype == Fl_Native_File_Chooser::BROWSE_DIRECTORY ||
       _btype == Fl_Native_File_Chooser::BROWSE_MULTI_DIRECTORY ||
       _btype == Fl_Native_File_Chooser::BROWSE_SAVE_DIRECTORY ) {
    retval = showdir();
  } else {
    retval = showfile();
  }
  // restore the correct state of mouse buttons and keyboard modifier keys (STR #3221)
  HWND h = GetForegroundWindow();
  if (h) {
    WNDPROC windproc = (WNDPROC)GetWindowLongPtrW(h, GWLP_WNDPROC);
    CallWindowProc(windproc, h, WM_ACTIVATEAPP, 1, 0);
  }
  return retval;
}

// RETURN ERROR MESSAGE
const char *Fl_WinAPI_Native_File_Chooser_Driver::errmsg() const {
  return(_errmsg ? _errmsg : "No error");
}

// GET FILENAME
const char* Fl_WinAPI_Native_File_Chooser_Driver::filename() const {
  if ( _pathnames && _tpathnames > 0 ) return(_pathnames[0]);
  return("");
}

// GET FILENAME FROM LIST OF FILENAMES
const char* Fl_WinAPI_Native_File_Chooser_Driver::filename(int i) const {
  if ( _pathnames && i < _tpathnames ) return(_pathnames[i]);
  return("");
}

// GET TOTAL FILENAMES CHOSEN
int Fl_WinAPI_Native_File_Chooser_Driver::count() const {
  return(_tpathnames);
}

// PRESET PATHNAME
//     Can be NULL if no preset is desired.
//
void Fl_WinAPI_Native_File_Chooser_Driver::directory(const char *val) {
  _directory = strfree(_directory);
  _directory = strnew(val);
}

// GET PRESET PATHNAME
//    Can return NULL if none set.
//
const char *Fl_WinAPI_Native_File_Chooser_Driver::directory() const {
  return(_directory);
}

// SET TITLE
//     Can be NULL if no title desired.
//
void Fl_WinAPI_Native_File_Chooser_Driver::title(const char *val) {
  _title = strfree(_title);
  _title = strnew(val);
}

// GET TITLE
//    Can return NULL if none set.
//
const char *Fl_WinAPI_Native_File_Chooser_Driver::title() const {
  return(_title);
}

// SET FILTER
//     Can be NULL if no filter needed
//
void Fl_WinAPI_Native_File_Chooser_Driver::filter(const char *val) {
  _filter = strfree(_filter);
  clear_filters();
  if ( val ) {
    _filter = strnew(val);
    parse_filter(_filter);
  }
  add_filter("All Files", "*.*");       // always include 'all files' option

#ifdef DEBUG
  dnullprint(_parsedfilt);
#endif /*DEBUG*/
}

// GET FILTER
//    Can return NULL if none set.
//
const char *Fl_WinAPI_Native_File_Chooser_Driver::filter() const {
  return(_filter);
}

// CLEAR FILTERS
void Fl_WinAPI_Native_File_Chooser_Driver::clear_filters() {
  _nfilters = 0;
  _parsedfilt = strfree(_parsedfilt);
}

// ADD A FILTER
void Fl_WinAPI_Native_File_Chooser_Driver::add_filter(const char *name_in,      // name of filter (optional: can be null)
                            const char *winfilter) {            // windows style filter (eg. "*.cxx;*.h")
  // No name? Make one..
  char name[1024];
  if ( !name_in || name_in[0] == '\0' ) {
    snprintf(name, sizeof(name), "%.*s Files", int(sizeof(name)-10), winfilter);
  } else {
    if ((strlen(name_in)+strlen(winfilter)+3) < sizeof(name)) {
      snprintf(name, sizeof(name), "%s (%s)", name_in, winfilter);
    } else {
      snprintf(name, sizeof(name), "%.*s", int(sizeof(name))-1, name_in);
    }
  }
  dnullcat(_parsedfilt, name);
  dnullcat(_parsedfilt, winfilter);
  _nfilters++;
  //DEBUG printf("DEBUG: ADD FILTER name=<%s> winfilter=<%s>\n", name, winfilter);
}

// RETURN HOW MANY DIFFERENT FILTERS WERE SPECIFIED
//   In: "foo.[CH]" or "foo.{C,H}"
//   Out: 2
//
static int count_filters(const char *filter) {
  int count = 0;
  char mode = 0;
  const char *in = filter;
  while (*in) {
    switch(*in) {
      case '\\':                        // escape next character
        ++in; if ( *in == 0 ) continue; // skip escape. EOL? done
        ++in;                           // skip escaped char
        continue;
      case LCURLY_CHR:                  // start "{aaa,bbb}"
        mode = *in;                     // set mode, parse over curly
        ++count;                        // at least +1 wildcard
        break;
      case RCURLY_CHR:                  // end "{aaa,bbb}"
        if ( mode == LCURLY_CHR )       // disable curly mode (if on)
          mode = 0;
        break;
      case LBRACKET_CHR:                // start "[xyz]"
        mode = *in;                     // set mode, parse over bracket
        break;
      case RBRACKET_CHR:                // end "[xyz]"
        if ( mode == LBRACKET_CHR )     // disable bracket mode (if on)
          mode = 0;
        break;
      default:                          // any other char
        switch (mode) {                 // handle {} or [] modes
          case LCURLY_CHR:              // handle "{aaa,bbb}"
            if (*in==',' || *in=='|')   // ',' and '|' adds filters
              ++count;
            break;
          case LBRACKET_CHR:            // handle "[xyz]"
            ++count;                    // all chars in []'s add new filter
            break;
        }
        break;
    }
    ++in;                               // parse past char
  }
  return count > 0 ? count : 1;         // return at least 1
}

// Convert FLTK style pattern matches to windows 'double-null' pattern
// Returns with the parsed double-null result in '_parsedfilt'.
//
//    Handles:
//        IN              OUT
//        -----------     -----------------------------
//        *.{ma,mb}       "*.{ma,mb} Files\0*.ma;*.mb\0\0"
//        *.[abc]         "*.[abc] Files\0*.a;*.b;*.c\0\0"
//        *.txt           "*.txt Files\0*.txt\0\0"
//        C Files\t*.[ch] "C Files\0*.c;*.h\0\0"
//
//    Example:
//         IN: "*.{ma,mb}"
//        OUT: "*.ma;*.mb Files\0*.ma;*.mb\0All Files\0*.*\0\0"
//              ---------------  ---------  ---------  ---
//                     |             |          |       |
//                   Title       Wildcards    Title    Wildcards
//
// Parsing Mode:
//         IN:"C Files\t*.{cxx,h}"
//             |||||||  |||||||||
//       mode: nnnnnnn  ww{{{{{{{
//             \_____/  \_______/
//              Name     Wildcard
//
void Fl_WinAPI_Native_File_Chooser_Driver::parse_filter(const char *in) {
  clear_filters();
  if ( ! in || in[0] == '\0' ) return;

  int has_name = strchr(in, '\t') ? 1 : 0;
  char mode = has_name ? 'n' : 'w';             // parse mode: n=name, w=wildcard

  // whatever input string is, our output won't be much longer in length..
  // use double length just for safety.
  size_t slen = strlen(in);
  char *wildprefix = new char[(slen+1)*2]; wildprefix[0] = 0;
  char *comp       = new char[(slen+1)*2]; comp[0] = 0;
  char *name       = new char[(slen+1)*2]; name[0] = 0;

  // Init
  int nwildcards = 0;
  int maxfilters = count_filters(in) + 1;       // count wildcard seps
  char **wildcards = new char*[maxfilters];     // parsed wildcards (can be several)
  int t;
  for ( t=0; t<maxfilters; t++ ) {
    wildcards[t] = new char[slen+1];
    wildcards[t][0] = '\0';
  }

  // Parse
  for ( ; 1; in++ ) {

    // DEBUG
    // printf("WORKING ON '%c': mode=<%c> name=<%s> wildprefix=<%s> nwildcards=%d wildcards[n]=<%s>\n",
    //        *in, mode, name, wildprefix, nwildcards, wildcards[nwildcards]);

    switch (*in) {
      case ',':
      case '|':
        if ( mode == LCURLY_CHR ) {
          // create new wildcard, copy in prefix
          strcat(wildcards[nwildcards++], wildprefix);
          continue;
        } else {
          goto regchar;
        }
        continue;

      // FINISHED PARSING A NAME?
      case '\t':
        if ( mode != 'n' ) goto regchar;
        // finish parsing name? switch to wildcard mode
        mode = 'w';
        break;

      // ESCAPE NEXT CHAR
      case '\\':
        ++in;
        goto regchar;

      // FINISHED PARSING ONE OF POSSIBLY SEVERAL FILTERS?
      case '\r':
      case '\n':
      case '\0':
      {
        if ( mode == 'w' ) {            // finished parsing wildcard?
          if ( nwildcards == 0 ) {
            strcpy(wildcards[nwildcards++], wildprefix);
          }
          // Append wildcards in Microsoft's "*.one;*.two" format
          comp[0] = 0;
          for ( t=0; t<nwildcards; t++ ) {
            if ( t != 0 ) strcat(comp, ";");
            strcat(comp, wildcards[t]);
          }
          // Add if not empty
          if ( comp[0] ) {
            add_filter(name, comp);
          }
        }
        // RESET
        for ( t=0; t<maxfilters; t++ ) {
          wildcards[t][0] = '\0';
        }
        nwildcards = 0;
        wildprefix[0] = name[0] = '\0';
        mode = strchr(in,'\t') ? 'n' : 'w';
        // DONE?
        if ( *in == '\0' ) {            // done
          // Free everything
          delete[] wildprefix;
          delete[] comp;
          delete[] name;
          for ( t=0; t<maxfilters; t++ ) delete[] wildcards[t];
          delete[] wildcards;
          return;
        }
        continue;                       // not done yet, more filters
      }

      // STARTING A WILDCARD?
      case LBRACKET_CHR:
      case LCURLY_CHR:
        mode = *in;
        if ( *in == LCURLY_CHR ) {
          // create new wildcard
          strcat(wildcards[nwildcards++], wildprefix);
        }
        continue;

      // ENDING A WILDCARD?
      case RBRACKET_CHR:
      case RCURLY_CHR:
        mode = 'w';     // back to wildcard mode
        continue;

      // ALL OTHER NON-SPECIAL CHARACTERS
      default:
      regchar:          // handle regular char
        switch ( mode ) {
          case LBRACKET_CHR:
            // create new wildcard
            ++nwildcards;
            // copy in prefix
            strcpy(wildcards[nwildcards-1], wildprefix);
            // append search char
            chrcat(wildcards[nwildcards-1], *in);
            continue;

          case LCURLY_CHR:
            if ( nwildcards > 0 ) {
              chrcat(wildcards[nwildcards-1], *in);
            }
            continue;

          case 'n':
            chrcat(name, *in);
            continue;

          case 'w':
            chrcat(wildprefix, *in);
            for ( t=0; t<nwildcards; t++ ) {
              chrcat(wildcards[t], *in);
            }
            continue;
        }
        break;
    }
  }
}

// SET 'CURRENTLY SELECTED FILTER'
void Fl_WinAPI_Native_File_Chooser_Driver::filter_value(int i) {
  _ofn_ptr->nFilterIndex = i + 1;
}

// RETURN VALUE OF 'CURRENTLY SELECTED FILTER'
int Fl_WinAPI_Native_File_Chooser_Driver::filter_value() const {
  return(_ofn_ptr->nFilterIndex ? _ofn_ptr->nFilterIndex-1 : _nfilters+1);
}

// PRESET FILENAME FOR 'SAVE AS' CHOOSER
void Fl_WinAPI_Native_File_Chooser_Driver::preset_file(const char* val) {
  _preset_file = strfree(_preset_file);
  _preset_file = strnew(val);
}

// GET PRESET FILENAME FOR 'SAVE AS' CHOOSER
const char* Fl_WinAPI_Native_File_Chooser_Driver::preset_file() const {
  return(_preset_file);
}

int Fl_WinAPI_Native_File_Chooser_Driver::filters() const {
  return(_nfilters);
}

static char *wchartoutf8(LPCWSTR in) {
  static char *out = NULL;
  static int lchar = 0;
  if (in == NULL)return NULL;
  int utf8len  = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL);
  if (utf8len > lchar) {
    lchar = utf8len;
    out = (char *)realloc(out, lchar * sizeof(char));
  }
  WideCharToMultiByte(CP_UTF8, 0, in, -1, out, utf8len, NULL, NULL);
  return out;
}

static LPCWSTR utf8towchar(const char *in) {
  static WCHAR *wout = NULL;
  static int lwout = 0;
  if (in == NULL)return NULL;
  int wlen = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0);
  if (wlen > lwout) {
    lwout = wlen;
    wout = (WCHAR *)realloc(wout, lwout * sizeof(WCHAR));
  }
  MultiByteToWideChar(CP_UTF8, 0, in, -1, wout, wlen);
  return wout;
}

#endif /* !FL_DOXYGEN */
