/******************************************************************************
 *
 * Project:  OpenGIS Simple Features Reference Implementation
 * Purpose:  Implements FileGDB OGR driver.
 * Author:   Ragi Yaser Burhum, ragi@burhum.com
 *           Paul Ramsey, pramsey at cleverelephant.ca
 *
 ******************************************************************************
 * Copyright (c) 2010, Ragi Yaser Burhum
 * Copyright (c) 2011, Paul Ramsey <pramsey at cleverelephant.ca>
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "ogr_fgdb.h"
#include "cpl_conv.h"
#include "FGdbUtils.h"
#include "cpl_multiproc.h"
#include "ogrmutexeddatasource.h"

CPL_CVSID("$Id: FGdbDriver.cpp 1761acd90777d5bcc49eddbc13c193098f0ed40b 2020-10-01 12:12:00 +0200 Even Rouault $")

extern "C" void RegisterOGRFileGDB();

/************************************************************************/
/*                            FGdbDriver()                              */
/************************************************************************/
FGdbDriver::FGdbDriver(): OGRSFDriver(), hMutex(nullptr)
{
}

/************************************************************************/
/*                            ~FGdbDriver()                             */
/************************************************************************/
FGdbDriver::~FGdbDriver()

{
    if( !oMapConnections.empty() )
        CPLDebug("FileGDB", "Remaining %d connections. Bug?",
                 (int)oMapConnections.size());
    if( hMutex != nullptr )
        CPLDestroyMutex(hMutex);
    hMutex = nullptr;
}

/************************************************************************/
/*                              GetName()                               */
/************************************************************************/

const char *FGdbDriver::GetName()

{
    return "FileGDB";
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

OGRDataSource *FGdbDriver::Open( const char* pszFilename, int bUpdate )

{
    // First check if we have to do any work.
    size_t nLen = strlen(pszFilename);
    if( nLen == 1 && pszFilename[0] == '.' )
    {
        char* pszCurrentDir = CPLGetCurrentDir();
        if( pszCurrentDir )
        {
            size_t nLen2 = strlen(pszCurrentDir);
            bool bOK = (nLen2 >= 4 && EQUAL(pszCurrentDir + nLen2 - 4, ".gdb"));
            CPLFree(pszCurrentDir);
            if( !bOK )
                return nullptr;
        }
        else
        {
            return nullptr;
        }
    }
    else if(! ((nLen >= 4 && EQUAL(pszFilename + nLen - 4, ".gdb")) ||
               (nLen >= 5 && EQUAL(pszFilename + nLen - 5, ".gdb/"))) )
        return nullptr;

    long hr;

    /* Check that the filename is really a directory, to avoid confusion with */
    /* Garmin MapSource - gdb format which can be a problem when the FileGDB */
    /* driver is loaded as a plugin, and loaded before the GPSBabel driver */
    /* (http://trac.osgeo.org/osgeo4w/ticket/245) */
    VSIStatBuf stat;
    if( CPLStat( pszFilename, &stat ) != 0 || !VSI_ISDIR(stat.st_mode) )
    {
        return nullptr;
    }

    CPLMutexHolderD(&hMutex);

    FGdbDatabaseConnection* pConnection = oMapConnections[pszFilename];
    if( pConnection != nullptr )
    {
        if( pConnection->IsFIDHackInProgress() )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Cannot open geodatabase at the moment since it is in 'FID hack mode'");
            return nullptr;
        }

        pConnection->m_nRefCount ++;
        CPLDebug("FileGDB", "ref_count of %s = %d now", pszFilename,
                 pConnection->m_nRefCount);
    }
    else
    {
        Geodatabase* pGeoDatabase = new Geodatabase;
        hr = ::OpenGeodatabase(StringToWString(pszFilename), *pGeoDatabase);

        if (FAILED(hr))
        {
            delete pGeoDatabase;

            if( OGRGetDriverByName("OpenFileGDB") != nullptr && bUpdate == FALSE )
            {
                std::wstring fgdb_error_desc_w;
                std::string fgdb_error_desc("Unknown error");
                fgdbError er;
                er = FileGDBAPI::ErrorInfo::GetErrorDescription(static_cast<fgdbError>(hr), fgdb_error_desc_w);
                if ( er == S_OK )
                {
                    fgdb_error_desc = WStringToString(fgdb_error_desc_w);
                }
                CPLDebug("FileGDB", "Cannot open %s with FileGDB driver: %s. Failing silently so OpenFileGDB can be tried",
                         pszFilename,
                         fgdb_error_desc.c_str());
            }
            else
            {
                GDBErr(hr, "Failed to open Geodatabase");
            }
            oMapConnections.erase(pszFilename);
            return nullptr;
        }

        CPLDebug("FileGDB", "Really opening %s", pszFilename);
        pConnection = new FGdbDatabaseConnection(pszFilename, pGeoDatabase);
        oMapConnections[pszFilename] = pConnection;
    }

    FGdbDataSource* pDS;

    pDS = new FGdbDataSource(this, pConnection);

    if(!pDS->Open( pszFilename, bUpdate, nullptr ) )
    {
        delete pDS;
        return nullptr;
    }
    else
    {
        OGRMutexedDataSource* poMutexedDS =
                new OGRMutexedDataSource(pDS, TRUE, hMutex, TRUE);
        if( bUpdate )
            return OGRCreateEmulatedTransactionDataSourceWrapper(poMutexedDS, this, TRUE, FALSE);
        else
            return poMutexedDS;
    }
}

