<commit_before>import SwiftUI
import WidgetKit
import WMF

// MARK: - Widget

struct TopReadWidget: Widget {
	private let kind: String = WidgetController.SupportedWidget.topRead.identifier

	public var body: some WidgetConfiguration {
		StaticConfiguration(kind: kind, provider: TopReadProvider(), content: { entry in
			TopReadView(entry: entry)
		})
		.configurationDisplayName(LocalizedStrings.topReadWidgetTitle)
		.description(LocalizedStrings.topReadWidgetDescription)
		.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
	}
}

// MARK: - Data

final class TopReadData {

	// MARK: Properties

	static let shared = TopReadData()

	let maximumRankedArticles = 4

	var placeholder: TopReadEntry {
		return TopReadEntry(date: Date())
	}

	// MARK: Public

	private var dataStore: MWKDataStore {
		MWKDataStore.shared()
	}

	func fetchLatestAvailableTopRead(_ completion: @escaping (TopReadEntry) -> Void) {
		guard
			let appLanguage = dataStore.languageLinkController.appLanguage,
			let topRead = dataStore.viewContext.group(of: .topRead, for: Date(), siteURL: appLanguage.siteURL()),
			let results = topRead.contentPreview as? [WMFFeedTopReadArticlePreview] else {
				completion(placeholder)
				return
		}

		var rankedElements: [TopReadEntry.RankedElement] = []

		for article in results {
			if let articlePreview = self.dataStore.fetchArticle(with: article.articleURL) {
				if let viewCounts = articlePreview.pageViewsSortedByDate {
					rankedElements.append(.init(title: article.displayTitle, description: article.wikidataDescription ?? article.snippet ?? "", articleURL: article.articleURL, thumbnailURL: article.thumbnailURL, viewCounts: viewCounts))
				}
			}
		}

		rankedElements = Array(rankedElements.prefix(maximumRankedArticles))

		let group = DispatchGroup()

		for (index, element) in rankedElements.enumerated() {
			group.enter()
			guard let thumbnailURL = element.thumbnailURL, let fetcher = ImageCacheController.shared else {
				group.leave()
				continue
			}

			fetcher.fetchImage(withURL: thumbnailURL, failure: { _ in
				group.leave()
			}, success: { fetchedImage in
				rankedElements[index].image = fetchedImage.image.staticImage
				group.leave()
			})
		}

		group.notify(queue: .main) {
			completion(TopReadEntry(date: Date(), rankedElements: rankedElements))
		}
	}

}

// MARK: - Model

struct TopReadEntry: TimelineEntry {
	struct RankedElement: Identifiable {
		var id: String = UUID().uuidString

		let title: String
		let description: String
		var articleURL: URL? = nil
		var image: UIImage? = nil
		var thumbnailURL: URL? = nil
		let viewCounts: [NSNumber]
	}

	let date: Date // for Timeline Entry
	var rankedElements: [RankedElement] = Array(repeating: RankedElement.init(title: "–", description: "–", image: nil, viewCounts: [.init(floatLiteral: 0)]), count: 4)
}

// MARK: - TimelineProvider

struct TopReadProvider: TimelineProvider {

	// MARK: Nested Types

	public typealias Entry = TopReadEntry

	// MARK: Properties

	private let dataStore = TopReadData.shared

	// MARK: TimelineProvider

	func placeholder(in: Context) -> TopReadEntry {
		return dataStore.placeholder
	}

	func getTimeline(in context: Context, completion: @escaping (Timeline<TopReadEntry>) -> Void) {
		dataStore.fetchLatestAvailableTopRead { entry in
			let timeline = Timeline(entries: [entry], policy: .atEnd)
			completion(timeline)
		}
	}

	func getSnapshot(in context: Context, completion: @escaping (TopReadEntry) -> Void) {
		// TODO: Support context.isPreview
		dataStore.fetchLatestAvailableTopRead { entry in
			completion(entry)
		}
	}

}

// MARK: - Views

struct TopReadView: View {
	@Environment(\.widgetFamily) private var family
	@Environment(\.colorScheme) private var colorScheme

	var entry: TopReadProvider.Entry?

	@ViewBuilder
	var body: some View {
		GeometryReader { proxy in
			switch family {
			case .systemMedium:
				rowBasedWidget(.systemMedium)
			case .systemLarge:
				rowBasedWidget(.systemLarge)
			default:
				smallWidget
					.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
					.overlay(TopReadOverlayView(rankedElement: entry?.rankedElements.first))
					.widgetURL(entry?.rankedElements.first?.articleURL)
			}
		}
	}

	// MARK: View Components

	@ViewBuilder
	var smallWidget: some View {
		if let image = entry?.rankedElements.first?.image {
			Image(uiImage: image).resizable().scaledToFill()
		} else {
			Rectangle().foregroundColor(Color(.systemFill)).scaledToFill()
		}
	}

