using System; using System.Collections.Generic; using System.Linq; using Serilog; using ShareeBike.Model.Connector; using ShareeBike.Repository.Response; using ShareeBike.Repository.Response.Stations; using ShareeBike.Repository.Response.Stations.Station; namespace ShareeBike.Repository { [Flags] public enum UpdateTarget { None = 0, BikesAvailableResponse = 1, BikesReservedOccupiedResponse = 2, StationsAvailableResponse = 4, All = BikesAvailableResponse | BikesReservedOccupiedResponse | StationsAvailableResponse } /// /// Manages station and bikes information. /// Bike is kept in a response- oriented way which is redundant an has to be synchronized for this reason when updated. /// public class CopriResponseModel { public CopriResponseModel( BikesAvailableResponse bikesAll, BikesReservedOccupiedResponse bikesReservedOccupied, StationsAvailableResponse stations) { BikesAll = bikesAll ?? new BikesAvailableResponse(); BikesReservedOccupied = bikesReservedOccupied ?? new BikesReservedOccupiedResponse(); Stations = stations ?? new StationsAvailableResponse(); } /// /// Holds /// - part of/ or all available bikes and /// - part of/ or all reserved bikes of logged in user if user is logged in and /// - part of/ or all occupied bikes of logged in user if user is logged in. /// /// /// Available bikes: Full information is only contained in . /// Available bikes: Count of available bikes per station is kept in . /// Reserved and occupied bikes: Are contained as well in . /// Reserved and occupied bikes: Are contained as well in . /// public BikesAvailableResponse BikesAll { get; private set; } /// /// Holds all reserved and occupied bikes of logged in user if not used without being logged in. /// /// /// Reserved and occupied bikes: Are contained as well in . /// Reserved and occupied bikes: Are contained as well in . /// public BikesReservedOccupiedResponse BikesReservedOccupied { get; private set; } /// /// All bike stations, count of available bikes at each station /// and all reserved and occupied bikes of logged in user if not used without being logged in. /// /// /// Count of available bikes a each station: Information contained in . /// Reserved and occupied bikes: Are contained as well in . /// public StationsAvailableResponse Stations { get; private set; } /// /// Updates model from response. /// /// /// Holds /// - part of/ or all available bikes and /// - part of/ or all reserved bikes of logged in user if user is logged in and /// - part of/ or all occupied bikes of logged in user if user is logged in. /// /// Parts of model which have to be updated. public UpdateTarget Update( BikesAvailableResponse bikesAllResponse, string stationId = null, string bikeId = null) { try { if (!string.IsNullOrEmpty(stationId)) { return UpdateFromBikesAtStation( bikesAllResponse, stationId); } if (!string.IsNullOrEmpty(bikeId)) { return UpdateFromBikesAtStation( bikesAllResponse, bikesAllResponse.bikes.Values.FirstOrDefault(x => x.bike.ToUpper() == bikeId.ToUpper())?.station); } return UpdateTarget.None; } catch (System.Exception ex) { Log.ForContext().Error( "Updating response model form {@Type} for station id {@StationId} and bike id {BikeId} failed. {@Exception}", nameof(BikesAvailableResponse), stationId, bikeId, ex); return UpdateTarget.None; } } /// /// Updates cache from bike which changed rental state from available or reserved to booked. /// /// public UpdateTarget Update(BikeInfoReservedOrBooked response) { try { var updateTarget = UpdateTarget.None; if (response == null || string.IsNullOrEmpty(response.bike)) { // Can nor update station or remove bike return updateTarget; } // Update station if required i.e. decrease count of available bikes if required. updateTarget = UpdateAvailableBikesCount( BikesAll.bikes.Values.FirstOrDefault(b => b.bike == response?.bike)?.station, i => i - 1, // Decrease count of available bikes. updateTarget); // A booked bike is no more available. BikesAll.bikes.Remove(response.bike); // Update Stations with latest reserved/ rented bike. Stations.bikes_occupied.Remove(response.bike); Stations.bikes_occupied.Add(response.bike, response); // Update BikesAll with latest reserved/ rented bike. BikesAll.bikes_occupied.Remove(response.bike); BikesAll.bikes_occupied.Add(response.bike, response); // Update BikesReservedOccupied with latest reserved/ rented bike. BikesReservedOccupied.bikes_occupied.Remove(response.bike); BikesReservedOccupied.bikes_occupied.Add(response.bike, response); return updateTarget | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse; } catch (System.Exception ex) { Log.ForContext().Error( "Updating response model form {@Type} failed. {@Exception}", nameof(BikeInfoReservedOrBooked), ex); return UpdateTarget.None; } } /// Updates cache from bike which changed rental state (reservation/ booking canceled). public UpdateTarget Update(BookingActionResponse response) { try { if (response == null || string.IsNullOrEmpty(response.bike)) { // Can nor update station or remove bike return UpdateTarget.None; } // Remove available bike from Stations. Stations.bikes_occupied.Remove(response.bike); // Remove available bike from BikesAll. BikesAll.bikes_occupied.Remove(response.bike); // Remove available bike from BikesReservedOccupied. BikesReservedOccupied.bikes_occupied.Remove(response.bike); return UpdateTarget.StationsAvailableResponse | UpdateTarget.BikesAvailableResponse | UpdateTarget.BikesReservedOccupiedResponse; } catch (System.Exception ex) { Log.ForContext().Error( "Updating response model form {@Type} failed. {@Exception}", nameof(BookingActionResponse), ex); return UpdateTarget.None; } } /// /// Updates model from response. /// /// /// Holds /// - all available bikes located at given station and /// - all reserved bikes of logged in user if user is logged in located at given and /// - all occupied bikes of logged in user if user is logged in located at given . /// /// Parts of model which have to be updated. private UpdateTarget UpdateFromBikesAtStation( BikesAvailableResponse bikesAllResponse, string stationId) { /// /// Synchronizes a dictionary with another, i.e. /// - adds values which do not yet exist /// - updates existing values with new ones and /// - removes bikes which do no more exist /// /// Source dictionary to be read from. /// Target directory to be written to. void Synchonize( ComparableDictionary sourceDict, ComparableDictionary targetDict, Action updateRequiredAction) where T : BikeInfoBase, IEquatable { if (targetDict == null) { // Nothing to do if target dictionary is empty. return; } // Add/ update bikes foreach (var sourceBike in sourceDict ?? new ComparableDictionary()) { // Check if bikes already exists. var targetBike = targetDict.Values.FirstOrDefault(x => x.bike == sourceBike.Value.bike); if (targetBike != null) { // Check if bikes equals if (sourceBike.Value.Equals(targetBike)) { // Bikes equal, process next. continue; } // Remove all bikes from target which part of source to be added later. targetDict.Remove(sourceBike.Key); } targetDict.Add(sourceBike.Key, sourceBike.Value); updateRequiredAction?.Invoke(); } // Remove bikes from target dictionary which are no more at station var bikesToRemove = targetDict .Where(x => x.Value.station == stationId) .Where(x => sourceDict?.Values?.FirstOrDefault(y => y.station == stationId) == null); foreach (var bike in bikesToRemove) { targetDict.Remove(bike.Key); updateRequiredAction?.Invoke(); } } if (BikesAll == bikesAllResponse || bikesAllResponse == null) { return UpdateTarget.None; } var updateTarget = UpdateTarget.None; // Update bikes all by updating existing bikes and adding new ones. Synchonize( bikesAllResponse.bikes, BikesAll.bikes, () => { updateTarget |= UpdateTarget.BikesAvailableResponse; }); Synchonize( bikesAllResponse.bikes_occupied, BikesAll.bikes_occupied, () => { updateTarget |= UpdateTarget.BikesAvailableResponse; }); // Update stations property count of available bikes for selected station. UpdateAvailableBikesCount( bikesAllResponse.bikes.Count, Stations.stations.Values.FirstOrDefault(x => x.station == stationId), () => updateTarget |= UpdateTarget.StationsAvailableResponse); // Add/ update stations bikes reserved and stations bikes occupied. Synchonize( bikesAllResponse.bikes_occupied, Stations.bikes_occupied, () => updateTarget |= UpdateTarget.StationsAvailableResponse); // Add/ update reserved and occupied bikes. Synchonize( bikesAllResponse.bikes_occupied, BikesReservedOccupied.bikes_occupied, () => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse); // Remove available bikes from bikes available response if there are: A bike reserved or occupied can not be available at the same time. Remove( bikesAllResponse.bikes_occupied.Values.Select(x => x.bike).ToList(), BikesAll.bikes, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Remove reserved and occupied bikes from bikes available response if there are: A bike available can not be reserved or occupied at the same time. Remove( bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(), BikesAll.bikes_occupied, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Remove reserved and occupied bikes from stations response if there are: A bike available can not be reserved or occupied at the same time. Remove( bikesAllResponse.bikes.Values.Select(x => x.bike).ToList(), Stations.bikes_occupied, () => updateTarget |= UpdateTarget.StationsAvailableResponse); return updateTarget; } /// /// Updates model from reserved and occupied bikes. /// /// Bikes to update with. /// Parts of model which have to be updated. public UpdateTarget Update(BikesReservedOccupiedResponse bikesReservedOccupiedResponse) { try { if (bikesReservedOccupiedResponse == BikesReservedOccupied || bikesReservedOccupiedResponse == null) { return UpdateTarget.None; } var updateTarget = UpdateTarget.BikesReservedOccupiedResponse; BikesReservedOccupied = bikesReservedOccupiedResponse; ; // Update bikes reserved or occupied from bikes reserved or occupied response. Write( bikesReservedOccupiedResponse.bikes_occupied, BikesAll.bikes_occupied, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Update stations from bikes reserved or occupied response. Write( bikesReservedOccupiedResponse.bikes_occupied, Stations.bikes_occupied, () => updateTarget |= UpdateTarget.StationsAvailableResponse); // Remove available bikes from bikes available response: A bike reserved/ occupied can not be available at the same time. Remove( bikesReservedOccupiedResponse.bikes_occupied.Values.Select(x => x.bike).ToList(), BikesAll.bikes, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Due to possible change of bikes available from bikes available response (step above), count of bikes from stations might need to be updated. UpdateAvailableBikesCount( BikesAll.bikes, Stations.stations, () => updateTarget |= UpdateTarget.StationsAvailableResponse); return updateTarget; } catch (System.Exception ex) { Log.ForContext().Error( "Updating response model form {@Type} failed. {@Exception}", nameof(BikesReservedOccupiedResponse), ex); return UpdateTarget.None; } } /// /// Updates model from stations response. /// /// Response to update from. /// public UpdateTarget Update(StationsAvailableResponse stationsResponse) { try { // Remove bike from dictionary if located at a list of stations. // This can only be done randomly because only the count of bikes without bike ids is known (task #901). void LimitAvailableBikesCountByStation( IList stations, ComparableBikeDictionary bikes, Action updateRequiredAction) { // Group available bikes by station. var groupedBikes = bikes.GroupBy(x => x.Value.station).ToList(); foreach (var station in stations) { var bikesAtStation = groupedBikes.FirstOrDefault(x => x.Key == station.station); var toReviseCount = bikesAtStation?.ToList()?.Count ?? 0; if (string.IsNullOrEmpty(bikesAtStation?.Key) || !station.TryGetBikesAvailableCount(out var trueCount) || toReviseCount <= trueCount) { // Either // - there is no bikes at current station or // - invalid format detected // - count of bikes is ok continue; } var surplus = toReviseCount - trueCount; // There are move bikes available at current station in bikes available response than the station object reports. // Randomly remove surplus bikes. var keys = bikesAtStation.Select(x => x.Key).ToList(); for (var i = 0; i < surplus; i++) { bikes.Remove(keys[i]); } updateRequiredAction?.Invoke(); } } if (Stations == stationsResponse || stationsResponse == null) { return UpdateTarget.None; } var updateTarget = UpdateTarget.StationsAvailableResponse; Stations = stationsResponse; // Ensure that bikes which have become occupied are no more in list of available bikes foreach (var bike in Stations.bikes_occupied.Values) { if (!BikesAll.bikes.ContainsKey(bike.bike)) continue; BikesAll.bikes.Remove(bike.bike); } // Ensure that there are not more bikes assigned to one station in bikes available object than there are reported in station response. // An example for this case is that a bike is brought to garage. LimitAvailableBikesCountByStation( Stations.stations.Values.ToList(), BikesAll.bikes, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Update bikes reserved and bikes occupied from stations response. Write( Stations.bikes_occupied, BikesAll.bikes_occupied, () => updateTarget |= UpdateTarget.BikesAvailableResponse); // Update bikes reserved or occupied from stations response. Write( Stations.bikes_occupied, BikesReservedOccupied.bikes_occupied, () => updateTarget |= UpdateTarget.BikesReservedOccupiedResponse); return updateTarget; } catch (System.Exception ex) { Log.ForContext().Error( "Updating response model form {@Type} failed. {@Exception}", nameof(StationsAvailableResponse), ex); return UpdateTarget.None; } } /// /// Removes bikes from a dictionary. /// /// Type of dictionary values to remove. /// Bikes to remove. /// Dictionary to removed bikes from. /// Action to be invoked if any bike gets removed from dictionary. private void Remove( IList bikesToRemove, ComparableDictionary target, Action updateRequiredAction) where T : BikeInfoBase { if (target == null) { // Nothing to do. return; } foreach (var bike in bikesToRemove) { if (!target.Remove(bike)) { continue; } updateRequiredAction?.Invoke(); } } /// /// Writes a dictionary to another if required. /// /// Source dictionary to be read from. /// Target directory to be written to. void Write( ComparableDictionary source, ComparableDictionary target, Action updateRequiredAction) { if (target == null || source == target) { return; } target.Clear(); foreach (var bike in source ?? new ComparableBikeDictionary()) { target.Add(bike.Key, bike.Value); } updateRequiredAction?.Invoke(); } private void UpdateAvailableBikesCount( ComparableBikeDictionary bikesAvailable, ComparableDictionary stations, Action updateRequiredAction) { foreach (var station in stations.Values) { UpdateAvailableBikesCount( bikesAvailable.Values.Where(x => x.station == station.station).Count(), station, updateRequiredAction); } } /// /// Updates the count of available bikes of a given station. /// /// Id of the station to update count for. /// Delegate to return the new count. /// Update action required private UpdateTarget UpdateAvailableBikesCount( string id, Func countDelegate, UpdateTarget currentUpdateTarget) { if (string.IsNullOrEmpty(id)) { // Bike was not available before. return currentUpdateTarget; } var station = Stations.stations.Values.FirstOrDefault(s => s.station == id); if (station == null) { // There is no matching station. return currentUpdateTarget; } // Decrement count of stations. station.SetBikesAvailableCount(countDelegate(station.GetBikesAvailableCount() ?? 1)); return currentUpdateTarget | UpdateTarget.StationsAvailableResponse; } private void UpdateAvailableBikesCount( int bikesAvailableCount, StationInfo station, Action updateRequiredAction) { if (station == null || !station.TryGetBikesAvailableCount(out var targetBikesCount) || targetBikesCount == bikesAvailableCount) { // Either // - there is no matching station or // - station count if not numeric or // - count matches return; } station.SetBikesAvailableCount(bikesAvailableCount); updateRequiredAction?.Invoke(); } } }