/***********************************************************************/
/*                     CreateDataSource()                              */
/***********************************************************************/

OGRDataSource* FGdbDriver::CreateDataSource( const char * conn,
                                           char **papszOptions)
{
    long hr;
    Geodatabase *pGeodatabase;
    std::wstring wconn = StringToWString(conn);
    int bUpdate = TRUE; // If we're creating, we must be writing.
    VSIStatBuf stat;

    CPLMutexHolderD(&hMutex);

    /* We don't support options yet, so warn if they send us some */
    if ( papszOptions )
    {
        /* TODO: warning, ignoring options */
    }

    /* Only accept names of form "filename.gdb" and */
    /* also .gdb.zip to be able to return FGDB with MapServer OGR output (#4199) */
    const char* pszExt = CPLGetExtension(conn);
    if ( !(EQUAL(pszExt,"gdb") || EQUAL(pszExt, "zip")) )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "FGDB data source name must use 'gdb' extension.\n" );
        return nullptr;
    }

    /* Don't try to create on top of something already there */
    if( CPLStat( conn, &stat ) == 0 )
    {
        CPLError( CE_Failure, CPLE_AppDefined,
                  "%s already exists.\n", conn );
        return nullptr;
    }

    /* Try to create the geodatabase */
    pGeodatabase = new Geodatabase; // Create on heap so we can store it in the Datasource
    hr = CreateGeodatabase(wconn, *pGeodatabase);

    /* Handle creation errors */
    if ( S_OK != hr )
    {
        const char *errstr = "Error creating geodatabase (%s).\n";
        if ( hr == -2147220653 )
            errstr = "File already exists (%s).\n";
        delete pGeodatabase;
        CPLError( CE_Failure, CPLE_AppDefined, errstr, conn );
        return nullptr;
    }

    FGdbDatabaseConnection* pConnection = new FGdbDatabaseConnection(conn, pGeodatabase);
    oMapConnections[conn] = pConnection;

    /* Ready to embed the Geodatabase in an OGR Datasource */
    FGdbDataSource* pDS = new FGdbDataSource(this, pConnection);
    if ( ! pDS->Open(conn, bUpdate, nullptr) )
    {
        delete pDS;
        return nullptr;
    }
    else
        return OGRCreateEmulatedTransactionDataSourceWrapper(
            new OGRMutexedDataSource(pDS, TRUE, hMutex, TRUE), this,
            TRUE, FALSE);
}

/************************************************************************/
/*                           StartTransaction()                         */
/************************************************************************/

OGRErr FGdbDriver::StartTransaction(OGRDataSource*& poDSInOut, int& bOutHasReopenedDS)
{
    CPLMutexHolderOptionalLockD(hMutex);

    bOutHasReopenedDS = FALSE;

    OGRMutexedDataSource* poMutexedDS = (OGRMutexedDataSource*)poDSInOut;
    FGdbDataSource* poDS = (FGdbDataSource* )poMutexedDS->GetBaseDataSource();
    if( !poDS->GetUpdate() )
        return OGRERR_FAILURE;
    FGdbDatabaseConnection* pConnection = poDS->GetConnection();
    if( pConnection->GetRefCount() != 1 )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot start transaction as database is opened in another connection");
        return OGRERR_FAILURE;
    }
    if( pConnection->IsLocked() )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Transaction is already in progress");
        return OGRERR_FAILURE;
    }

    bOutHasReopenedDS = TRUE;

    CPLString osName(poMutexedDS->GetName());
    CPLString osNameOri(osName);
    if( osName.back()== '/' || osName.back()== '\\' )
        osName.resize(osName.size()-1);

