public void TestDedupeSegments()
        {
            var audioRecording = AudioRecordingFactory.Create();

            var segments = new RemoteSegmentWithData[]
            {
                new RemoteSegmentWithData(audioRecording, new Range <double>(0, 60), 1.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(0, 60), 2.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(33, 93), 3.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(100.375, 160.375), 4.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(100.375, 160.375), 5.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(900, 960), 6.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(0, 60), 7.AsArray < object > ()),
            };

            var actual = EventMetadataResolver.DedupeSegments(segments);

            var expected = new RemoteSegmentWithData[]
            {
                new RemoteSegmentWithData(audioRecording, new Range <double>(0, 60), new object[] { 1, 2, 7 }),
                new RemoteSegmentWithData(audioRecording, new Range <double>(33, 93), 3.AsArray < object > ()),
                new RemoteSegmentWithData(audioRecording, new Range <double>(100.375, 160.375), new object[] { 4, 5 }),
                new RemoteSegmentWithData(audioRecording, new Range <double>(900, 960), 6.AsArray < object > ()),
            };

            Assert.AreEqual(expected.Length, actual.Length);
            foreach (var(expectedItem, actualItem) in expected.Zip(actual, ValueTuple.Create))
            {
                Assert.AreEqual(expectedItem.Source, actualItem.Source);
                Assert.AreEqual(expectedItem.Offsets, actualItem.Offsets);
                CollectionAssert.AreEqual(expectedItem.Data.ToArray(), actualItem.Data.ToArray());
            }
        }
        public static async Task <int> ExecuteAsync(Arguments arguments)
        {
            Log.Info("Event statistics analysis begin");

            // validate arguments

            var input  = arguments.Source;
            var config = arguments.Config.ToFileInfo();

            if (!input.Exists)
            {
                throw new FileNotFoundException("Cannot find source file", input.FullName);
            }

            // try an automatically find the config file
            if (config == null)
            {
                throw new FileNotFoundException("No config file argument provided");
            }
            else if (!config.Exists)
            {
                Log.Warn($"Config file {config.FullName} not found... attempting to resolve config file");

                // we use  the original input string - Using FileInfo fullname always produces an
                // absolute path relative to pwd... we don't want to prematurely make assumptions:
                // e.g. We require a missing absolute path to fail... that wouldn't work with .Name
                // e.g. We require a relative path to try and resolve, using .FullName would fail the first absolute
                //    check inside ResolveConfigFile
                config = ConfigFile.Resolve(arguments.Config, Directory.GetCurrentDirectory().ToDirectoryInfo());
            }

            // if a temp dir is not given, use output dir as temp dir
            if (arguments.TempDir == null)
            {
                Log.Warn("No temporary directory provided, using backup directory");
            }

            // Remote: create an instance of our API helpers
            IApi api = arguments.WorkbenchApi.IsNullOrEmpty() ? Api.Default : Api.Parse(arguments.WorkbenchApi);

            // log some helpful messages
            Log.Info("Events file:         " + input);
            Log.Info("Configuration file:  " + config);
            Log.Info("Output folder:       " + arguments.Output);
            Log.Info("Temp File Directory: " + arguments.TempDir);
            Log.Info("Api:                 " + api);

            // Remote: Test we can log in to the workbench
            var auth = new AuthenticationService(api);
            Task <IAuthenticatedApi> task;

            if (arguments.AuthenticationToken.IsNotWhitespace())
            {
                Log.Debug("Using token for authentication");
                task = auth.CheckLogin(arguments.AuthenticationToken);
            }
            else
            {
                var username = LoggedConsole.Prompt("Enter your username or email for the acoustic workbench:");
                var password = LoggedConsole.Prompt("Enter your password for the acoustic workbench:", forPassword: true);
                task = auth.Login(username, password);

                //task = auth.Login("*****@*****.**", "tsettest");
            }

            LoggedConsole.WriteWaitingLine(task, "Logging into workbench...");

            var authenticatedApi = await task.TimeoutAfter(Service.ClientTimeout).ConfigureAwait(false);

            Log.Info("Login success" + authenticatedApi);

            // read events/annotation file
            Log.Info("Now reading input data");

            // Read events from provided CSV file.
            // Also tag them with an order index to allow sorting in the same order as they were provided to us.
            var events = Csv
                         .ReadFromCsv <ImportedEvent>(input, throwOnMissingField: false)
                         .Select(
                (x, i) =>
            {
                x.Order = i;
                return(x);
            })
                         .ToArray();

            if (events.Length == 0)
            {
                Log.Warn("No events imported - source file empty. Exiting");
                return(ExceptionLookup.NoData);
            }

            Log.Info($"Events read, {events.Length} read.");

            // need to validate the events
            var invalidEvents = events.Where(e => !e.IsValid()).ToArray();

            if (invalidEvents.Length > 0)
            {
                throw new InvalidOperationException(
                          "Invalid event detected."
                          + $" {invalidEvents.Length} events are not valid. The first invalid event is {invalidEvents[0]}");
            }

            // next gather meta data for all events
            // and transform list of events into list of segments
            // NOTE: to save on I/O sometimes if events share the same audio block, then multiple events will be
            // bundled into the same analysis segment.
            var resolver = new EventMetadataResolver(
                authenticatedApi,
                PaddingFunction,
                arguments.Parallel ? 25 : 1);
            var metadataTask = resolver.GetRemoteMetadata(events);

            // wait for 1 second per event - this should be an order of magnitude greater than what is needed
            ISegment <AudioRecording>[] segments = await metadataTask.TimeoutAfter(events.Length);

            Log.Info($"Metadata collected, preparing to start analysis");

            // finally time to start preparing jobs
            ISourcePreparer preparer = new RemoteSourcePreparer(authenticatedApi, allowSegmentcutting: false);

            AnalysisCoordinator coordinator = new AnalysisCoordinator(
                preparer,
                SaveBehavior.Never,
                uniqueDirectoryPerSegment: false,
                isParallel: arguments.Parallel);

            // instantiate the Analysis
            EventStatisticsAnalysis analysis = new EventStatisticsAnalysis();

            // derserialize the config file
            var configuration = analysis.ParseConfig(config);

            AnalysisSettings settings = analysis.DefaultSettings;

            settings.AnalysisOutputDirectory = arguments.Output;
            settings.AnalysisTempDirectory   = arguments.TempDir;
            settings.Configuration           = configuration;

            var results = coordinator.Run(segments, analysis, settings);

            var allEvents = results.SelectMany(es => es.Events).ToArray();

            var eventsWithErrors = allEvents.Count(x => ((EventStatistics)x).Error);

            if (eventsWithErrors > 0)
            {
                Log.Warn($"Errors occurred when calculating statistics for {eventsWithErrors} events.");
            }

            Log.Trace("Sorting event statistics results");
            Array.Sort(allEvents);

            Log.Info("Executing summary");

            // TODO: implement if needed
            analysis.SummariseResults(settings, null, allEvents, null, null, results);

            Log.Debug("Summary complete");

            var instanceOutputDirectory =
                AnalysisCoordinator.GetNamedDirectory(settings.AnalysisOutputDirectory, analysis);

            var resultName = FilenameHelpers.AnalysisResultPath(
                instanceOutputDirectory,
                input,
                analysis.Identifier,
                "csv");

            // NOTE: we are only saving event files
            Log.Info($"Writing results to {resultName}");
            analysis.WriteEventsFile(resultName.ToFileInfo(), allEvents.AsEnumerable());
            Log.Debug("Writing events completed");

            var summaryStats = new
            {
                numberEvents           = allEvents.Length,
                durationEvents         = allEvents.Sum(x => ((EventStatistics)x).EventDurationSeconds),
                numberRecordings       = allEvents.Select(x => ((EventStatistics)x).AudioRecordingId).Distinct().Count(),
                durationAudioProcessed = results.Sum(x => x.SegmentAudioDuration.TotalSeconds),
                remoteAudioDownloaded  = (preparer as RemoteSourcePreparer)?.TotalBytesRecieved,
            };

            Log.Info("Summary statistics:\n" + Json.SerializeToString(summaryStats));

            Log.Success("Event statistics analysis complete!");

            return(ExceptionLookup.Ok);
        }