// // ServersPresenter.swift // PrivadoVPN // // Created by Juraldinio on 3/11/21. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation protocol ServersModuleOutput: AnyObject { var serverRecords: [ServerRecord] { get } var bestRecord: ServerRecord? { get } var currentPlan: Customer.Plan { get } var customerURL: String { get } var order: ServerListRecord.Order { get } func update(order: ServerListRecord.Order) func navigate(to route: Route) func serverSelected(record: ServerRecord) } final class ServersPresenter { private enum Constants { enum LocalizedString { static let premiumTitle = "modal.premium.title" static let premiumDescription = "modal.premium.description" static let premiumButton = "modal.premium.button" } static let premiumUrlPath = "https://privadovpn.com/pricing/" } private weak var output: ServersModuleOutput? weak var viewInput: ServersControllerInput? weak var favouritesViewInput: FavouriteControllerInput? weak var serverListViewInput: ServerListControllerInput? private var preparedRecords: [ServerListRecord] { guard let output = self.output else { return [] } return Self.prepare(using: output.serverRecords, plan: output.currentPlan, favoruties: self.favouriteRecords) } private var bestRecord: ServerRecord? private var currentOrder: ServerListRecord.Order = .latencyASC private var coordinator: CoreDataCoordinator? private var favouriteRecords = [FavouriteModel]() private var favouriteCities = [ServerListCityRecord]() private var sortOptions: [SortPanelOrder] = [.latencyASC, .latencyDESC, .nameASC, .nameDESC] private var records: [ServerListRecord] { didSet { self.markLastCities() } } // MARK: - Init init(output: ServersModuleOutput?, coordinator: CoreDataCoordinator?) { self.output = output self.coordinator = coordinator guard let output = output else { self.records = [] return } self.currentOrder = output.order let favourites = Self.restoreFavourites(using: coordinator) let records = Self.prepare(using: output.serverRecords, plan: output.currentPlan, favoruties: favourites) self.records = Self.sort(list: records, plan: output.currentPlan, using: self.currentOrder) self.bestRecord = self.output?.bestRecord self.favouriteRecords = favourites } // MARK: - Static private static func prepare(using serverRecords: [ServerRecord], plan: Customer.Plan, favoruties: [FavouriteModel]) -> [ServerListRecord] { return serverRecords .map { record -> ServerListCityRecord in let isFavourite = favoruties.contains { $0.cityName == record.city && $0.isFreemium == record.isFreemium} return ServerListCityRecord(serverRecord: record, isFavourite: isFavourite, plan: plan) }.reduce([ServerListRecord]()) { acc, city in var result = acc if !city.country.isExist { // Attention! We need sort countries only in freemium! let countryRecord = acc.first(where: { if let country = $0 as? ServerListCountryRecord , country.code == city.countryCode , (plan == .freemium ? country.isFreemium == city.isFreemium : true) { return true } return false }) // Country record already exists if let country = countryRecord as? ServerListCountryRecord { country.add(city: city) city.country = country } else { let countryName = CountryResolver.name(from: city.countryCode) // Attention! We need sort countries only in freemium! let country = ServerListCountryRecord(country: countryName, code: city.countryCode, flag: city.countryCode, isFreemium: plan == .freemium ? city.isFreemium : false, plan: city.plan) country.add(city: city) city.country = country result.append(country) } } return result } } private static func sort(list: [ServerListRecord], plan: Customer.Plan, using order: ServerListRecord.Order) -> [ServerListRecord] { var records = list .filter { !$0.isNotification } // at first remove all notifications .sorted(by: { lvalue, rvalue in // at top level we have Country only! if plan == .freemium , let lCountry = lvalue as? ServerListCountryRecord , let rCountry = rvalue as? ServerListCountryRecord { switch (lCountry.isFreemium, rCountry.isFreemium) { case (true, true), (false, false): return Self.areIncreasing(lValue: lvalue, rValue: rvalue, for: order) case (true, false): return true case (false, true): return false } } return Self.areIncreasing(lValue: lvalue, rValue: rvalue, for: order) }) records.forEach { if let country = $0 as? ServerListCountryRecord { country.sortCities { lCity, rCity in Self.areIncreasing(lValue: lCity, rValue: rCity, for: order) } } } // Insert notification between freemium and not if plan == .freemium , let index = records.firstIndex(where: { if let country = $0 as? ServerListCountryRecord , !country.isFreemium { return true } return false }) { let text = NSLocalizedString("serverlist.upgrade.text", comment: "") let notification = ServerListNotificationRecord(notification: text) records.insert(notification, at: index) } return records } private static func areIncreasing(lValue: ServerListRecord, rValue: ServerListRecord, for order: ServerListRecord.Order) -> Bool { switch order { case .default: return false case .nameASC, .nameDESC: return Self.checkNames(areAscending: order == .nameASC, lValue: lValue, rValue: rValue) case .latencyASC, .latencyDESC: return Self.checkLatencies(areAscending: order == .latencyASC, lValue: lValue, rValue: rValue) } } private static func filter(list: [ServerListRecord], by searchQuery: String) -> [ServerListRecord] { guard !searchQuery.isEmpty else { return list } let query = searchQuery.lowercased() let filtered = list.filter { record in if let country = record as? ServerListCountryRecord { let hasCity = country.cities.contains { $0.city.lowercased().contains(query) } return country.country.lowercased().contains(query) || hasCity } else if let city = record as? ServerListCityRecord { return city.city.lowercased().contains(query) } else { return false } } filtered.forEach { record in if let country = record as? ServerListCountryRecord { country.filterCities { $0.city.lowercased().contains(query) } } } return filtered } private static func checkLatencies(areAscending: Bool, lValue: ServerListRecord, rValue: ServerListRecord) -> Bool { if let lCountry = lValue as? ServerListCountryRecord , let rCountry = rValue as? ServerListCountryRecord , let lBestLatency = lCountry.getBestLatency(highest: areAscending) , let rBestLatency = rCountry.getBestLatency(highest: areAscending) { return areAscending ? lBestLatency > rBestLatency : lBestLatency < rBestLatency } else if let lCity = lValue as? ServerListCityRecord , let rCity = rValue as? ServerListCityRecord , lCity.countryCode == rCity.countryCode { return areAscending ? lCity.latency > rCity.latency : lCity.latency < rCity.latency } return false } private static func checkNames(areAscending: Bool, lValue: ServerListRecord, rValue: ServerListRecord) -> Bool { if let lCountry = lValue as? ServerListCountryRecord , let rCountry = rValue as? ServerListCountryRecord { return areAscending ? lCountry.country < rCountry.country : lCountry.country > rCountry.country } else if let lCity = lValue as? ServerListCityRecord , let rCity = rValue as? ServerListCityRecord , lCity.countryCode == rCity.countryCode { return areAscending ? lCity.city < rCity.city : lCity.city > rCity.city } return false } private static func restoreFavourites(using coordinator: CoreDataCoordinator?) -> [FavouriteModel] { guard let coordinator = coordinator else { return [] } let fetch: NSFetchRequest = FavouriteRecord.fetchRequest() let result = coordinator.mainContext.fetch(fetch) return result.map { FavouriteModel(cityName: $0.city, isFreemium: $0.isFreeemium) } } private func markLastCities() { self.records.forEach { record in guard let country = record as? ServerListCountryRecord else { return } country.cities.first { $0.isLast == true }?.isLast = false country.cities.last?.isLast = true } } private func saveFavourites() { /// save favourites when leave screen in case user changed list multiple times guard let coordinator = self.coordinator else { return } let context = coordinator.createContext() let request: NSFetchRequest = FavouriteRecord.fetchRequest() context.fetch(request).forEach { context.delete($0) } context.save() coordinator.save() self.favouriteRecords.forEach { let record: FavouriteRecord = context.create() record.city = $0.cityName record.isFreeemium = $0.isFreemium context.save() } coordinator.save() } } // MARK: - ServersControllerOutput extension ServersPresenter: ServersControllerOutput { func serversViewIsReady() { self.markLastCities() self.serverListViewInput?.reloadList() guard let bestRecord = self.bestRecord else { return } let titleString = bestRecord.city + ", " + CountryResolver.name(from: bestRecord.countryCode) self.viewInput?.bestLocationWith(title: titleString, flag: bestRecord.countryCode) self.viewInput?.configureSortPanel(with: self.sortOptions) } func serverListChildrensCount(for record: ServerListRecord?) -> Int { guard let record = record else { return self.records.filter { $0.isCountry || $0.isNotification }.count } guard record.isCountry , let countryRecord = record as? ServerListCountryRecord else { return 0 } return countryRecord.cities.count } func serverListChild(at index: Int, for record: ServerListRecord?) -> ServerListRecord { guard let record = record else { return self.records[safe: index] ?? ServerListRecord.empty() } guard let country = record as? ServerListCountryRecord , let city = country.cities[safe: index] else { return ServerListRecord.empty() } return city } func serverSelected(at row: Int) { let cityRecord = self.favouriteCities[row] self.serverListSelected(cityRecord, cityRecord.country) } func serverListSelected(_ record: ServerListRecord, _ country: ServerListCountryRecord?) { guard let city = record as? ServerListCityRecord, let country = country else { return } let plan = self.output?.currentPlan ?? .freemium if plan == .freemium, !city.isFreemium { CometLogger.shared.event(name: PrivadoConstants.Event.Statistic.Application.premiumServer, attributes: [ PrivadoConstants.Event.Attributes.extradata: [ PrivadoConstants.Event.Attributes.city: city.city, PrivadoConstants.Event.Attributes.country: country.country ] ], secured: nil) let settings: Route.ModalSettings = .closable(title: NSLocalizedString(Constants.LocalizedString.premiumTitle, comment: ""), description: NSLocalizedString(Constants.LocalizedString.premiumDescription, comment: ""), buttonTitle: NSLocalizedString(Constants.LocalizedString.premiumButton, comment: ""), action: URL(string: Constants.premiumUrlPath)) let route: Route = .modal(settings: settings) self.output?.navigate(to: route) return } self.output?.serverSelected(record: city.serverRecord) self.output?.navigate(to: .back) } func serverListSelectedFavourite(_ record: ServerListCityRecord) { if let index = self.favouriteRecords.firstIndex(where: { $0.cityName == record.city && $0.isFreemium == record.isFreemium }) { record.isFavourite = false self.favouriteRecords.remove(at: index) } else { record.isFavourite = true self.favouriteRecords.append(FavouriteModel(cityName: record.city, isFreemium: record.isFreemium)) } self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite } self.sortFavourites(by: self.currentOrder) self.favouritesViewInput?.reloadList(searching: false) } func serverListSelectedFavourite(at row: Int) { let city = self.favouriteCities[row] self.favouriteRecords.removeAll { $0.cityName == city.city && $0.isFreemium == city.isFreemium } self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.first { $0.city == city.city && $0.isFreemium == city.isFreemium }?.isFavourite = false self.favouriteCities.remove(at: row) self.favouritesViewInput?.reloadList(searching: false) self.serverListViewInput?.reloadList() } func serverListWillDisappear() { self.saveFavourites() } func favouriteViewIsReady() { self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite } self.sortFavourites(by: self.currentOrder) self.favouritesViewInput?.reloadList(searching: false) } func favouriteRecord(at index: Int) -> ServerListCityRecord { return self.favouriteCities[index] } func favouriteRecordsCount() -> Int { self.favouriteCities.count } func upgradeButtonTapped() { guard let output = self.output, let url = URL(string: output.customerURL) else { return } openApplicationRoute(.navigate(url: url)) } func bestLocationSelected() { guard let bestRecord = self.output?.bestRecord else { return } self.output?.serverSelected(record: bestRecord) self.output?.navigate(to: .back) } } // MARK: - SortPanelOutput extension ServersPresenter: SearchPanelOutput { func sortPanelSelected(at index: Int) { guard let output = self.output else { return } let order = self.sortOptions[index] let listOrder: ServerListRecord.Order switch order { case .default, .nameASC: listOrder = .nameASC case .nameDESC: listOrder = .nameDESC case .latencyASC: listOrder = .latencyASC case .latencyDESC: listOrder = .latencyDESC } self.records = Self.sort(list: self.records, plan: output.currentPlan, using: listOrder) self.serverListViewInput?.reloadList() self.sortFavourites(by: listOrder) self.favouritesViewInput?.reloadList(searching: false) self.currentOrder = listOrder } func sortFavourites(by listOrder: ServerListRecord.Order) { self.favouriteCities = self.favouriteCities.sorted(by: { lCity, rCity in switch listOrder { case .nameASC: return lCity.city < rCity.city case .nameDESC: return lCity.city > rCity.city case .latencyASC: return lCity.latency > rCity.latency case .latencyDESC: return lCity.latency < rCity.latency default: return lCity.latency > rCity.latency } }) } func sortPanelDidSelect(order: SortPanelOrder) { guard let output = self.output else { return } let listOrder: ServerListRecord.Order switch order { case .default, .nameASC: listOrder = .nameASC case .nameDESC: listOrder = .nameDESC case .latencyASC: listOrder = .latencyASC case .latencyDESC: listOrder = .latencyDESC } self.records = Self.sort(list: self.records, plan: output.currentPlan, using: listOrder) self.serverListViewInput?.reloadList() self.sortFavourites(by: listOrder) self.favouritesViewInput?.reloadList(searching: false) self.currentOrder = listOrder } func sortPanelSearch(query: String) { guard let output = self.output else { return } var records = [ServerListRecord]() Self.filter(list: self.preparedRecords, by: query) .forEach { record in guard let country = record as? ServerListCountryRecord else { records.append(record) return } guard country.cities.isEmpty else { records.append(record) return } let countries = self.preparedRecords .filter { $0.isCountry } .filter { guard let value = $0 as? ServerListCountryRecord else { return false } return value == country } records.append(contentsOf: countries) } self.records = Self.sort(list: records, plan: output.currentPlan, using: self.currentOrder) self.serverListViewInput?.reloadList() self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite } self.sortFavourites(by: self.currentOrder) self.favouritesViewInput?.reloadList(searching: true) } func sortPanelSearchCancel() { guard let output = self.output else { return } self.records = Self.sort(list: self.preparedRecords, plan: output.currentPlan, using: self.currentOrder) self.serverListViewInput?.reloadList() self.favouriteCities = self.records.compactMap { $0 as? ServerListCountryRecord }.flatMap { $0.cities }.filter { $0.isFavourite } self.sortFavourites(by: self.currentOrder) self.favouritesViewInput?.reloadList(searching: true) self.viewInput?.removeSortPanel() } }