#ifndef WIN32
    int bPerLayerCopyingForTransaction = poDS->HasPerLayerCopyingForTransaction();
#endif

    pConnection->m_nRefCount ++;
    delete poDSInOut;
    poDSInOut = nullptr;
    poMutexedDS = nullptr;
    poDS = nullptr;

    pConnection->CloseGeodatabase();

    CPLString osEditedName(osName);
    osEditedName += ".ogredited";

    CPLPushErrorHandler(CPLQuietErrorHandler);
    CPL_IGNORE_RET_VAL(CPLUnlinkTree(osEditedName));
    CPLPopErrorHandler();

    OGRErr eErr = OGRERR_NONE;

    CPLString osDatabaseToReopen;
#ifndef WIN32
    if( bPerLayerCopyingForTransaction )
    {
        int bError = FALSE;

        if( VSIMkdir( osEditedName, 0755 ) != 0 )
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "Cannot create directory '%s'.",
                      osEditedName.c_str() );
            bError = TRUE;
        }

        // Only copy a0000000X.Y files with X >= 1 && X <= 8, gdb and timestamps
        // and symlink others
        char** papszFiles = VSIReadDir(osName);
        for(char** papszIter = papszFiles; !bError && *papszIter; ++papszIter)
        {
            if( strcmp(*papszIter, ".") == 0 || strcmp(*papszIter, "..") == 0 )
                continue;
            if( ((*papszIter)[0] == 'a' && atoi((*papszIter)+1) >= 1 &&
                 atoi((*papszIter)+1) <= 8) || EQUAL(*papszIter, "gdb") ||
                 EQUAL(*papszIter, "timestamps") )
            {
                if( CPLCopyFile(CPLFormFilename(osEditedName, *papszIter, nullptr),
                                CPLFormFilename(osName, *papszIter, nullptr)) != 0 )
                {
                    bError = TRUE;
                    CPLError(CE_Failure, CPLE_AppDefined,
                             "Cannot copy %s", *papszIter);
                }
            }
            else
            {
                CPLString osSourceFile;
                if( CPLIsFilenameRelative(osName) )
                    osSourceFile = CPLFormFilename(CPLSPrintf("../%s", CPLGetFilename(osName.c_str())), *papszIter, nullptr);
                else
                    osSourceFile = osName;
                if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE1") ||
                    CPLSymlink( osSourceFile,
                                CPLFormFilename(osEditedName.c_str(), *papszIter, nullptr),
                                nullptr ) != 0 )
                {
                    bError = TRUE;
                    CPLError(CE_Failure, CPLE_AppDefined,
                             "Cannot symlink %s", *papszIter);
                }
            }
        }
        CSLDestroy(papszFiles);

        if( bError )
        {
            eErr = OGRERR_FAILURE;
            osDatabaseToReopen = osName;
        }
        else
            osDatabaseToReopen = osEditedName;
    }
    else
#endif
    {
        if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE1") ||
            CPLCopyTree( osEditedName, osName ) != 0 )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Cannot backup geodatabase");
            eErr = OGRERR_FAILURE;
            osDatabaseToReopen = osName;
        }
        else
            osDatabaseToReopen = osEditedName;
    }

    pConnection->m_pGeodatabase = new Geodatabase;
    long hr = ::OpenGeodatabase(StringToWString(osDatabaseToReopen), *(pConnection->m_pGeodatabase));
    if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE2") || FAILED(hr))
    {
        delete pConnection->m_pGeodatabase;
        pConnection->m_pGeodatabase = nullptr;
        Release(osName);
        GDBErr(hr, CPLSPrintf("Failed to open %s. Dataset should be closed",
                              osDatabaseToReopen.c_str()));

        return OGRERR_FAILURE;
    }

    FGdbDataSource* pDS = new FGdbDataSource(this, pConnection);
    pDS->Open(osDatabaseToReopen, TRUE, osNameOri);

#ifndef WIN32
    if( eErr == OGRERR_NONE && bPerLayerCopyingForTransaction )
    {
        pDS->SetPerLayerCopyingForTransaction(bPerLayerCopyingForTransaction);
        pDS->SetSymlinkFlagOnAllLayers();
    }
