import Foundation
import SwiftUI

// swiftlint:disable file_length

public struct TypographyV1: Hashable {
    public let name: String
    public var size: CGFloat
    public var style: Font.TextStyle
    public var weight: Weight
    public var kerning: CGFloat
    public var lineHeight: CGFloat

    public init(
        name: String,
        size: CGFloat,
        style: Font.TextStyle,
        weight: Weight = .neueMontrealRegular,
        kerning: CGFloat = 0,
        lineHeight: CGFloat = 0
    ) {
        self.name = name
        self.size = size
        self.style = style
        self.weight = weight
        self.kerning = kerning
        self.lineHeight = lineHeight
    }
}

public extension TypographyV1 {
    /// size: 60pt, lineHeight: 60pt, weight: light
    static let headlineXL: TypographyV1 = .init(
        name: "Headline XL",
        size: 60,
        style: .largeTitle,
        weight: .editorialNewLight,
        lineHeight: 60
    )

    /// size: 40pt, lineHeight: 48pt, weight: light
    static let headline1: TypographyV1 = .init(
        name: "Headline 1",
        size: 40,
        style: .largeTitle,
        weight: .editorialNewLight,
        lineHeight: 48
    )

    /// size: 32pt, lineHeight: 40pt, weight: semi bold
    static let headline2: TypographyV1 = .init(
        name: "Headline 2",
        size: 32,
        style: .title,
        weight: .ppNeueMontrealSemiBold,
        lineHeight: 40
    )

    /// size: 24pt, lineHeight: 32pt, weight: semi bold
    static let headline3: TypographyV1 = .init(
        name: "Headline 3",
        size: 24,
        style: .title,
        weight: .ppNeueMontrealSemiBold,
        lineHeight: 32
    )

    /// size: 20pt, lineHeight: 24pt, weight: medium
    static let headline4: TypographyV1 = .init(
        name: "Headline 4",
        size: 20,
        style: .title,
        weight: .neueMontrealMedium,
        lineHeight: 24
    )

    /// size: 20pt, lineHeight: 24pt, weight: medium
    static let headline4Wide: TypographyV1 = .init(
        name: "Headline 4",
        size: 20,
        style: .title,
        weight: .neueMontrealMedium,
        kerning: 0.6,
        lineHeight: 24
    )

    /// size: 18pt, lineHeight: 20pt, weight: medium
    static let headline5: TypographyV1 = .init(
        name: "Headline 5",
        size: 18,
        style: .title,
        weight: .neueMontrealMedium,
        kerning: 1.04,
        lineHeight: 20
    )

    /// size: 17pt, lineHeight: 16pt, weight: medium
    static let headline6: TypographyV1 = .init(
        name: "Headline 6",
        size: 17,
        style: .title,
        weight: .neueMontrealMedium,
        kerning: 1.02,
        lineHeight: 16
    )

    /// size: 12pt, lineHeight: 16pt, weight: medium
    static let subtitleSmall: TypographyV1 = .init(
        name: "Subtitle Small",
        size: 12,
        style: .body,
        weight: .neueMontrealMedium,
        lineHeight: 16
    )

    /// size: 12pt, lineHeight: 16pt, weight: regular
    static let bodySmall: TypographyV1 = .init(
        name: "Body Small",
        size: 12,
        style: .body,
        weight: .neueMontrealRegular,
        lineHeight: 16
    )

    /// size: 15pt, lineHeight: 16pt, weight: medium
    static let body1: TypographyV1 = .init(
        name: "Body 1",
        size: 15,
        style: .body,
        weight: .neueMontrealMedium,
        lineHeight: 16
    )

    /// size: 15pt, lineHeight: 16pt, weight: medium
    static let body1Wide: TypographyV1 = .init(
        name: "Body 1 Wide",
        size: 15,
        style: .body,
        weight: .neueMontrealMedium,
        kerning: 1.04,
        lineHeight: 16
    )

    /// size: 15pt, lineHeight: 24pt, weight: regular
    static let body2: TypographyV1 = .init(
        name: "Body 2",
        size: 15,
        style: .body,
        weight: .neueMontrealRegular,
        kerning: 0.4,
        lineHeight: 24
    )