	@ViewBuilder
	func rowBasedWidget(_ family: WidgetFamily) -> some View {
		let showSparkline = family == .systemLarge ? true : false
		let rowCount = family == .systemLarge ? 4 : 2<commit_msg>Open `groupURL` when widget is tapped to display top read in app.<commit_after>import SwiftUI
import WidgetKit
import WMF

// MARK: - Widget

struct TopReadWidget: Widget {
	private let kind: String = WidgetController.SupportedWidget.topRead.identifier

	public var body: some WidgetConfiguration {
		StaticConfiguration(kind: kind, provider: TopReadProvider(), content: { entry in
			TopReadView(entry: entry)
		})
		.configurationDisplayName(LocalizedStrings.topReadWidgetTitle)
		.description(LocalizedStrings.topReadWidgetDescription)
		.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
	}
}

// MARK: - Data

final class TopReadData {

	// MARK: Properties

	static let shared = TopReadData()

	let maximumRankedArticles = 4

	var placeholder: TopReadEntry {
		return TopReadEntry(date: Date())
	}

	// MARK: Public

	private var dataStore: MWKDataStore {
		MWKDataStore.shared()
	}

	func fetchLatestAvailableTopRead(_ completion: @escaping (TopReadEntry) -> Void) {
		guard
			let appLanguage = dataStore.languageLinkController.appLanguage,
			let topRead = dataStore.viewContext.group(of: .topRead, for: Date(), siteURL: appLanguage.siteURL()),
			let results = topRead.contentPreview as? [WMFFeedTopReadArticlePreview] else {
				completion(placeholder)
				return
		}

		var rankedElements: [TopReadEntry.RankedElement] = []

		for article in results {
			if let articlePreview = self.dataStore.fetchArticle(with: article.articleURL) {
				if let viewCounts = articlePreview.pageViewsSortedByDate {
					rankedElements.append(.init(title: article.displayTitle, description: article.wikidataDescription ?? article.snippet ?? "", articleURL: article.articleURL, thumbnailURL: article.thumbnailURL, viewCounts: viewCounts))
				}
			}
		}

		rankedElements = Array(rankedElements.prefix(maximumRankedArticles))

		let group = DispatchGroup()

		for (index, element) in rankedElements.enumerated() {
			group.enter()
			guard let thumbnailURL = element.thumbnailURL, let fetcher = ImageCacheController.shared else {
				group.leave()
				continue
			}

			fetcher.fetchImage(withURL: thumbnailURL, failure: { _ in
				group.leave()
			}, success: { fetchedImage in
				rankedElements[index].image = fetchedImage.image.staticImage
				group.leave()
			})
		}

		group.notify(queue: .main) {
			completion(TopReadEntry(date: Date(), rankedElements: rankedElements, groupURL: topRead.url))
		}
	}

}

// MARK: - Model

struct TopReadEntry: TimelineEntry {
	struct RankedElement: Identifiable {
		var id: String = UUID().uuidString

		let title: String
		let description: String
		var articleURL: URL? = nil
		var image: UIImage? = nil
		var thumbnailURL: URL? = nil
		let viewCounts: [NSNumber]
	}

	let date: Date // for Timeline Entry
	var rankedElements: [RankedElement] = Array(repeating: RankedElement.init(title: "–", description: "–", image: nil, viewCounts: [.init(floatLiteral: 0)]), count: 4)
	var groupURL: URL? = nil
}

// MARK: - TimelineProvider

struct TopReadProvider: TimelineProvider {

	// MARK: Nested Types

	public typealias Entry = TopReadEntry

	// MARK: Properties

	private let dataStore = TopReadData.shared

	// MARK: TimelineProvider

	func placeholder(in: Context) -> TopReadEntry {
		return dataStore.placeholder
	}

	func getTimeline(in context: Context, completion: @escaping (Timeline<TopReadEntry>) -> Void) {
		dataStore.fetchLatestAvailableTopRead { entry in
			let timeline = Timeline(entries: [entry], policy: .atEnd)
			completion(timeline)
		}
	}

	func getSnapshot(in context: Context, completion: @escaping (TopReadEntry) -> Void) {
		// TODO: Support context.isPreview
		dataStore.fetchLatestAvailableTopRead { entry in
			completion(entry)
		}
	}

}

// MARK: - Views

struct TopReadView: View {
	@Environment(\.widgetFamily) private var family
	@Environment(\.colorScheme) private var colorScheme

	var entry: TopReadProvider.Entry?

	@ViewBuilder
	var body: some View {
		GeometryReader { proxy in
			switch family {
			case .systemMedium:
				rowBasedWidget(.systemMedium)
					.widgetURL(entry?.groupURL)
			case .systemLarge:
				rowBasedWidget(.systemLarge)
					.widgetURL(entry?.groupURL)
			default:
				smallWidget
					.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
					.overlay(TopReadOverlayView(rankedElement: entry?.rankedElements.first))
					.widgetURL(entry?.rankedElements.first?.articleURL)
			}
		}
	}

	// MARK: View Components

	@ViewBuilder
	var smallWidget: some View {
		if let image = entry?.rankedElements.first?.image {
			Image(uiImage: image).resizable().scaledToFill()
		} else {
			Rectangle().foregroundColor(Color(.systemFill)).scaledToFill()
		}
	}

	@ViewBuilder
	func rowBasedWidget(_ family: WidgetFamily) -> some View {
		let showSparkline = family == .systemLarge ? true : false
		let rowCount = family == .systemLarge ? 4 : 2
{'fraction_non_alphanumeric': 0.10683268808203411, 'fraction_numerical': 0.0010463534581981794, 'mean_word_length': 4.31, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '505046', 'n_tokens_mistral': 3286, 'n_tokens_neox': 2874, 'n_words': 706}