#endif

    poDSInOut = new OGRMutexedDataSource(pDS, TRUE, hMutex, TRUE);

    if( eErr == OGRERR_NONE )
        pConnection->SetLocked(TRUE);
    return eErr;
}

/************************************************************************/
/*                           CommitTransaction()                        */
/************************************************************************/

OGRErr FGdbDriver::CommitTransaction(OGRDataSource*& poDSInOut, int& bOutHasReopenedDS)
{
    CPLMutexHolderOptionalLockD(hMutex);

    bOutHasReopenedDS = FALSE;

    OGRMutexedDataSource* poMutexedDS = (OGRMutexedDataSource*)poDSInOut;
    FGdbDataSource* poDS = (FGdbDataSource* )poMutexedDS->GetBaseDataSource();
    FGdbDatabaseConnection* pConnection = poDS->GetConnection();
    if( !pConnection->IsLocked() )
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "No transaction in progress");
        return OGRERR_FAILURE;
    }

    bOutHasReopenedDS = TRUE;

    CPLString osName(poMutexedDS->GetName());
    CPLString osNameOri(osName);
    if( osName.back()== '/' || osName.back()== '\\' )
        osName.resize(osName.size()-1);

#ifndef WIN32
    int bPerLayerCopyingForTransaction = poDS->HasPerLayerCopyingForTransaction();
#endif

    pConnection->m_nRefCount ++;
    delete poDSInOut;
    poDSInOut = nullptr;
    poMutexedDS = nullptr;
    poDS = nullptr;

    pConnection->CloseGeodatabase();

    CPLString osEditedName(osName);
    osEditedName += ".ogredited";

#ifndef WIN32
    if( bPerLayerCopyingForTransaction )
    {
        int bError = FALSE;
        char** papszFiles;
        std::vector<CPLString> aosTmpFilesToClean;

        // Check for files present in original copy that are not in edited copy
        // That is to say deleted layers
        papszFiles = VSIReadDir(osName);
        for(char** papszIter = papszFiles; !bError && *papszIter; ++papszIter)
        {
            if( strcmp(*papszIter, ".") == 0 || strcmp(*papszIter, "..") == 0 )
                continue;
            VSIStatBufL sStat;
            if( (*papszIter)[0] == 'a' &&
                VSIStatL( CPLFormFilename(osEditedName, *papszIter, nullptr), &sStat ) != 0 )
            {
                if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE1") ||
                    VSIRename( CPLFormFilename(osName, *papszIter, nullptr),
                               CPLFormFilename(osName, *papszIter, "tmp") ) != 0 )
                {
                    CPLError(CE_Failure, CPLE_AppDefined, "Cannot rename %s to %s",
                             CPLFormFilename(osName, *papszIter, nullptr),
                             CPLFormFilename(osName, *papszIter, "tmp"));
                    bError = TRUE;
                }
                else
                    aosTmpFilesToClean.push_back(CPLFormFilename(osName, *papszIter, "tmp"));
            }
        }
        CSLDestroy(papszFiles);

        // Move modified files from edited directory to main directory
        papszFiles = VSIReadDir(osEditedName);
        for(char** papszIter = papszFiles; !bError && *papszIter; ++papszIter)
        {
            if( strcmp(*papszIter, ".") == 0 || strcmp(*papszIter, "..") == 0 )
                continue;
            struct stat sStat;
            if( lstat( CPLFormFilename(osEditedName, *papszIter, nullptr), &sStat ) != 0 )
            {
                CPLError(CE_Failure, CPLE_AppDefined, "Cannot stat %s",
                         CPLFormFilename(osEditedName, *papszIter, nullptr));
                bError = TRUE;
            }
            else if( !S_ISLNK(sStat.st_mode) )
            {
                // If there was such a file in original directory, first rename it
                // as a temporary file
                if( lstat( CPLFormFilename(osName, *papszIter, nullptr), &sStat ) == 0 )
                {
                    if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE2") ||
                        VSIRename( CPLFormFilename(osName, *papszIter, nullptr),
                                   CPLFormFilename(osName, *papszIter, "tmp") ) != 0 )
                    {
                        CPLError(CE_Failure, CPLE_AppDefined, "Cannot rename %s to %s",
                                 CPLFormFilename(osName, *papszIter, nullptr),
                                 CPLFormFilename(osName, *papszIter, "tmp"));
                        bError = TRUE;
                    }
                    else
                        aosTmpFilesToClean.push_back(CPLFormFilename(osName, *papszIter, "tmp"));
                }
                if( !bError )
                {
                    if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE3") ||
                        CPLMoveFile( CPLFormFilename(osName, *papszIter, nullptr),
                                     CPLFormFilename(osEditedName, *papszIter, nullptr) ) != 0 )
                    {
                        CPLError(CE_Failure, CPLE_AppDefined, "Cannot move %s to %s",
                                 CPLFormFilename(osEditedName, *papszIter, nullptr),
                                 CPLFormFilename(osName, *papszIter, nullptr));
                        bError = TRUE;
                    }
                    else
                        CPLDebug("FileGDB", "Move %s to %s",
                                 CPLFormFilename(osEditedName, *papszIter, nullptr),
                                 CPLFormFilename(osName, *papszIter, nullptr));
                }
            }
        }
        CSLDestroy(papszFiles);

        if( !bError )
        {
            for(size_t i=0;i<aosTmpFilesToClean.size();i++)
            {
                if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE4") ||
                    VSIUnlink(aosTmpFilesToClean[i]) != 0 )
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Cannot remove %s. Manual cleanup required", aosTmpFilesToClean[i].c_str());
                }
            }
        }

        if( bError )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "An error occurred while moving files from %s back to %s. "
                     "Manual cleaning must be done and dataset should be closed",
                     osEditedName.c_str(),
                     osName.c_str());
            pConnection->SetLocked(FALSE);
            Release(osName);
            return OGRERR_FAILURE;
        }
        else if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE5") ||
                 CPLUnlinkTree(osEditedName) != 0 )
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                    "Cannot remove %s. Manual cleanup required", osEditedName.c_str());
        }
    }
    else