    /// size: 15pt, lineHeight: 24pt, weight: regular
    static let body2thin: TypographyV1 = .init(
        name: "Body 2 Thin",
        size: 15,
        style: .body,
        weight: .ppNeueMontrealRegular,
        lineHeight: 24
    )

    /// size: 15pt, lineHeight: 24pt, weight: medium
    static let body3: TypographyV1 = .init(
        name: "Body 3",
        size: 15,
        style: .body,
        weight: .neueMontrealMedium,
        kerning: 0.1,
        lineHeight: 24
    )

    /// size: 15pt, lineHeight: 24pt, weight: Book
    static let body3Book: TypographyV1 = .init(
        name: "Body 3 Book",
        size: 15,
        style: .body,
        weight: .ppNeueMontrealBook,
        lineHeight: 24
    )

    /// size: 15pt, lineHeight: 24pt, weight: medium
    static let body3Bold: TypographyV1 = .init(
        name: "Body 3 Bold",
        size: 15,
        style: .body,
        weight: .ppNeueMontrealBold,
        lineHeight: 24
    )

    /// size: 32pt, lineHeight: 33pt, weight: medium
    static let body4: TypographyV1 = .init(
        name: "Body 4",
        size: 32,
        style: .body,
        weight: .neueMontrealMedium,
        kerning: 1.02,
        lineHeight: 33
    )

    /// size: 17pt, lineHeight: 24pt, weight: medium
    static let button1: TypographyV1 = .init(
        name: "Button 1",
        size: 17,
        style: .body,
        weight: .neueMontrealMedium,
        lineHeight: 24
    )

    /// size: 14pt, lineHeight: 16pt, weight: regular
    static let caption: TypographyV1 = .init(
        name: "Caption",
        size: 14,
        style: .caption,
        weight: .neueMontrealRegular,
        kerning: 0.7,
        lineHeight: 16
    )

    /// size: 14pt, lineHeight: 16pt, weight: medium
    static let caption2: TypographyV1 = .init(
        name: "Caption 2",
        size: 14,
        style: .caption,
        weight: .neueMontrealMedium,
        kerning: 0.7,
        lineHeight: 16
    )

    /// size: 12pt, lineHeight: 24pt, weight: medium
    static let caption3: TypographyV1 = .init(
        name: "Caption 3",
        size: 12,
        style: .caption,
        weight: .neueMontrealMedium,
        kerning: 0.4,
        lineHeight: 24
    )

    /// size: 12pt, lineHeight: 12pt, weight: regular
    static let caption4: TypographyV1 = .init(
        name: "Caption 4",
        size: 12,
        style: .caption,
        weight: .neueMontrealRegular,
        lineHeight: 16
    )

    /// size: 12pt, lineHeight: 16pt, weight: medium
    static let caption5: TypographyV1 = .init(
        name: "Caption 5",
        size: 12,
        style: .caption,
        weight: .neueMontrealMedium,
        kerning: 0.4,
        lineHeight: 16
    )

    /// size: 10pt, lineHeight: 16pt, weight: medium
    static let caption6: TypographyV1 = .init(
        name: "Caption 6",
        size: 10,
        style: .caption,
        weight: .neueMontrealMedium
    )

    /// size: 18pt, lineHeight: 24pt, weight: medium
    static let navigationTitle: TypographyV1 = .init(
        name: "Navigation Title",
        size: 18,
        style: .title,
        weight: .neueMontrealMedium,
        lineHeight: 24
    )

    /// size: 12pt, lineHeight: 16pt, weight: medium
    static let monospace: TypographyV1 = .init(
        name: "monospace",
        size: 12,
        style: .caption,
        weight: .inputSans,
        lineHeight: 16
    )

    /// size: 10pt, lineHeight: 14pt, weight: medium
    static let monospaceSmall: TypographyV1 = .init(
        name: "monospace Small",
        size: 10,
        style: .caption,
        weight: .inputSans,
        lineHeight: 14
    )

    /// size: 14pt, lineHeight: 18pt, weight: regular
    static let playerCaption: TypographyV1 = .init(
        name: "OmniPlayer Creation Caption",
        size: 14,
        style: .body,
        weight: .neueMontrealRegular,
        lineHeight: 18
    )

    /// size: 14pt, lineHeight: 18pt, weight: regular, kerning: 0.28
    static let playerCardBody: TypographyV1 = .init(
        name: "OmniPlayer Card Body",
        size: 14,
        style: .body,
        weight: .neueMontrealRegular,
        kerning: 0.28,
        lineHeight: 18
    )

