// // PDFGenerator.swift // SportsTime // import Foundation import PDFKit import UIKit actor PDFGenerator { // MARK: - Generate PDF func generatePDF(for trip: Trip, games: [UUID: RichGame]) async throws -> Data { let pageWidth: CGFloat = 612 // Letter size let pageHeight: CGFloat = 792 let margin: CGFloat = 50 let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)) let data = pdfRenderer.pdfData { context in var currentY: CGFloat = margin // Page 1: Cover context.beginPage() currentY = drawCoverPage( context: context, trip: trip, pageWidth: pageWidth, margin: margin ) // Page 2+: Itinerary context.beginPage() currentY = margin currentY = drawItineraryHeader( context: context, y: currentY, pageWidth: pageWidth, margin: margin ) for day in trip.itineraryDays() { // Check if we need a new page if currentY > pageHeight - 200 { context.beginPage() currentY = margin } currentY = drawDay( context: context, day: day, games: games, y: currentY, pageWidth: pageWidth, margin: margin ) currentY += 20 // Space between days } // Summary page context.beginPage() drawSummaryPage( context: context, trip: trip, pageWidth: pageWidth, margin: margin ) } return data } // MARK: - Cover Page private func drawCoverPage( context: UIGraphicsPDFRendererContext, trip: Trip, pageWidth: CGFloat, margin: CGFloat ) -> CGFloat { var y: CGFloat = 150 // Title let titleAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: 32), .foregroundColor: UIColor.black ] let title = trip.name let titleRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 50) (title as NSString).draw(in: titleRect, withAttributes: titleAttributes) y += 60 // Date range let subtitleAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 18), .foregroundColor: UIColor.darkGray ] let dateRange = trip.formattedDateRange let dateRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 30) (dateRange as NSString).draw(in: dateRect, withAttributes: subtitleAttributes) y += 50 // Quick stats let statsAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.gray ] let stats = """ \(trip.stops.count) Cities • \(trip.totalGames) Games • \(trip.formattedTotalDistance) \(trip.tripDuration) Days • \(trip.formattedTotalDriving) Driving """ let statsRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 50) (stats as NSString).draw(in: statsRect, withAttributes: statsAttributes) y += 80 // Cities list let citiesTitle = "Cities Visited" let citiesTitleRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 25) (citiesTitle as NSString).draw(in: citiesTitleRect, withAttributes: [ .font: UIFont.boldSystemFont(ofSize: 16), .foregroundColor: UIColor.black ]) y += 30 for city in trip.cities { let cityRect = CGRect(x: margin + 20, y: y, width: pageWidth - margin * 2 - 20, height: 20) ("• \(city)" as NSString).draw(in: cityRect, withAttributes: statsAttributes) y += 22 } return y } // MARK: - Itinerary Header private func drawItineraryHeader( context: UIGraphicsPDFRendererContext, y: CGFloat, pageWidth: CGFloat, margin: CGFloat ) -> CGFloat { let headerAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: 24), .foregroundColor: UIColor.black ] let header = "Day-by-Day Itinerary" let headerRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 35) (header as NSString).draw(in: headerRect, withAttributes: headerAttributes) return y + 50 } // MARK: - Day Section private func drawDay( context: UIGraphicsPDFRendererContext, day: ItineraryDay, games: [UUID: RichGame], y: CGFloat, pageWidth: CGFloat, margin: CGFloat ) -> CGFloat { var currentY = y // Day header let dayHeaderAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: 16), .foregroundColor: UIColor.systemBlue ] let dayHeader = "Day \(day.dayNumber): \(day.formattedDate)" let dayHeaderRect = CGRect(x: margin, y: currentY, width: pageWidth - margin * 2, height: 25) (dayHeader as NSString).draw(in: dayHeaderRect, withAttributes: dayHeaderAttributes) currentY += 28 // City if let city = day.primaryCity { let cityAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.darkGray ] let cityRect = CGRect(x: margin + 10, y: currentY, width: pageWidth - margin * 2 - 10, height: 20) ("📍 \(city)" as NSString).draw(in: cityRect, withAttributes: cityAttributes) currentY += 24 } // Travel segment if day.hasTravelSegment { for segment in day.travelSegments { let travelText = "🚗 \(segment.fromLocation.name) → \(segment.toLocation.name) (\(segment.formattedDistance), \(segment.formattedDuration))" let travelRect = CGRect(x: margin + 10, y: currentY, width: pageWidth - margin * 2 - 10, height: 20) (travelText as NSString).draw(in: travelRect, withAttributes: [ .font: UIFont.systemFont(ofSize: 12), .foregroundColor: UIColor.gray ]) currentY += 22 } } // Games for gameId in day.gameIds { if let richGame = games[gameId] { let gameText = "⚾ \(richGame.fullMatchupDescription)" let gameRect = CGRect(x: margin + 10, y: currentY, width: pageWidth - margin * 2 - 10, height: 20) (gameText as NSString).draw(in: gameRect, withAttributes: [ .font: UIFont.systemFont(ofSize: 13), .foregroundColor: UIColor.black ]) currentY += 20 let venueText = " \(richGame.venueDescription) • \(richGame.game.gameTime)" let venueRect = CGRect(x: margin + 10, y: currentY, width: pageWidth - margin * 2 - 10, height: 18) (venueText as NSString).draw(in: venueRect, withAttributes: [ .font: UIFont.systemFont(ofSize: 11), .foregroundColor: UIColor.gray ]) currentY += 22 } } // Rest day indicator if day.isRestDay { let restText = "😴 Rest Day" let restRect = CGRect(x: margin + 10, y: currentY, width: pageWidth - margin * 2 - 10, height: 20) (restText as NSString).draw(in: restRect, withAttributes: [ .font: UIFont.italicSystemFont(ofSize: 12), .foregroundColor: UIColor.gray ]) currentY += 22 } // Separator line currentY += 5 let path = UIBezierPath() path.move(to: CGPoint(x: margin, y: currentY)) path.addLine(to: CGPoint(x: pageWidth - margin, y: currentY)) UIColor.lightGray.setStroke() path.lineWidth = 0.5 path.stroke() currentY += 10 return currentY } // MARK: - Summary Page private func drawSummaryPage( context: UIGraphicsPDFRendererContext, trip: Trip, pageWidth: CGFloat, margin: CGFloat ) { var y: CGFloat = margin // Header let headerAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: 24), .foregroundColor: UIColor.black ] let header = "Trip Summary" let headerRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 35) (header as NSString).draw(in: headerRect, withAttributes: headerAttributes) y += 50 // Stats let statAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.black ] let stats = [ ("Total Duration", "\(trip.tripDuration) days"), ("Total Distance", trip.formattedTotalDistance), ("Total Driving Time", trip.formattedTotalDriving), ("Average Daily Driving", String(format: "%.1f hours", trip.averageDrivingHoursPerDay)), ("Cities Visited", "\(trip.stops.count)"), ("Games Attended", "\(trip.totalGames)"), ("Sports", trip.uniqueSports.map { $0.rawValue }.joined(separator: ", ")) ] for (label, value) in stats { let labelRect = CGRect(x: margin, y: y, width: 200, height: 22) ("\(label):" as NSString).draw(in: labelRect, withAttributes: [ .font: UIFont.boldSystemFont(ofSize: 13), .foregroundColor: UIColor.darkGray ]) let valueRect = CGRect(x: margin + 200, y: y, width: pageWidth - margin * 2 - 200, height: 22) (value as NSString).draw(in: valueRect, withAttributes: statAttributes) y += 26 } // Score (if available) if let score = trip.score { y += 20 let scoreHeader = "Trip Score: \(score.scoreGrade) (\(score.formattedOverallScore)/100)" let scoreRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 30) (scoreHeader as NSString).draw(in: scoreRect, withAttributes: [ .font: UIFont.boldSystemFont(ofSize: 18), .foregroundColor: UIColor.systemGreen ]) } // Footer y = 720 let footerText = "Generated by Sport Travel Planner" let footerRect = CGRect(x: margin, y: y, width: pageWidth - margin * 2, height: 20) (footerText as NSString).draw(in: footerRect, withAttributes: [ .font: UIFont.italicSystemFont(ofSize: 10), .foregroundColor: UIColor.lightGray ]) } } // MARK: - Export Service actor ExportService { private let pdfGenerator = PDFGenerator() func exportToPDF(trip: Trip, games: [UUID: RichGame]) async throws -> URL { let data = try await pdfGenerator.generatePDF(for: trip, games: games) let fileName = "\(trip.name.replacingOccurrences(of: " ", with: "_"))_\(Date().timeIntervalSince1970).pdf" let url = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) try data.write(to: url) return url } func shareTrip(_ trip: Trip) -> URL? { // Generate a shareable deep link // In production, this would create a proper share URL let baseURL = "sportstime://trip/" return URL(string: baseURL + trip.id.uuidString) } }