#endif
    {
        CPLString osTmpName(osName);
        osTmpName += ".ogrtmp";

        /* Install the backup copy as the main database in 3 steps : */
        /* first rename the main directory  in .tmp */
        /* then rename the edited copy under regular name */
        /* and finally dispose the .tmp directory */
        /* That way there's no risk definitely losing data */
        if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE1") ||
            VSIRename(osName, osTmpName) != 0 )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Cannot rename %s to %s. Edited database during transaction is in %s"
                    "Dataset should be closed",
                    osName.c_str(), osTmpName.c_str(), osEditedName.c_str());
            pConnection->SetLocked(FALSE);
            Release(osName);
            return OGRERR_FAILURE;
        }

        if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE2") ||
            VSIRename(osEditedName, osName) != 0 )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Cannot rename %s to %s. The original geodatabase is in '%s'. "
                    "Dataset should be closed",
                    osEditedName.c_str(), osName.c_str(), osTmpName.c_str());
            pConnection->SetLocked(FALSE);
            Release(osName);
            return OGRERR_FAILURE;
        }

        if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE3") ||
            CPLUnlinkTree(osTmpName) != 0 )
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                    "Cannot remove %s. Manual cleanup required", osTmpName.c_str());
        }
    }

    pConnection->m_pGeodatabase = new Geodatabase;
    long hr = ::OpenGeodatabase(StringToWString(osName), *(pConnection->m_pGeodatabase));
    if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE_REOPEN") || FAILED(hr))
    {
        delete pConnection->m_pGeodatabase;
        pConnection->m_pGeodatabase = nullptr;
        pConnection->SetLocked(FALSE);
        Release(osName);
        GDBErr(hr, "Failed to re-open Geodatabase. Dataset should be closed");
        return OGRERR_FAILURE;
    }

    FGdbDataSource* pDS = new FGdbDataSource(this, pConnection);
    pDS->Open(osNameOri, TRUE, nullptr);
    //pDS->SetPerLayerCopyingForTransaction(bPerLayerCopyingForTransaction);
    poDSInOut = new OGRMutexedDataSource(pDS, TRUE, hMutex, TRUE);

    pConnection->SetLocked(FALSE);

    return OGRERR_NONE;
}

/************************************************************************/
/*                           RollbackTransaction()                      */
/************************************************************************/