    static let playerCardBodyItalic: TypographyV1 = .init(
        name: "OmniPlayer Card Body Italic",
        size: 14,
        style: .body,
        weight: .ppNeueMontrealItalic,
        kerning: 0.28,
        lineHeight: 18
    )

    /// size: 18pt, lineHeight: 20pt, weight: medium, kerning: 0.72
    static let playlistCards: TypographyV1 = .init(
        name: "Playlist cards",
        size: 18,
        style: .headline,
        weight: .ppNeueMontrealMedium,
        kerning: 0.72,
        lineHeight: 20
    )

    /// size: 20pt, weight: medium
    static let button: TypographyV1 = .init(
        name: "Button",
        size: 14,
        style: .body,
        weight: .ppNeueMontrealMedium,
        lineHeight: 20
    )

    /// size: 16pt, weight: regular
    static let heading4: TypographyV1 = .init(
        name: "Heading H4",
        size: 16,
        style: .body,
        weight: .ppNeueMontrealRegular,
        lineHeight: 20
    )

    /// size: 14pt, lineHeight: 18pt, weight: regular
    static let moreInfoCaption: TypographyV1 = .init(
        name: "More Info Sheet Caption",
        size: 14,
        style: .body,
        weight: .neueMontrealRegular,
        lineHeight: 18
    )
}

public extension View {
    @ViewBuilder
    func typographyV1(_ typography: TypographyV1) -> some View {
        modifier(typography)
    }

    @ViewBuilder
    func typographyV1(_ typography: TypographyV1?) -> some View {
        if let typography = typography {
            self.typographyV1(typography)
        } else {
            self
        }
    }
}

public extension Text {
    /// Applies typography with kerning (Don't support line height)
    func inlineTypographyV1(_ typography: TypographyV1) -> Text {
        return self
            .kerning(typography.kerning)
            .font(typography.font)
    }

    /// Applies typography with kerning and line height support
    func typographyV1(
        _ typography: TypographyV1,
        relativeTo textStyle: Font.TextStyle? = nil,
        isMonospacedDigit: Bool = false
    ) -> some View {
        var custom: Font
        if let textStyle = textStyle {
            custom = .custom(typography.fontName.rawValue, size: typography.size, relativeTo: textStyle)
        } else {
            custom = .custom(typography.fontName.rawValue, size: typography.size)
        }

        if isMonospacedDigit {
            custom = custom.monospacedDigit()
        }

        let defaultLineHeight = typography.uiFont?.lineHeight ?? 0

        return self
            .kerning(typography.kerning)
            .font(custom)
            .modifier(if: typography.lineHeight > defaultLineHeight) { content in
                // Add line height to font - https://stackoverflow.com/a/64652348/7000188
                content
                    .lineSpacing(typography.lineHeight - defaultLineHeight)
                    .padding(.vertical, (typography.lineHeight - defaultLineHeight) / 2)
            }
    }
}

public extension TextEditor {
    /// Applies typography with kerning and line height support
    func typographyV1(
        _ typography: TypographyV1,
        relativeTo textStyle: Font.TextStyle? = nil,
        isMonospacedDigit: Bool = false
    ) -> some View {
        var custom: Font
        if let textStyle = textStyle {
            custom = .custom(typography.fontName.rawValue, size: typography.size, relativeTo: textStyle)
        } else {
            custom = .custom(typography.fontName.rawValue, size: typography.size)
        }

        if isMonospacedDigit {
            custom = custom.monospacedDigit()
        }

        let defaultLineHeight = typography.uiFont?.lineHeight ?? 0

        return self
            .font(custom)
            .modifier(if: typography.lineHeight > defaultLineHeight) { content in
                // Add line height to font - https://stackoverflow.com/a/64652348/7000188
                content
                    .lineSpacing(typography.lineHeight - defaultLineHeight)
                    .padding(.vertical, (typography.lineHeight - defaultLineHeight) / 2)
            }
    }
}

public extension TypographyV1 {
    func editorialNewRegular() -> TypographyV1 {
        weight(.editorialNewRegular)
    }

    func editorialNewLight() -> TypographyV1 {
        weight(.editorialNewLight)
    }

