//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//

@_exported import CompoundDesignTokens
import SwiftUI

public extension Image {
    /// The icons used by Element as defined in Compound Design Tokens.
    static let compound = CompoundIcons()
}

/// A view that displays an icon from Compound. The icon defaults to a size of 24pt
/// and scales with Dynamic Type, relative to any font given to it by the `font` modifier.
public struct CompoundIcon: View {
    /// The size of the icon.
    public enum Size {
        /// An icon size of 16pt.
        case xSmall
        /// An icon size of 20pt.
        case small
        /// An icon size of 24pt.
        case medium
        /// A custom icon size.
        case custom(CGFloat)
        
        var value: CGFloat {
            switch self {
            case .xSmall: return 16
            case .small: return 20
            case .medium: return 24
            case .custom(let size): return size
            }
        }
    }
    
    private var image: Image
    private var size: Size
    private var font: Font
    
    private var fontSize: FontSize {
        FontSize.reflecting(font) ?? .style(.body)
    }
    
    /// Creates an icon using a key path from the Compound tokens. The size will be
    /// 24pt and will scale relative to the `bodyLG` font when Dynamic Type is used.
    ///
    /// - Parameters:
    ///   - icon: The icon to show.
    public init(_ icon: KeyPath<CompoundIcons, Image>) {
        // Letro: Add a mechanism to override icons
        image = Self.resolve(icon)
        
        size = .medium
        font = .compound.bodyLG
    }
    
    /// Creates an icon using a key path from the Compound tokens.
    ///
    /// - Parameters:
    ///   - icon: The icon to show.
    ///   - size: The size of the icon.
    ///   - font: The font that should be used for scaling with Dynamic Type.
    public init(_ icon: KeyPath<CompoundIcons, Image>, size: Size, relativeTo font: Font) {
        // Letro: Add a mechanism to override icons
        image = Self.resolve(icon)
        
        self.size = size
        self.font = font
    }
    
    /// Creates an icon using a custom image to allow assets from outside
    /// of Compound to scale in the same way as icons. The size will be 24pt
    /// and will scale relative to the `bodyLG` font when Dynamic Type is used.
    ///
    /// - Parameters:
    ///   - customImage: The image that should be displayed
    ///
    /// ** Note:** The image should have a square frame or it may end up distorted.
    public init(customImage: Image) {
        image = customImage
        size = .medium
        font = .compound.bodyLG
    }
    
    /// Creates an icon using a custom image to allow assets from outside
    /// of Compound to scale in the same way as icons.
    ///
    /// - Parameters:
    ///   - customImage: The image that should be displayed
    ///   - size: The size of the icon.
    ///   - font: The font that should be used for scaling with Dynamic Type.
    ///
    /// ** Note:** The image should have a square frame or it may end up distorted.
    public init(customImage: Image, size: Size, relativeTo font: Font) {
        image = customImage
        self.size = size
        self.font = font
    }
    
    public var body: some View {
        image
            .resizable()
            .modifier(CompoundIconFrame(fontSize: size.value, textStyle: fontSize.style))
    }

    // Letro: Add a mechanism to override icons
    private static var overrides = [AnyKeyPath: Image]()
    public static func override(_ keyPath: KeyPath<CompoundIcons, Image>, with image: Image?) {
        overrides[keyPath] = image
    }
    private static func resolve(_ keyPath: KeyPath<CompoundIcons, Image>) -> Image {
        overrides[keyPath] ?? .compound[keyPath: keyPath]
    }
}

/// A simple modifier that applies a square frame of a given size that will be
/// scaled dynamically based upon the specified text style.
private struct CompoundIconFrame: ViewModifier {
    @ScaledMetric private var size: CGFloat
    
    init(fontSize: CGFloat, textStyle: Font.TextStyle) {
        _size = ScaledMetric(wrappedValue: fontSize, relativeTo: textStyle)
    }
    
    func body(content: Content) -> some View {
        content
            .frame(width: size, height: size)
    }
}