OGRErr FGdbDriver::RollbackTransaction(OGRDataSource*& poDSInOut, int& bOutHasReopenedDS)
{
    CPLMutexHolderOptionalLockD(hMutex);

    bOutHasReopenedDS = FALSE;

    OGRMutexedDataSource* poMutexedDS = (OGRMutexedDataSource*)poDSInOut;
    FGdbDataSource* poDS = (FGdbDataSource* )poMutexedDS->GetBaseDataSource();
    FGdbDatabaseConnection* pConnection = poDS->GetConnection();
    if( !pConnection->IsLocked() )
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "No transaction in progress");
        return OGRERR_FAILURE;
    }

    bOutHasReopenedDS = TRUE;

    CPLString osName(poMutexedDS->GetName());
    CPLString osNameOri(osName);
    if( osName.back()== '/' || osName.back()== '\\' )
        osName.resize(osName.size()-1);

    //int bPerLayerCopyingForTransaction = poDS->HasPerLayerCopyingForTransaction();

    pConnection->m_nRefCount ++;
    delete poDSInOut;
    poDSInOut = nullptr;
    poMutexedDS = nullptr;
    poDS = nullptr;

    pConnection->CloseGeodatabase();

    CPLString osEditedName(osName);
    osEditedName += ".ogredited";

    OGRErr eErr = OGRERR_NONE;
    if( EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE1") ||
        CPLUnlinkTree(osEditedName) != 0 )
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Cannot remove %s. Manual cleanup required", osEditedName.c_str());
        eErr = OGRERR_FAILURE;
    }

    pConnection->m_pGeodatabase = new Geodatabase;
    long hr = ::OpenGeodatabase(StringToWString(osName), *(pConnection->m_pGeodatabase));
    if (EQUAL(CPLGetConfigOption("FGDB_SIMUL_FAIL", ""), "CASE2") ||
        FAILED(hr))
    {
        delete pConnection->m_pGeodatabase;
        pConnection->m_pGeodatabase = nullptr;
        pConnection->SetLocked(FALSE);
        Release(osName);
        GDBErr(hr, "Failed to re-open Geodatabase. Dataset should be closed");
        return OGRERR_FAILURE;
    }

    FGdbDataSource* pDS = new FGdbDataSource(this, pConnection);
    pDS->Open(osNameOri, TRUE, nullptr);
    //pDS->SetPerLayerCopyingForTransaction(bPerLayerCopyingForTransaction);
    poDSInOut = new OGRMutexedDataSource(pDS, TRUE, hMutex, TRUE);

    pConnection->SetLocked(FALSE);

    return eErr;
}

/***********************************************************************/
/*                            Release()                                */
/***********************************************************************/

void FGdbDriver::Release(const char* pszName)
{
    CPLMutexHolderOptionalLockD(hMutex);

    FGdbDatabaseConnection* pConnection = oMapConnections[pszName];
    if( pConnection != nullptr )
    {
        pConnection->m_nRefCount --;
        CPLDebug("FileGDB", "ref_count of %s = %d now", pszName,
                 pConnection->m_nRefCount);
        if( pConnection->m_nRefCount == 0 )
        {
            pConnection->CloseGeodatabase();
            delete pConnection;
            oMapConnections.erase(pszName);
        }
    }
}

/***********************************************************************/
/*                         CloseGeodatabase()                          */
/***********************************************************************/

void FGdbDatabaseConnection::CloseGeodatabase()
{
    if( m_pGeodatabase != nullptr )
    {
        CPLDebug("FileGDB", "Really closing %s now", m_osName.c_str());
        ::CloseGeodatabase(*m_pGeodatabase);
        delete m_pGeodatabase;
        m_pGeodatabase = nullptr;
    }
}

/***********************************************************************/
/*                         OpenGeodatabase()                           */
/***********************************************************************/

int FGdbDatabaseConnection::OpenGeodatabase(const char* pszFSName)
{
    m_pGeodatabase = new Geodatabase;
    long hr = ::OpenGeodatabase(StringToWString(CPLString(pszFSName)), *m_pGeodatabase);
    if (FAILED(hr))
    {
        delete m_pGeodatabase;
        m_pGeodatabase = nullptr;
        return FALSE;
    }
    return TRUE;
}

/***********************************************************************/
/*                         TestCapability()                            */
/***********************************************************************/

int FGdbDriver::TestCapability( const char * pszCap )
{
    if (EQUAL(pszCap, ODrCCreateDataSource) )
        return TRUE;

    else if (EQUAL(pszCap, ODrCDeleteDataSource) )
        return TRUE;

    return FALSE;
}
/************************************************************************/
/*                          DeleteDataSource()                          */
/************************************************************************/

