using Serilog;
using System;
using System.ComponentModel;
using System.Text.RegularExpressions;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.State;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
using Xamarin.Forms;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike
{
///
/// Defines the type of BikesViewModel child items, i.e. BikesViewModel derives from ObservableCollection<BikeViewModelBase>.
/// Holds references to
/// - connection state services
/// - copri service
/// - view service
///
public abstract class BikeViewModelBase
{
///
/// Time format for text "Gebucht seit".
///
public const string TIMEFORMAT = "dd. MMMM HH:mm";
/// Provides info about the smart device (phone, tablet, ...).
protected ISmartDevice SmartDevice;
///
/// Reference on view service to show modal notifications and to perform navigation.
///
protected IViewService ViewService { get; }
/// Provides a connect object.
protected Func ConnectorFactory { get; }
/// Delegate to retrieve connected state.
protected Func IsConnectedDelegate { get; }
/// Removes bike from bikes view model.
protected Action BikeRemoveDelegate { get; }
/// Object to manage update of view model objects from Copri.
public Func ViewUpdateManager { get; }
///
/// Holds the bike to display.
///
protected BikeInfoMutable Bike;
/// Reference on the user
protected IUser ActiveUser { get; }
/// Holds the view context in which bike view model is used.
protected ViewContext ViewContext { get; }
///
/// Provides context related info.
///
private IInUseStateInfoProvider StateInfoProvider { get; }
/// View model to be used for progress report and unlocking/ locking view.
protected IBikesViewModel BikesViewModel { get; }
/// Delegate to open browser.
private Action OpenUrlInBrowser;
///
/// Notifies GUI about changes.
///
public abstract event PropertyChangedEventHandler PropertyChanged;
///
/// Notifies children about changed bike state.
///
public abstract void OnSelectedBikeStateChanged();
/// Raises events in order to update GUI.
public abstract void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs);
///
/// Constructs a bike view model object.
///
/// Provides info about the smart device (phone, tablet, ...).
/// Bike to be displayed.
/// Object holding logged in user or an empty user object.
/// Holds the view context in which bike view model is used.
/// Provides in use state information.
/// View model to be used for progress report and unlocking/ locking view.
/// Delegate to open browser.
public BikeViewModelBase(
Func isConnectedDelegate,
Func connectorFactory,
Action bikeRemoveDelegate,
Func viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser activeUser,
ViewContext viewContext,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action openUrlInBrowser)
{
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
BikeRemoveDelegate = bikeRemoveDelegate;
ViewUpdateManager = viewUpdateManager;
SmartDevice = smartDevice;
ViewService = viewService;
Bike = selectedBike
?? throw new ArgumentException(string.Format("Can not construct {0}- object, bike object is null.", typeof(BikeViewModelBase)));
ActiveUser = activeUser
?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(BikeViewModelBase)));
ViewContext = viewContext;
StateInfoProvider = stateInfoProvider
?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(IInUseStateInfoProvider)));
selectedBike.PropertyChanged +=
(sender, eventargs) => OnSelectedBikePropertyChanged(eventargs.PropertyName);
var battery = selectedBike.Drive?.Battery;
if (battery != null)
{
battery.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(BatteryMutable.CurrentChargeBars))
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(CurrentChargeBars)));
}
};
}
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
OpenUrlInBrowser = openUrlInBrowser ?? (url => { Log.ForContext().Error($"No browse service available to open {url}."); });
}
///
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
///
///
private void OnSelectedBikePropertyChanged(string nameOfProp)
{
if (nameOfProp == nameof(State))
{
OnSelectedBikeStateChanged(); // Notify derived class about change of state.
}
var state = State;
if (LastState != state)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(State)));
LastState = state;
}
var stateText = StateText;
if (LastStateText != stateText)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateText)));
LastStateText = stateText;
}
var stateColor = StateColor;
if (LastStateColor != stateColor)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateColor)));
LastStateColor = stateColor;
}
}
///
/// Gets the display name of the bike containing of bike id and type of bike..
///
public string Name => Bike.GetDisplayName();
public string TypeOfBike => Bike.GetDisplayTypeOfBike();
///
/// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
///
public AaRideType? AaRideType => Bike.AaRideType;
public string WheelType => Bike.GetDisplayWheelType();
///
/// Gets the unique Id of bike or an empty string, if no name is defined to avoid duplicate display of id.
///
public string DisplayId => Bike.GetDisplayId();
///
/// Gets the unique Id of bike used by derived model to determine which bike to remove.
///
public string Id => Bike.Id;
public string StationId => $"Station {Bike.StationId}";
public string DisplayName => Bike.GetDisplayName();
public bool IsBikeWithCopriLock => Bike.LockModel == Model.Bikes.BikeInfoNS.BikeNS.LockModel.Sigo;
/// Returns if type of bike is a cargo pedelec bike.
public bool IsBatteryChargeVisible =>
Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
&& (!Bike.Drive.Battery.IsHidden.HasValue /* no value means show battery level */ || Bike.Drive.Battery.IsHidden.Value == false);
/// Gets the image path for bike type City bike, CargoLong, Trike or Pedelec.
public string DisplayedBikeImageSourceString => $"bike_{Bike.TypeOfBike}_{Bike.Drive.Type}_{Bike.WheelType}.png";
///
/// Gets the current charge level.
///
public string CurrentChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
? Bike.Drive.Battery.CurrentChargeBars?.ToString() ?? string.Empty
: string.Empty;
///
/// Gets the value if current charge level is low ( <= 1 ).
///
public bool IsCurrentChargeLow => this.CurrentChargeBars == "1" || this.CurrentChargeBars == "0";
///
/// Gets the current charge level.
///
public string MaxChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
? Bike.Drive.Battery.MaxChargeBars?.ToString() ?? string.Empty
: string.Empty;
///
/// Returns status of a bike as text (binds to GUI).
///
/// Log invalid states for diagnose purposes.
public string StateText
{
get
{
switch (Bike.State.Value)
{
case InUseStateEnum.FeedbackPending:
return AppResources.StatusTextFeedbackPending;
case InUseStateEnum.Disposable:
return AppResources.StatusTextAvailable;
}
if (!ActiveUser.IsLoggedIn)
{
// Nobody is logged in.
switch (Bike.State.Value)
{
case InUseStateEnum.Reserved:
return GetReservedInfo(
Bike.State.RemainingTime,
Bike.StationId,
null); // Hide reservation code because no one but active user should see code
case InUseStateEnum.Booked:
return GetBookedInfo(
Bike.State.From,
Bike.StationId,
null); // Hide reservation code because no one but active user should see code
default:
return string.Format("Unbekannter status {0}.", Bike.State.Value);
}
}
switch (Bike.State.Value)
{
case InUseStateEnum.Reserved:
return Bike.State.MailAddress == ActiveUser.Mail
? GetReservedInfo(
Bike.State.RemainingTime,
Bike.StationId,
Bike.State.Code)
: "Fahrrad bereits reserviert durch anderen Nutzer.";
case InUseStateEnum.Booked:
return Bike.State.MailAddress == ActiveUser.Mail
? GetBookedInfo(
Bike.State.From,
Bike.StationId,
Bike.State.Code)
: "Fahrrad bereits gebucht durch anderen Nutzer.";
default:
return string.Format("Unbekannter status {0}.", Bike.State.Value);
}
}
}
/// Gets the value of property when PropertyChanged was fired.
private string LastStateText { get; set; }
///
/// Gets reserved into display text.
///
/// Log unexpected states.
///
/// Display text
private string GetReservedInfo(
TimeSpan? p_oRemainingTime,
string p_strStation = null,
string p_strCode = null)
{
return StateInfoProvider.GetReservedInfo(p_oRemainingTime, p_strStation, p_strCode);
}
///
/// Gets booked into display text.
///
/// Log unexpected states.
///
/// Display text
private string GetBookedInfo(
DateTime? p_oFrom,
string p_strStation = null,
string p_strCode = null)
{
return StateInfoProvider.GetBookedInfo(p_oFrom, p_strStation, p_strCode);
}
///
/// Exposes the bike state.
///
public InUseStateEnum State => Bike.State.Value;
/// Gets the value of property when PropertyChanged was fired.
public InUseStateEnum LastState { get; set; }
///
/// Gets the color which visualizes the state of bike in relation to logged in user.
///
public Color StateColor
{
get
{
if (!ActiveUser.IsLoggedIn)
{
return Color.Default;
}
var l_oSelectedBikeState = Bike.State;
switch (l_oSelectedBikeState.Value)
{
case InUseStateEnum.Reserved:
return l_oSelectedBikeState.MailAddress == ActiveUser.Mail
? InUseStateEnum.Reserved.GetColor()
: Color.Red; // Bike is reserved by someone else
case InUseStateEnum.Booked:
return l_oSelectedBikeState.MailAddress == ActiveUser.Mail
? InUseStateEnum.Booked.GetColor()
: Color.Red; // Bike is booked by someone else
default:
return Color.Default;
}
}
}
/// Holds description about the tariff.
public TariffDescriptionViewModel TariffDescription => new TariffDescriptionViewModel(Bike.TariffDescription);
/// Gets the value of property when PropertyChanged was fired.
public Color LastStateColor { get; set; }
/// Gets first url from text.
/// url to extract text from.
/// Gets first url or an empty string if on url is contained in text.
public static string GetUrlFirstOrDefault(string htmlSource)
{
if (string.IsNullOrEmpty(htmlSource))
return string.Empty;
try
{
var matches = new Regex(@"https://[-a-zA-Z0-9+&@#/%?=~_|!:, .;]*[-a-zA-Z0-9+&@#/%=~_|]").Matches(htmlSource);
return matches.Count > 0
? matches[0].Value
: string.Empty;
}
catch (Exception e)
{
Log.ForContext().Error("Extracting URL failed. {Exception}", e);
return string.Empty;
}
}
///
/// Check if bike has to be removed and if yes invoke remove delegate.
///
/// Id of bike to remove.
/// Previous state used to decide whether to remove bike or not.
public void CheckRemoveBike(string Id, InUseStateEnum lastState)
{
switch (ViewContext.Page)
{
case PageContext.MyBikes:
// Bike is shown on page My Bikes.
switch (Bike.State.Value)
{
case InUseStateEnum.FeedbackPending:
case InUseStateEnum.Reserved:
case InUseStateEnum.Booked:
// Bike has still to be shown at my bikes page to give feedback or manage bike.
break;
default:
BikeRemoveDelegate(Id);
break;
}
break;
case PageContext.BikesAtStation:
// Bike is shown on page Bike At Station.
switch (lastState != InUseStateEnum.Booked)
{
case true:
// Only remove bike if bike was rented before.
break;
default:
switch (ViewContext.StationId == Bike.StationId)
{
case true:
// Do not remove bike if bike is returned a current station.
break;
default:
BikeRemoveDelegate(Id);
break;
}
break;
}
break;
}
}
}
}