public extension Label {
    /// Creates a label with an icon from Compound and a title generated from a string.
    /// The icon size will be 24pt, scaling relative to the `bodyLG` with Dynamic Type.
    /// - Parameters:
    ///   - title: A string used as the label’s title.
    ///   - icon: The icon to use from Compound.
    init(_ title: some StringProtocol, icon: KeyPath<CompoundIcons, Image>) where Title == Text, Icon == CompoundIcon {
        self.init {
            Text(title)
        } icon: {
            CompoundIcon(icon)
        }
    }
    
    /// Creates a label with an icon from Compound and a title generated from a string.
    /// - Parameters:
    ///   - title: A string used as the label’s title.
    ///   - icon: The icon to use from Compound.
    ///   - iconSize: The size of the icon.
    ///   - font: The font that the icon should scale relative to with Dynamic Type.
    init(_ title: some StringProtocol,
         icon: KeyPath<CompoundIcons, Image>,
         iconSize: CompoundIcon.Size,
         relativeTo font: Font) where Title == Text, Icon == CompoundIcon {
        self.init {
            Text(title)
        } icon: {
            CompoundIcon(icon, size: iconSize, relativeTo: font)
        }
    }
}

// MARK: - Previews

struct CompoundIcon_Previews: PreviewProvider, TestablePreview {
    static var previews: some View {
        form
            .previewDisplayName("Form")
        buttons
            .padding(8)
            .previewLayout(.sizeThatFits)
            .previewDisplayName("Buttons")
        accessibilityIcons
            .previewDisplayName("Accessibility Icons Only")
        accessibilityLabels
            .previewDisplayName("Accessibility Labels")
    }
    
    static var accessibilityIcons: some View {
        VStack(spacing: 8) {
            ForEach(DynamicTypeSize.allCases, id: \.self) { size in
                HStack(spacing: 8) {
                    CompoundIcon(\.userProfile, size: .xSmall, relativeTo: .compound.bodyXS)
                    CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodySM)
                    CompoundIcon(\.userProfile, size: .medium, relativeTo: .compound.bodyLG)
                }
                .dynamicTypeSize(size)
            }
        }
    }
    
    static var accessibilityLabels: some View {
        Grid(alignment: .leading) {
            ForEach(DynamicTypeSize.allCases, id: \.self) { size in
                GridRow {
                    Label("Test XS", icon: \.userProfile, iconSize: .xSmall, relativeTo: .compound.bodyXS)
                        .font(.compound.bodyXS)
                    Label("Test Small", icon: \.userProfile, iconSize: .small, relativeTo: .compound.bodySM)
                        .font(.compound.bodySM)
                    Label("Test Medium", icon: \.userProfile, iconSize: .medium, relativeTo: .compound.bodyLG)
                        .font(.compound.bodyLG)
                }
                .lineLimit(1)
                .dynamicTypeSize(size)
            }
        }
    }
    
    static var form: some View {
        Form {
            Section {
                ListRow(label: .action(title: "Plain Icon", icon: \.userProfile),
                        kind: .label)
                ListRow(label: .default(title: "Plain Icon", icon: \.userProfile),
                        kind: .label)
                ListRow(label: .default(title: "Plain Icon", systemIcon: .personCropCircle),
                        kind: .label)
            }
        }
        .compoundList()
        .safeAreaInset(edge: .bottom) {
            Button { } label: {
                Label("Button", icon: \.userProfile)
            }
            .buttonStyle(.compound(.primary))
            .padding()
        }
    }
    
    static var buttons: some View {
        VStack(spacing: 8) {
            Button { } label: {
                Label { Text("Body Large") } icon: {
                    CompoundIcon(\.userProfile, size: .medium, relativeTo: .compound.bodyLG)
                }
            }
            .font(.compound.bodyLG)
            .buttonStyle(.borderedProminent)
            
            Button { } label: {
                Label { Text("Body Small") } icon: {
                    CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodySM)
                }
            }
            .font(.compound.bodySM)
            .buttonStyle(.borderedProminent)
            
            Button { } label: {
                Label { Text("Body xSmall") } icon: {
                    CompoundIcon(\.userProfile, size: .xSmall, relativeTo: .compound.bodyXS)
                }
            }
            .font(.compound.bodyXS)
            .buttonStyle(.borderedProminent)
        }
    }
}
