import WidgetKit
import SwiftUI
import WMF
import UIKit

// MARK: - Views

struct OnThisDayColors {
    static func blueColor(_ colorScheme: ColorScheme) -> Color {
        return colorScheme == .light ? Color(.blue600) : Color(.blue300)
    }

    static func grayColor(_ colorScheme: ColorScheme) -> Color {
        return colorScheme == .light ? Color(.gray500) : Color(.gray300)
    }

    static func widgetBackgroundColor(_ colorScheme: ColorScheme) -> Color {
        return colorScheme == .light ? .white : .black
    }

    static func boxShadowColor(_ colorScheme: ColorScheme) -> Color {
        return colorScheme == .light ? Color(.gray300.withAlphaComponent(0.55)) : .clear
    }

    static func boxBackgroundColor(_ colorScheme: ColorScheme) -> Color {
        return colorScheme == .light ? .white : Color(.gray700)
    }
}

struct OnThisDayView: View {
    @Environment(\.widgetFamily) private var widgetSize
    @Environment(\.sizeCategory) var textSize
    var entry: OnThisDayProvider.Entry

    var needsVerticalCompression: Bool {
        return textSize == .extraExtraExtraLarge
    }

    @ViewBuilder
    var body: some View {
        GeometryReader { proxy in
            switch widgetSize {
            case .systemLarge:
                VStack(alignment: .leading, spacing: 0) {
                    OnThisDayHeaderElement(widgetTitle: entry.onThisDayTitle, yearRange: entry.yearRange, monthDay: entry.monthDay)
                        .padding(.bottom, needsVerticalCompression ? 3 : 9)
                    MainOnThisDayTopElement(monthDay: entry.monthDay, eventYear: entry.eventYear, eventYearsAgo: entry.eventYearsAgo, fullDate: entry.fullDate)
                    /// The full `MainOnThisDayElement` is not used in the large widget. We need the `Spacer` and the `eventSnippet` text to be part of the same `VStack` to render correctly. (Otherwise, the "text is so long it must be cutoff" and/or the "text is so short we need blank space at the bottom" scenario perform incorrectly.)
                    if let eventSnippet = entry.eventSnippet, let title = entry.articleTitle, let articleSnippet = entry.articleSnippet {
                        LargeWidgetMiddleSection(eventSnippet: eventSnippet, title: title, description: articleSnippet, image: entry.articleImage, link: entry.articleURL ?? entry.contentURL)
                            .padding(.top, needsVerticalCompression ? 3 : 9)
                            .layoutPriority(1.0)
                    }
                    OnThisDayAdditionalEventsElement(otherEventsText: entry.otherEventsText)
                }
                .padding([.top, .leading, .trailing], 16)
            case .systemMedium:
                VStack(alignment: .leading, spacing: 0) {
                    MainOnThisDayElement(monthDay: entry.monthDay, eventYear: entry.eventYear, eventYearsAgo: entry.eventYearsAgo, fullDate: entry.fullDate, snippet: entry.eventSnippet)
                    OnThisDayAdditionalEventsElement(otherEventsText: entry.otherEventsText)
                }
                .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
            case .systemSmall:
                MainOnThisDayElement(monthDay: entry.monthDay, eventYear: entry.eventYear, eventYearsAgo: entry.eventYearsAgo, fullDate: entry.fullDate, snippet: entry.eventSnippet)
                .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
            default:
                MainOnThisDayElement(monthDay: entry.monthDay, eventYear: entry.eventYear, eventYearsAgo: entry.eventYearsAgo, fullDate: entry.fullDate, snippet: entry.eventSnippet)
                .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
            }
        }
        .overlay(errorBox)
        .environment(\.layoutDirection, entry.isRTLLanguage ? .rightToLeft : .leftToRight)
        .widgetURL(entry.contentURL)
    }

    var errorBox: some View {
        if let error = entry.error {
            return AnyView(ErrorSquare(error: error))
        } else {
            return AnyView(EmptyView())
        }
    }
}

