// Given
        // - a path to a .tdb file  (specifying the root folder)
        // - a list of ddbFiles (which must be located in sub-folders relative to the root folder)
        // create a .ddb File in the root folder that merges data found in the .ddbFiles into it, in particular, the tables:
        // - DataTable
        // - Detections
        // If fatal errors occur in the merge, abort
        // Return the relevant error messages in the ErrorsAndWarnings object.
        // Note: if a merged .ddb File already exists in that root folder, it will be backed up and then over-written

        public async static Task <ErrorsAndWarnings> TryMergeDatabasesAsync(string tdbFile, List <string> sourceDDBFilePaths, IProgress <ProgressBarArguments> progress)
        {
            ErrorsAndWarnings errorMessages = new ErrorsAndWarnings();

            string rootFolderPath         = Path.GetDirectoryName(tdbFile);
            string destinationDDBFileName = Constant.File.MergedFileName;
            string destinationDDBFilePath = Path.Combine(rootFolderPath, destinationDDBFileName);
            string rootFolderName         = rootFolderPath.Split(Path.DirectorySeparatorChar).Last();


            if (sourceDDBFilePaths == null)
            {
                errorMessages.Errors.Add("No databases (.ddb files) were found in the sub-folders, so there was nothing to merge.");
                return(errorMessages);
            }

            // if the mergedatabase file was previously created, it may be included in the source list.
            // So just skip over it, as it no longer exists and we don't actually want it
            sourceDDBFilePaths.RemoveAll(Item => Item == destinationDDBFilePath);

            if (sourceDDBFilePaths.Count == 0)
            {
                errorMessages.Errors.Add("No databases (.ddb files) were found in the sub-folders, so there was nothing to merge.");
                return(errorMessages);
            }

            // Check to see if we can actually open the template.
            // As we can't have out parameters in an async method, we return the state and the desired templateDatabase as a tuple
            // Original form: if (!(await TemplateDatabase.TryCreateOrOpenAsync(templateDatabasePath, out this.templateDatabase).ConfigureAwait(true))
            Tuple <bool, TemplateDatabase> tupleResult = await TemplateDatabase.TryCreateOrOpenAsync(tdbFile).ConfigureAwait(true);

            TemplateDatabase templateDatabase = tupleResult.Item2;

            if (!tupleResult.Item1)
            {
                // notify the user the template couldn't be loaded rather than silently doing nothing
                errorMessages.Errors.Add("Could not open the template .tdb file: " + tdbFile);
                return(errorMessages);
            }

            // if the merge file exists, move it to the backup folder as we will be overwriting it.
            bool backupMade = false;

            if (File.Exists(destinationDDBFilePath))
            {
                // Backup the old merge file by moving it to the backup folder
                // Note that we do the move instead of copy as we will be overwriting the file anyways
                backupMade = FileBackup.TryCreateBackup(destinationDDBFilePath, true);
            }

            FileDatabase fd = await FileDatabase.CreateEmptyDatabase(destinationDDBFilePath, templateDatabase).ConfigureAwait(true);

            fd.Dispose();
            fd = null;

            // Open the database
            SQLiteWrapper destinationDDB = new SQLiteWrapper(destinationDDBFilePath);

            // Get the DataLabels from the DataTable in the main database.
            // We will later check to see if they match their counterparts in each database to merge in
            List <string> mergedDDBDataLabels = destinationDDB.SchemaGetColumns(Constant.DBTables.FileData);

            int sourceDDBFilePathsCount = sourceDDBFilePaths.Count;

            for (int i = 0; i < sourceDDBFilePathsCount; i++)
            {
                if (sourceDDBFilePaths[i].Equals(destinationDDBFilePath))
                {
                    // if the mergedatabase file was previously created, it may be included in the source list.
                    // So just skip over it, as it no longer exists and we don't actually want it
                    continue;
                }

                // Try to merge each database into the merged database
                await Task.Run(() =>
                {
                    // Report progress, introducing a delay to allow the UI thread to update and to make the progress bar linger on the display
                    progress.Report(new ProgressBarArguments((int)((i + 1) / (double)sourceDDBFilePathsCount * 100.0),
                                                             String.Format("Merging {0}/{1} databases. Please wait...", i + 1, sourceDDBFilePathsCount),
                                                             "Merging...",
                                                             false, false));
                    Thread.Sleep(250);
                    ListComparisonEnum listComparisonEnum = MergeDatabases.InsertSourceDataBaseTablesintoDestinationDatabase(destinationDDB, sourceDDBFilePaths[i], rootFolderPath, mergedDDBDataLabels);
                    if (listComparisonEnum != ListComparisonEnum.Identical)
                    {
                        string message = listComparisonEnum == ListComparisonEnum.ElementsDiffer
                        ? "Its template uses different data labels"
                        : "Its template has the same data labels, but in a different order";
                        string trimmedPath = sourceDDBFilePaths[i].Substring(rootFolderPath.Length + 1);
                        errorMessages.Warnings.Add(String.Format("'{0}' was skipped. {1}", trimmedPath, message));
                    }
                }).ConfigureAwait(true);
            }
            // After the merged database is constructed, set the Folder column to the current root folder
            if (!String.IsNullOrEmpty(rootFolderName))
            {
                destinationDDB.ExecuteNonQuery(Sql.Update + Constant.DBTables.FileData + Sql.Set + Constant.DatabaseColumn.Folder + Sql.Equal + Sql.Quote(rootFolderName));
            }

            // After the merged database is constructed, reset fields in the ImageSetTable to the defaults i.e., first row, selection all,
            if (!String.IsNullOrEmpty(rootFolderName))
            {
                destinationDDB.ExecuteNonQuery(Sql.Update + Constant.DBTables.ImageSet + Sql.Set + Constant.DatabaseColumn.MostRecentFileID + Sql.Equal + "1");
                destinationDDB.ExecuteNonQuery(Sql.Update + Constant.DBTables.ImageSet + Sql.Set + Constant.DatabaseColumn.Selection + Sql.Equal + ((int)FileSelectionEnum.All).ToString());
                destinationDDB.ExecuteNonQuery(Sql.Update + Constant.DBTables.ImageSet + Sql.Set + Constant.DatabaseColumn.SortTerms + Sql.Equal + Sql.Quote(Constant.DatabaseValues.DefaultSortTerms));
            }
            if (backupMade && (errorMessages.Errors.Any() || errorMessages.Warnings.Any()))
            {
                errorMessages.Warnings.Add(String.Format("Note: A backup of your original {0} can be found in the {1} folder", destinationDDBFileName, Constant.File.BackupFolder));
            }
            return(errorMessages);
        }
        // Merge a .ddb file specified in the sourceDDBPath path into the destinationDDB database.
        // Also update the Relative path to reflect the new location of the sourceDDB paths as defined in the rootFolderPath
        private static ListComparisonEnum InsertSourceDataBaseTablesintoDestinationDatabase(SQLiteWrapper destinationDDB, string SourceDDBPath, string rootFolderPath, List <string> sourceDataLabels)
        {
            // Check the arguments for null
            ThrowIf.IsNullArgument(destinationDDB, nameof(destinationDDB));

            // Check to see if the datalabels in the sourceDDB matches those in the destinationDataLabels.
            // If not, generate a warning and abort the merge
            SQLiteWrapper      sourceDDB             = new SQLiteWrapper(SourceDDBPath);
            List <string>      destinationDataLabels = sourceDDB.SchemaGetColumns(Constant.DBTables.FileData);
            ListComparisonEnum listComparisonEnum    = Compare.CompareLists(sourceDataLabels, destinationDataLabels);

            //if (listComparisonEnum != ListComparisonEnum.Identical)
            if (listComparisonEnum == ListComparisonEnum.ElementsDiffer)
            {
                return(listComparisonEnum);
            }

            string attachedDB               = "attachedDB";
            string tempDataTable            = "tempDataTable";
            string tempMarkersTable         = "tempMarkersTable";
            string tempDetectionsTable      = "tempDetectionsTable";
            string tempClassificationsTable = "tempClassificationsTable";

            // Determine the path prefix to add to the Relative Path i.e., the difference between the .tdb root folder and the path to the ddb file
            string pathPrefixToAdd = GetDifferenceBetweenPathAndSubPath(SourceDDBPath, rootFolderPath);

            // Calculate an ID offset (the current max Id), where we will be adding that to all Ids in the ddbFile to merge.
            // This will guarantee that there are no duplicate primary keys
            int offsetId = destinationDDB.ScalarGetCountFromSelect(QueryGetMax(Constant.DatabaseColumn.ID, Constant.DBTables.FileData));

            // Create the first part of the query to:
            // - Attach the ddbFile
            // - Create a temporary DataTable mirroring the one in the sourceDDB (so updates to that don't affect the original ddb)
            // - Update the DataTable with the modified Ids
            // - Update the DataTable with the path prefix
            // - Insert the DataTable  into the main db's DataTable
            // Form: ATTACH DATABASE 'sourceDDB' AS attachedDB;
            //       CREATE TEMPORARY TABLE tempDataTable AS SELECT * FROM attachedDB.DataTable;
            //       UPDATE tempDataTable SET Id = (offsetID + tempDataTable.Id);
            //       UPDATE TempDataTable SET RelativePath =  CASE WHEN RelativePath = '' THEN ("PrefixPath" || RelativePath) ELSE ("PrefixPath\\" || RelativePath) EMD
            //       INSERT INTO DataTable SELECT * FROM tempDataTable;
            string query = Sql.BeginTransactionSemiColon;

            query += QueryAttachDatabaseAs(SourceDDBPath, attachedDB);
            query += QueryCreateTemporaryTableFromExistingTable(tempDataTable, attachedDB, Constant.DBTables.FileData);
            query += QueryAddOffsetToIDInTable(tempDataTable, Constant.DatabaseColumn.ID, offsetId);
            query += QueryAddPrefixToRelativePathInTable(tempDataTable, pathPrefixToAdd);
            // query += QueryInsertTable2DataIntoTable1(Constant.DBTables.FileData, tempDataTable);
            query += QueryInsertTable2DataIntoTable1(Constant.DBTables.FileData, tempDataTable, sourceDataLabels);

            // Create the second part of the query to:
            // - Create a temporary Markers Table mirroring the one in the sourceDDB (so updates to that don't affect the original ddb)
            // - Update the Markers Table with the modified Ids
            // - Insert the Markers Table  into the main db's Markers Table
            // Form: CREATE TEMPORARY TABLE tempMarkers AS SELECT * FROM attachedDB.Markers;
            //       UPDATE tempMarkers SET Id = (offsetID + tempMarkers.Id);
            //       INSERT INTO Markers SELECT * FROM tempMarkers;
            query += QueryCreateTemporaryTableFromExistingTable(tempMarkersTable, attachedDB, Constant.DBTables.Markers);
            query += QueryAddOffsetToIDInTable(tempMarkersTable, Constant.DatabaseColumn.ID, offsetId);
            query += QueryInsertTable2DataIntoTable1(Constant.DBTables.Markers, tempMarkersTable);

            // Now we need to see if we have to handle detection table updates.
            // Check to see if the destinationDDB file and the sourceDDB file each have a Detections table.
            bool sourceDetectionsExists      = FileDatabase.TableExists(Constant.DBTables.Detections, SourceDDBPath);
            bool destinationDetectionsExists = destinationDDB.TableExists(Constant.DBTables.Detections);

            // If the main database doesn't have detections, but the database to merge into it does,
            // then we have to create the detection tables to the main database.
            if (destinationDetectionsExists == false && sourceDetectionsExists)
            {
                DetectionDatabases.CreateOrRecreateTablesAndColumns(destinationDDB);

                // As its the first time we see a database with detections, import the Detection Categories, Classification Categories and Info
                // This assumes (perhaps incorrectly) that all databases the merge in have the same detection/classification categories and info.
                // FORM: INSERT INTO DetectionCategories SELECT * FROM attachedDB.DetectionCategories;
                //              INSERT INTO ClassificationCategories SELECT * FROM attachedDB.ClassifciationCategories;
                //              INSERT INTO Info SELECT * FROM attachedDB.Info;
                query += QueryInsertTableDataFromAnotherDatabase(Constant.DBTables.DetectionCategories, attachedDB);
                query += QueryInsertTableDataFromAnotherDatabase(Constant.DBTables.ClassificationCategories, attachedDB);
                query += QueryInsertTableDataFromAnotherDatabase(Constant.DBTables.Info, attachedDB);
            }

            // Create the third part of the query only if the toMergeDDB contains a detections table
            // (as otherwise we don't have to update the detection table in the main ddb.
            // - Create a temporary Detections table mirroring the one in the toMergeDDB (so updates to that don't affect the original ddb)
            // - Update the Detections Table with both the modified Ids and detectionIDs
            // - Insert the Detections Table into the main db's Detections Table
            // Form: CREATE TEMPORARY TABLE tempDetectionsTable AS SELECT * FROM attachedDB.Detections;
            //       UPDATE TempDetectionsTable SET Id = (offsetId + TempDetectionsTable.Id);
            //       UPDATE TempDetectionsTable SET DetectionID = (offsetDetectionId + TempDetectionsTable.DetectionId);
            //       INSERT INTO Detections SELECT * FROM TempDetectionsTable;"
            // The Classifications form is similar, except it used the classification-specific tables, ids, offsets, etc.
            if (sourceDetectionsExists)
            {
                // The database to merge in has detections, so the SQL query also updates the Detections table.
                // Calculate an offset (the max DetectionIDs), where we will be adding that to all detectionIds in the ddbFile to merge.
                // However, the offeset should be 0 if there are no detections in the main DB, so we can just reusue this as is.
                // as we will be creating the detection table and then just adding to it.
                int offsetDetectionId = (destinationDetectionsExists)
                    ? destinationDDB.ScalarGetCountFromSelect(QueryGetMax(Constant.DetectionColumns.DetectionID, Constant.DBTables.Detections))
                    : 0;
                query += QueryCreateTemporaryTableFromExistingTable(tempDetectionsTable, attachedDB, Constant.DBTables.Detections);
                query += QueryAddOffsetToIDInTable(tempDetectionsTable, Constant.DatabaseColumn.ID, offsetId);
                query += QueryAddOffsetToIDInTable(tempDetectionsTable, Constant.DetectionColumns.DetectionID, offsetDetectionId);
                query += QueryInsertTable2DataIntoTable1(Constant.DBTables.Detections, tempDetectionsTable);

                // Similar to the above, we also update the classifications
                int offsetClassificationId = (destinationDetectionsExists)
                    ? destinationDDB.ScalarGetCountFromSelect(QueryGetMax(Constant.ClassificationColumns.ClassificationID, Constant.DBTables.Classifications))
                    : 0;
                query += QueryCreateTemporaryTableFromExistingTable(tempClassificationsTable, attachedDB, Constant.DBTables.Classifications);
                query += QueryAddOffsetToIDInTable(tempClassificationsTable, Constant.ClassificationColumns.ClassificationID, offsetClassificationId);
                query += QueryAddOffsetToIDInTable(tempClassificationsTable, Constant.ClassificationColumns.DetectionID, offsetDetectionId);
                query += QueryInsertTable2DataIntoTable1(Constant.DBTables.Classifications, tempClassificationsTable);
            }
            query += Sql.EndTransactionSemiColon;
            destinationDDB.ExecuteNonQuery(query);

            return(ListComparisonEnum.Identical);
        }