    func neueMontrealRegular() -> TypographyV1 {
        weight(.neueMontrealRegular)
    }

    func neueMontrealMedium() -> TypographyV1 {
        weight(.neueMontrealMedium)
    }

    func ppNeueMontrealBook() -> TypographyV1 {
        weight(.ppNeueMontrealBook)
    }

    func ppNeueMontrealSemiBold() -> TypographyV1 {
        weight(.ppNeueMontrealSemiBold)
    }

    func ppNeueMontrealBold() -> TypographyV1 {
        weight(.ppNeueMontrealBold)
    }

    func ppNeueMontrealItalic() -> TypographyV1 {
        weight(.ppNeueMontrealItalic)
    }

    func inputSans() -> TypographyV1 {
        weight(.inputSans)
    }

    func inputSansRegular() -> TypographyV1 {
        weight(.inputSansRegular)
    }

    func epilogueExtraBold() -> TypographyV1 {
        weight(.epilogueExtraBold)
    }

    internal func weight(_ weight: Weight) -> TypographyV1 {
        var copy = self
        copy.weight = weight
        return copy
    }

    func lineHeight(_ lineHeight: CGFloat) -> TypographyV1 {
        var copy = self
        copy.lineHeight = lineHeight
        return copy
    }

    func size(_ settingSize: (CGFloat) -> CGFloat) -> TypographyV1 {
        var copy = self
        copy.size = settingSize(copy.size)
        return copy
    }

    func kerning(_ kerning: CGFloat) -> TypographyV1 {
        var copy = self
        copy.kerning = kerning
        return copy
    }
}

extension TypographyV1: ViewModifier {
    var fontName: FontResource {
        switch weight {
        case .neueMontrealRegular: return .neueMontrealRegular
        case .neueMontrealMedium: return .neueMontrealMedium
        case .ppNeueMontrealRegular: return .ppNeueMontrealRegular
        case .ppNeueMontrealMedium: return .ppNeueMontrealMedium
        case .ppNeueMontrealBook: return .ppNeueMontrealBook
        case .ppNeueMontrealBookItalic: return .ppNeueMontrealBookItalic
        case .ppNeueMontrealSemiBold: return .ppNeueMontrealSemiBold
        case .ppNeueMontrealSemiBoldItalic: return .ppNeueMontrealSemiBoldItalic
        case .ppNeueMontrealBold: return .ppNeueMontrealBold
        case .ppNeueMontrealItalic: return .ppNeueMontrealItalic
        case .ppNeueMontrealMediumItalic: return .ppNeueMontrealMediumItalic
        case .editorialNewLight: return .editorialNewLight
        case .editorialNewRegular: return .editorialNewRegular
        case .ppEditorialNewRegular: return .ppEditorialNewRegular
        case .ppEditorialNewLight: return .ppEditorialNewLight
        case .ppEditorialNewItalic: return .ppEditorialNewItalic
        case .ppEditorialNewLightItalic: return .ppEditorialNewLightItalic
        case .inputSans: return .inputSans
        case .inputSansRegular: return .inputSansRegular
        case .inputSansLight: return .inputSansLight
        case .inputSansExtraLight: return .inputSansExtraLight
        case .epilogueExtraBold: return .epilogueExtraBold
        }
    }

    public var font: Font {
        loadCustomFonts()
        return Font.custom(fontName.rawValue, size: size, relativeTo: style)
    }

    public var uiFont: UIFont? {
        loadCustomFonts()
        guard let descriptor = UIFont(name: fontName.rawValue, size: size)?.fontDescriptor else {
            return nil
        }

        return UIFont(descriptor: descriptor, size: size)
    }

    public func body(content: Content) -> some View {
        content.font(font)
    }
}

extension TypographyV1: CustomStringConvertible, CustomDebugStringConvertible {
    public var description: String { name }
    public var debugDescription: String {
        "\(name) \(size): \(style) \(weight)"
    }
}

public extension TypographyV1 {
    static func attributedContainer(
        _ typography: TypographyV1,
        _ uiColor: UIColor
    ) -> AttributeContainer {
        // Ensure that custom fonts are loaded and accessible as UIFont
        let font = typography.uiFont ?? UIFont.systemFont(ofSize: typography.size)

        // Define the attributes for the bold and colored text
        var attributes = AttributeContainer()
        attributes.font = UIFontMetrics.default.scaledFont(for: font)
        attributes.foregroundColor = uiColor

        return attributes
    }
}