OGRErr FGdbDriver::DeleteDataSource( const char *pszDataSource )
{
    CPLMutexHolderD(&hMutex);

    std::wstring wstr = StringToWString(pszDataSource);

    long hr = 0;

    if( S_OK != (hr = ::DeleteGeodatabase(wstr)) )
    {
        GDBErr(hr, "Failed to delete Geodatabase");
        return OGRERR_FAILURE;
    }

    return OGRERR_NONE;
}

/***********************************************************************/
/*                       RegisterOGRFileGDB()                          */
/***********************************************************************/

void RegisterOGRFileGDB()

{
    if (! GDAL_CHECK_VERSION("OGR FGDB"))
        return;

    OGRSFDriver* poDriver = new FGdbDriver;
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                                "ESRI FileGDB" );
    poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "gdb" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/vector/filegdb.html" );

    poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
                               "<CreationOptionList/>" );

    poDriver->SetMetadataItem( GDAL_DS_LAYER_CREATIONOPTIONLIST,
"<LayerCreationOptionList>"
"  <Option name='FEATURE_DATASET' type='string' description='FeatureDataset folder into to put the new layer'/>"
"  <Option name='LAYER_ALIAS' type='string' description='Alias of layer name'/>"
"  <Option name='GEOMETRY_NAME' type='string' description='Name of geometry column' default='SHAPE'/>"
"  <Option name='GEOMETRY_NULLABLE' type='boolean' description='Whether the values of the geometry column can be NULL' default='YES'/>"
"  <Option name='FID' type='string' description='Name of OID column' default='OBJECTID' deprecated_alias='OID_NAME'/>"
"  <Option name='XYTOLERANCE' type='float' description='Snapping tolerance, used for advanced ArcGIS features like network and topology rules, on 2D coordinates, in the units of the CRS'/>"
"  <Option name='ZTOLERANCE' type='float' description='Snapping tolerance, used for advanced ArcGIS features like network and topology rules, on Z coordinates, in the units of the CRS'/>"
"  <Option name='XORIGIN' type='float' description='X origin of the coordinate precision grid'/>"
"  <Option name='YORIGIN' type='float' description='Y origin of the coordinate precision grid'/>"
"  <Option name='ZORIGIN' type='float' description='Z origin of the coordinate precision grid'/>"
"  <Option name='XYSCALE' type='float' description='X,Y scale of the coordinate precision grid'/>"
"  <Option name='ZSCALE' type='float' description='Z scale of the coordinate precision grid'/>"
"  <Option name='XML_DEFINITION' type='string' description='XML definition to create the new table. The root node of such a XML definition must be a &lt;esri:DataElement&gt; element conformant to FileGDBAPI.xsd'/>"
"  <Option name='CREATE_MULTIPATCH' type='boolean' description='Whether to write geometries of layers of type MultiPolygon as MultiPatch' default='NO'/>"
"  <Option name='COLUMN_TYPES' type='string' description='A list of strings of format field_name=fgdb_filed_type (separated by comma) to force the FileGDB column type of fields to be created'/>"
"  <Option name='CONFIGURATION_KEYWORD' type='string-select' description='Customize how data is stored. By default text in UTF-8 and data up to 1TB'>"
"    <Value>DEFAULTS</Value>"
"    <Value>TEXT_UTF16</Value>"
"    <Value>MAX_FILE_SIZE_4GB</Value>"
"    <Value>MAX_FILE_SIZE_256TB</Value>"
"    <Value>GEOMETRY_OUTOFLINE</Value>"
"    <Value>BLOB_OUTOFLINE</Value>"
"    <Value>GEOMETRY_AND_BLOB_OUTOFLINE</Value>"
"  </Option>"
"</LayerCreationOptionList>");

    poDriver->SetMetadataItem( GDAL_DMD_CREATIONFIELDDATATYPES,
                               "Integer Real String Date DateTime Binary" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONFIELDDATASUBTYPES, "Int16 Float32" );
    poDriver->SetMetadataItem( GDAL_DCAP_NOTNULL_FIELDS, "YES" );
    poDriver->SetMetadataItem( GDAL_DCAP_DEFAULT_FIELDS, "YES" );
    poDriver->SetMetadataItem( GDAL_DCAP_NOTNULL_GEOMFIELDS, "YES" );

    OGRSFDriverRegistrar::GetRegistrar()->RegisterDriver(poDriver);
}