struct LargeYValuePreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 20
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct SmallYValuePreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 20
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct ErrorSquare: View {
    @Environment(\.widgetFamily) private var widgetSize
    let error: OnThisDayData.ErrorType

    var body: some View {
        Rectangle().foregroundColor(error.errorColor)
            .overlay(
                Text(error.errorText)
                    .font(.caption)
                    .bold()
                    .multilineTextAlignment(.leading)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
                    .foregroundColor(.white)
                    .padding([.leading, .top, .bottom], 16)
                    .padding(.trailing, widgetSize == .systemSmall ? 16 : 90)
            )
    }
}

struct TimelineView<Content: View>: View {
    enum DotStyle {
        case large, small, none
    }
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.widgetFamily) private var widgetSize

    @SwiftUI.State private var circleYOffset: CGFloat = 0
    var dotStyle: DotStyle
    var isLineTopFaded: Bool
    var isLineBottomFaded: Bool
    var mainView: Content

    var gradient: LinearGradient {
        if isLineTopFaded {
            let colorGradient = Gradient(stops: [
                Gradient.Stop(color: OnThisDayColors.widgetBackgroundColor(colorScheme), location: 0),
                Gradient.Stop(color: OnThisDayColors.blueColor(colorScheme), location: 0.65)
            ])
            return LinearGradient(gradient: colorGradient, startPoint: .top, endPoint: .bottom)
        /// No bottom gradient on large widgets. See Phab for details: https://phabricator.wikimedia.org/T259840#6448845
        } else if isLineBottomFaded && widgetSize != .systemLarge {
            let colorGradient = Gradient(stops: [
                // Small widgets have a much larger final section, and thus gradient starts later.
                Gradient.Stop(color: OnThisDayColors.blueColor(colorScheme), location: (widgetSize == .systemSmall ? 0.75 : 0.45)),
                Gradient.Stop(color: OnThisDayColors.widgetBackgroundColor(colorScheme), location: 1.0)
            ])
            return LinearGradient(gradient: colorGradient, startPoint: .top, endPoint: .bottom)
        } else {
            // plain blue
            return LinearGradient(gradient: Gradient(colors: [OnThisDayColors.blueColor(colorScheme)]), startPoint: .top, endPoint: .bottom)
        }
    }

    var body: some View {
        let lineWidth: CGFloat = 1
        HStack(alignment: .top) {
            ZStack(alignment: .top) {
                TimelinePathElement()
                    .stroke(lineWidth: lineWidth)
                    .fill(gradient)
                switch dotStyle {
                case .large: TimelineLargeCircleElement(lineWidth: lineWidth, circleYOffset: circleYOffset)
                case .small: TimelineSmallCircleElement(lineWidth: lineWidth, circleYOffset: circleYOffset)
                case .none: EmptyView()
                }
            }
            .frame(width: TimelineLargeCircleElement.largeCircleHeight)
            mainView
        }
        .onPreferenceChange(SmallYValuePreferenceKey.self, perform: { yOffset in
            if dotStyle == .small {
                self.circleYOffset = yOffset
            }
        })
        .onPreferenceChange(LargeYValuePreferenceKey.self, perform: { yOffset in
            if dotStyle == .large {
                self.circleYOffset = yOffset
            }
        })
    }
}

/// This is extremely hacky. Once adding padding to views and/or a Spacer() view, the timeline portion of view doesn't take up the full vertical spacce that it should. After exploring numerous other options, I went with this choice - adding some arbitrary extra length to the end of the line. Someday when SwiftUI layout works better, we can remove the +20.
struct TimelinePathElement: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.midX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY + 20))
        return path
    }
}

struct TimelineSmallCircleElement: View {
    @Environment(\.colorScheme) var colorScheme

    static let smallCircleHeight: CGFloat = 9.0
    let lineWidth: CGFloat
    let circleYOffset: CGFloat

