using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using TINK.Model.Connector;
using TINK.Repository.Exception;
using TINK.Repository.Request;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Services.Geolocation;
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class CloseCommand
{
///
/// Possible steps of closing a lock.
///
public enum Step
{
StartStopingPolling,
StartingQueryingLocation,
ClosingLock,
WaitStopPollingQueryLocation,
///
/// Notifies back end about modified lock state.
///
UpdateLockingState,
QueryLocationTerminated
}
///
/// Possible states of closing a lock.
///
public enum State
{
OutOfReachError,
CouldntCloseMovingError,
CouldntCloseBoltBlockedError,
GeneralCloseError,
StartGeolocationException,
WaitGeolocationException,
WebConnectFailed,
ResponseIsInvalid,
BackendUpdateFailed
}
///
/// Interface to notify view model about steps/ state changes of closing process.
///
public interface ICloseCommandListener
{
///
/// Reports current step.
///
/// Current step to report.
void ReportStep(Step currentStep);
///
/// Reports current state.
///
/// Current state to report.
/// Message describing the current state.
///
Task ReportStateAsync(State currentState, string message);
}
///
/// Closes the lock and updates copri.
///
/// Interface to notify view model about steps/ state changes of closing process.
/// Task which stops polling.
public static async Task InvokeAsync(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func isConnectedDelegate,
Func connectorFactor,
ICloseCommandListener listener,
Task stopPollingTask)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception ex)
{
Log.ForContext().Error("An exception {exception} was thrown invoking step- action for set {step} ", ex, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception ex)
{
Log.ForContext().Error("An exception {exception} was thrown invoking state- action for set {state} ", ex, state);
}
}
// Wait for geolocation and polling task to stop (on finished or on canceled).
async Task WaitForPendingTasks(Task locationTask)
{
// Step: Wait until getting geolocation has completed.
InvokeCurrentStep(Step.WaitStopPollingQueryLocation);
Log.ForContext().Debug($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
try
{
await Task.WhenAll(new List { locationTask, stopPollingTask ?? Task.CompletedTask });
}
catch (Exception ex)
{
// No location information available.
Log.ForContext().Information("Canceling query location/ wait for polling task to finish failed. {Exception}", ex);
await InvokeCurrentStateAsync(State.WaitGeolocationException, ex.Message);
InvokeCurrentStep(Step.QueryLocationTerminated);
return null;
}
Log.ForContext().Debug($"Steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} finished.");
InvokeCurrentStep(Step.QueryLocationTerminated);
return locationTask.Result;
}
// Updates locking state
async Task UpdateLockingState(IGeolocation location, DateTime timeStamp)
{
// Step: Update backend.
InvokeCurrentStep(Step.UpdateLockingState);
try
{
await connectorFactor(true).Command.UpdateLockingStateAsync(
bike,
location != null
? new LocationDto.Builder
{
Latitude = location.Latitude,
Longitude = location.Longitude,
Accuracy = location.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(location.Timestamp.DateTime),
}.Build()
: null);
}
catch (Exception exception)
{
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", bike);
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
return;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", bike, copriException.Message, copriException.Response);
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
return;
}
else
{
Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
return;
}
}
}
//// Start Action
//// Step: Start query geolocation data.
Log.ForContext().Debug($"Starting step {Step.StartingQueryingLocation}...");
InvokeCurrentStep(Step.StartingQueryingLocation);
var ctsLocation = new CancellationTokenSource();
Task currentLocationTask = Task.FromResult(null);
var timeStampNow = DateTime.Now;
try
{
currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext().Information("Starting query location failed. {Exception}", bike, ex);
await InvokeCurrentStateAsync(State.StartGeolocationException, ex.Message);
}
//// Step: Close lock.
IGeolocation currentLocation;
Log.ForContext().Debug($"Starting step {Step.ClosingLock}...");
InvokeCurrentStep(Step.ClosingLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext().Debug("Lock can not be closed. {Exception}", exception);
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntCloseMovingException)
{
Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
}
else if (exception is CouldntCloseBoltBlockedException)
{
Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
}
else
{
Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
}
Log.ForContext().Error("Lock can not be closed. {Exception}", exception);
// Signal cts to cancel getting geolocation.
ctsLocation.Cancel();
// Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
// Update current state from exception
bike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.UnknownDisconnected;
if (!isConnectedDelegate())
{
// Lock state can not be updated because there is no connected.
throw;
}
if (exception is OutOfReachException)
{
// Locking state can not be updated because lock is not connected.
throw;
}
// Update backend.
// Do this even if current lock state is open (lock state must not necessarily be open before try to open, i.e. something undefined between open and closed).
await UpdateLockingState(currentLocation, timeStampNow);
throw;
}
//// Step: Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
// Keep geolocation where closing action occurred.
bike.LockInfo.Location = currentLocation;
if (!isConnectedDelegate())
{
return;
}
//// Step: Update backend.
await UpdateLockingState(currentLocation, timeStampNow);
}
}
}