public extension TypographyV1 {
    enum FontResource: String, Hashable, Codable, CaseIterable {
        case neueMontrealRegular = "NeueMontreal-Regular"
        case neueMontrealMedium = "NeueMontreal-Medium"
        case ppNeueMontrealRegular = "PPNeueMontreal-Regular"
        case ppNeueMontrealMedium = "PPNeueMontreal-Medium"
        case ppNeueMontrealBook = "PPNeueMontreal-Book"
        case ppNeueMontrealBookItalic = "PPNeueMontreal-BookItalic"
        case ppNeueMontrealSemiBold = "PPNeueMontreal-SemiBold"
        case ppNeueMontrealSemiBoldItalic = "PPNeueMontreal-SemiBolditalic"
        case ppNeueMontrealBold = "PPNeueMontreal-Bold"
        case ppNeueMontrealItalic = "PPNeueMontreal-Italic"
        case ppNeueMontrealMediumItalic = "PPNeueMontreal-MediumItalic"
        case editorialNewRegular = "EditorialNew-Regular"
        case editorialNewLight = "EditorialNew-Light"
        case ppEditorialNewRegular = "PPEditorialNew-Regular"
        case ppEditorialNewLight = "PPEditorialNew-Light"
        case ppEditorialNewItalic = "PPEditorialNew-Italic"
        case ppEditorialNewLightItalic = "PPEditorialNew-LightItalic"
        case inputSans = "InputSans-Medium"
        case inputSansRegular = "InputSans-Regular"
        case inputSansLight = "InputSans-Light"
        case inputSansExtraLight = "InputSans-ExtraLight"
        case epilogueExtraBold = "Epilogue-ExtraBold"

        public var fontName: String {
            return rawValue
        }

        public func uiFont(_ size: CGFloat) -> UIFont? {
            loadCustomFonts()
            guard let descriptor = UIFont(name: fontName, size: size)?.fontDescriptor else {
                return nil
            }

            return UIFont(descriptor: descriptor, size: size)
        }
    }

    enum Weight: String, Hashable, Codable, CaseIterable {
        case neueMontrealRegular
        case neueMontrealMedium
        case ppNeueMontrealRegular
        case ppNeueMontrealMedium
        case ppNeueMontrealBook
        case ppNeueMontrealBookItalic
        case ppNeueMontrealSemiBold
        case ppNeueMontrealSemiBoldItalic
        case ppNeueMontrealBold
        case ppNeueMontrealItalic
        case ppNeueMontrealMediumItalic
        case editorialNewRegular
        case editorialNewLight
        case ppEditorialNewRegular
        case ppEditorialNewLight
        case ppEditorialNewItalic
        case ppEditorialNewLightItalic
        case inputSans
        case inputSansRegular
        case inputSansLight
        case inputSansExtraLight
        case epilogueExtraBold
    }

    static let allTypography: [TypographyV1] = [
        .headlineXL,
        .headline1,
        .headline2,
        .headline3,
        .headline4,
        .headline4Wide,
        .headline5,
        .headline6,
        .body1,
        .body1Wide,
        .body2,
        .body2thin,
        .body3,
        .body3Book,
        .body3Bold,
        .body4,
        .button1,
        .caption,
        .caption2,
        .caption3,
        .caption4,
        .caption5,
        .caption6,
        .navigationTitle,
        .monospace,
        .monospaceSmall,
    ]
}

struct TypographyV1_Previews: PreviewProvider {
    static var previews: some View {
        TypographyV1Sample(title: "The quick brown fox jumps over the lazy dog")
    }
}

struct TypographyV1Sample: View {
    let title: String

    init(title: String) {
        self.title = title
    }

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                ForEach(TypographyV1.allTypography, id: \.self) { typography in
                    Text(typography.name)
                        .typographyV1(typography)

                    Text("\(typography.weight.rawValue) \(typography.size.description)")
                        .typographyV1(.caption.weight(typography.weight))

                    Text(title)
                        .typographyV1(typography)
                        .padding()
                        .background(Color.gray.opacity(0.1))
                        .clipShape(RoundedRectangle(cornerRadius: 15))

                    Divider()
                }
            }
            .padding()
        }
    }
}