    var body: some View {
        Circle()
        .overlay(
            Circle()
                .stroke(OnThisDayColors.blueColor(colorScheme), lineWidth: lineWidth)
        ).foregroundColor(OnThisDayColors.widgetBackgroundColor(colorScheme))
            .frame(width: TimelineSmallCircleElement.smallCircleHeight, height: TimelineSmallCircleElement.smallCircleHeight)
            .padding(EdgeInsets(top: circleYOffset, leading: 0, bottom: 0, trailing: 0))
    }
}

struct TimelineLargeCircleElement: View {
    static let largeCircleHeight: CGFloat = 17.0
    @Environment(\.colorScheme) var colorScheme

    let lineWidth: CGFloat
    let circleYOffset: CGFloat

    var body: some View {
        GeometryReader { geometry in
        Circle()
            .stroke(OnThisDayColors.blueColor(colorScheme), lineWidth: lineWidth)
            .overlay(
                Circle()
                .overlay(
                    Circle()
                        .stroke(OnThisDayColors.blueColor(colorScheme), lineWidth: lineWidth)
                )
                    .frame(width: TimelineSmallCircleElement.smallCircleHeight, height: TimelineSmallCircleElement.smallCircleHeight)
                    .foregroundColor(OnThisDayColors.blueColor(colorScheme))
            )
            .frame(width: geometry.size.width, height: geometry.size.width)
            .padding(.top, circleYOffset)
        }
    }
}

struct OnThisDayHeaderElement: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.sizeCategory) var textSize
    let widgetTitle: String
    let yearRange: String
    let monthDay: String

    var needsVerticalCompression: Bool {
        return textSize == .extraExtraExtraLarge
    }

    var body: some View {
        /// Custom spacing (handled by middle text element) from 10 Sept 2020 video call w/ Carolyn, the app designer.
        VStack(spacing: 0) {
            Text(widgetTitle)
                .foregroundColor(OnThisDayColors.grayColor(colorScheme))
                .font(.subheadline)
                .bold()
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
            Text(monthDay)
                .font(.title2)
                .bold()
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(.top, needsVerticalCompression ? 2 : 5)
                .padding(.bottom, needsVerticalCompression ? 2 : 7)
            Text(yearRange)
                .foregroundColor(OnThisDayColors.grayColor(colorScheme))
                .font(.subheadline)
                .bold()
                .multilineTextAlignment(.leading)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

struct MainOnThisDayElement: View {
    @Environment(\.widgetFamily) private var widgetSize

    var monthDay: String
    var eventYear: String
    var eventYearsAgo: String?
    var fullDate: String
    var snippet: String?

    /// For unknown reasons, the layout of the `TimelineView` for a large widget is different from the rest. (A larger comment is above.) One side affect is that (as of iOS 14, beta 6) the large dot is not properly centered on the large widget. This `isLargeWidget` boolean allows us to manually correct for the error.

    var body: some View {
        VStack(spacing: 0) {
            MainOnThisDayTopElement(monthDay: monthDay, eventYear: eventYear, eventYearsAgo: eventYearsAgo, fullDate: fullDate)
            if let snippet = snippet {
                TimelineView(dotStyle: .none, isLineTopFaded: false, isLineBottomFaded: widgetSize == .systemSmall, mainView:
                        Text(snippet)
                            .font(.caption)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .padding(.top, 9)
                            .padding(.bottom, (widgetSize == .systemMedium ? 4 : 8))
                ).layoutPriority(1.0)
            }
        }.layoutPriority(1.0)
    }
}

struct MainOnThisDayTopElement: View {
    let eventYearPadding: CGFloat = 16.0
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.widgetFamily) private var widgetSize

    var monthDay: String
    var eventYear: String
    var eventYearsAgo: String?
    var fullDate: String

    var firstLineText: String {
        if widgetSize != .systemMedium {
            return eventYear
        } else {
            return fullDate
        }
    }

    private var secondLineText: String? {
        if widgetSize == .systemSmall {
            return monthDay
        } else {
            return eventYearsAgo
        }
    }

    var body: some View {
        if let secondLineText = secondLineText {
            TimelineView(dotStyle: .large, isLineTopFaded: true, isLineBottomFaded: false, mainView:
                    Text(firstLineText)
                        .font(.subheadline)
                        .foregroundColor(OnThisDayColors.blueColor(colorScheme))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .overlay(
                            GeometryReader { geometryProxy in
                              Color.clear
                                .preference(key: LargeYValuePreferenceKey.self, value: startYOfCircle(viewHeight: geometryProxy.size.height, circleHeight: TimelineLargeCircleElement.largeCircleHeight, topPadding: eventYearPadding))
                            }
                        )
                        .padding(.top, eventYearPadding)
            )
            TimelineView(dotStyle: .none, isLineTopFaded: false, isLineBottomFaded: false, mainView:
                    Text(secondLineText)
                        .font(.caption)
                        .foregroundColor(OnThisDayColors.grayColor(colorScheme))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding(.top, 3)
            )
        }
    }
}

struct LargeWidgetMiddleSection: View {
    let eventSnippet: String
    let title: String
    let description: String
    let image: UIImage?
    let link: URL?

    var body: some View {
        TimelineView(dotStyle: .none, isLineTopFaded: false, isLineBottomFaded: false, mainView:
            VStack(alignment: .leading, spacing: 0) {
                Text(eventSnippet)
                    .font(.caption)
                if let link = link {
                    Link(destination: link) {
                        ArticleRectangleBox(title: title, description: description, image: image)
                    }
                } else {
                    ArticleRectangleBox(title: title, description: description, image: image)
                }
                Spacer(minLength: 0)
            }
        )
    }
}

struct ArticleRectangleBox: View {
    @Environment(\.colorScheme) var colorScheme

    let title: String
    let description: String
    let image: UIImage?

    var body: some View {
        HStack(spacing: 9) {
            VStack {
                Text(title)
                    .font(.caption)
                    .bold()
                    .frame(maxWidth: .infinity, alignment: .leading)
                Text(description)
                    .font(.caption)
                    .lineLimit(1)
                    .foregroundColor(OnThisDayColors.grayColor(colorScheme))
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 36, height: 36, alignment: .center)
                    .cornerRadius(2.0)
            }
        }
        .padding(9)
        .background(
            RoundedRectangle(cornerRadius: 2.0)
                .shadow(color: OnThisDayColors.boxShadowColor(colorScheme), radius: 4.0, x: 0, y: 2)
                .foregroundColor(OnThisDayColors.boxBackgroundColor(colorScheme))
        )
        .padding([.top, .bottom], 9)
        .padding([.trailing], 35)
    }
}

struct OnThisDayAdditionalEventsElement: View {
    @Environment(\.colorScheme) var colorScheme

    let otherEventsText: String

    var body: some View {
        TimelineView(dotStyle: .small, isLineTopFaded: false, isLineBottomFaded: true, mainView:
            Text(otherEventsText)
                .font(.footnote)
                .bold()
                .lineLimit(1)
                .foregroundColor(OnThisDayColors.blueColor(colorScheme))
                .frame(maxWidth: .infinity, alignment: .topLeading)
            .padding(.top, 2)
            .overlay(
                GeometryReader { geometryProxy in
                  Color.clear
                    .preference(key: SmallYValuePreferenceKey.self, value: startYOfCircle(viewHeight: geometryProxy.size.height, circleHeight: TimelineSmallCircleElement.smallCircleHeight, topPadding: 0))
                    /// The padding of 0 is a little arbitrary. These views aren't perfectly laid out in SwiftUI - see the "+20" comment above - and depending on spacing/padding we sometims need to tweak the padding to make this layout properly.
                }
            )
            .padding(.bottom, 16)
        )
    }
}

private func startYOfCircle(viewHeight: CGFloat, circleHeight: CGFloat, topPadding: CGFloat = 0) -> CGFloat {
    return topPadding + ((viewHeight - circleHeight)/2)
}

// MARK: - Preview

struct OnThisDayDayWidget_Previews: PreviewProvider {
    static var previews: some View {
        OnThisDayView(entry: OnThisDayData.shared.placeholderEntryFromLanguage(nil))
            .previewContext(WidgetPreviewContext(family: .systemLarge))
    }
}
