//
//  Label.swift
//  
//  Copyright © 2016-2019 Apple Inc. All rights reserved.
//

import UIKit
import PlaygroundSupport
import SPCCore

/// A label is a component that displays text. It has properties that let you customize how the text is displayed.
/// For example you can use textStyle, to set the font, size and color of the text.
/// A label has inputs that can be connected to both numeric and text outputs.
///
/// - localizationKey: Label
public class Label: PlaceableComponent {
    public override class var componentType: String { return String(describing: Label.self) }
    
    public override var liveComponent: LiveComponent? {
        return liveLabel
    }
    
    private var liveLabel: LiveLabel?
    
    fileprivate enum EventName: ComponentEventName {
        case textSizeModeChanged
        case multiLineChanged
        case shadowColorChanged
        case shadowOffsetChanged
        case textChanged
        case textStyleChanged
    }
    
    /// Options for how the label resizes itself to fit the text.
    ///
    /// - localizationKey: Label.TextSizeMode
    public enum TextSizeMode: Int {
        /// The label is automatically resized to fit the text — the default behavior.
        ///
        /// - localizationKey: TextSizeMode.auto
        case auto
        /// The label has a fixed width and adjusts its height to fit the text.
        ///
        /// - localizationKey: TextSizeMode.fixedWidth
        case fixedWidth
        /// The label has a fixed height and adjusts its width to fit the text.
        ///
        /// - localizationKey: TextSizeMode.fixedHeight
        case fixedHeight
        /// The label has a fixed size which does not change.
        ///
        /// - localizationKey: TextSizeMode.fixed
        case fixed
    }
    
    // MARK: Properties
    
    /// The way in which the label resizes itself to fit the text.
    ///
    /// - localizationKey: Label.textSizeMode
    public var textSizeMode: TextSizeMode {
        get {
                return _textSizeMode
        }
        set {
            _textSizeMode = newValue
            if Process.isUser {
                updateLiveComponent(.textSizeModeChanged, value: newValue.rawValue.playgroundValue)
            }
        }
    }
    private var _textSizeMode: TextSizeMode = .auto

    /// Indicates whether the label displays more than one line, with `false` (single line) being the default.
    ///
    /// - localizationKey: Label.multiLine
    public var multiLine: Bool {
        get {
            return _multiLine
        }
        set {
            _multiLine = newValue
            if Process.isUser {
                updateLiveComponent(.multiLineChanged, value: newValue.playgroundValue)
            }
        }
    }
    private var _multiLine = false
    
    /// The color of the shadow around the text, with the default being clear.
    ///
    /// - localizationKey: Label.shadowColor
    public var shadowColor: Color {
        get {
            return liveLabel?.shadowColor ?? _shadowColor
        }
        set {
            _shadowColor = newValue
            liveLabel?.shadowColor = newValue
            updateLiveComponent(.shadowColorChanged, value: newValue.playgroundValue)
        }
    }
    private var _shadowColor = UIColor()

    /// The amount that the shadow is offset from the text, with the default being zero.
    ///
    /// - localizationKey: Label.shadowOffset
    public var shadowOffset: Size {
        get {
            guard let offset = liveLabel?.shadowOffset.size else { return _shadowOffset }
            return offset
        }
        set {
            _shadowOffset = newValue
            liveLabel?.shadowOffset = newValue.cgSize
            updateLiveComponent(.shadowOffsetChanged, value: newValue.playgroundValue)
        }
    }
    private var _shadowOffset = Size.zero

    /// The text to be displayed in the label.
    ///
    /// - localizationKey: Label.text
    public var text: String {
        get {
            guard let text = liveLabel?.text else { return _text }
            return text
        }
        set {
            _text = newValue
            liveLabel?.text = newValue
            if Process.isUser {
                updateLiveComponent(.textChanged, value: newValue.playgroundValue)
            }
        }
    }
    private var _text = String()
    
    /// The style of the text. Use this property to set the font, size, color and alignment of the text.
    ///
    /// Example usage:
    /// ````
    /// label.textStyle = TextStyle(.Copperplate, fontSize: 48, color: .orange, alignment: .center)
    /// ````
    /// - localizationKey: Label.textStyle
    public var textStyle: TextStyle {
        get {
            guard let textStyle = liveLabel?.textStyle else { return _textStyle }
            return textStyle
        }
        set {
            _textStyle = newValue
            liveLabel?.textStyle = newValue
            if Process.isUser {
                updateLiveComponent(.textStyleChanged, value: newValue.playgroundValue)
            }
        }
    }
    private var _textStyle = TextStyle(.HelveticaNeue)

    // MARK: SpacePlaceable
    
    /// The default size of the label if its size is not set: 200 in width by 30 in height.
    ///
    /// - localizationKey: Label.intrinsicSize
    public override var intrinsicSize: Size {
        liveLabel?.sizeToFit()
        let size = Size(liveLabel?.intrinsicContentSize ?? CGSize(width: 200, height: 30))
        return size
    }

    // MARK: Inputs

    /// An input that accepts event notifications of type `Int`. When the input receives an event, the label displays the event value as a string.
    ///
    /// You can connect an output of type `Output<Int>` to this input.
    ///
    /// - localizationKey: Label.integerInput
    public private(set) lazy var integerInput = Input<Int>({ [weak self] value in
        self?.text = "\(value)"
    })
    
    /// An input that accepts event notifications of type `Double`. When the input receives an event, the label displays the event value as a string.
    ///
    /// You can connect an output of type `Output<Double>` to this input.
    ///
    /// - localizationKey: Label.doubleInput
    public private(set) lazy var doubleInput = Input<Double>({ [weak self] value in
        self?.text = "\(value)"
    })
    
    /// An input that accepts event notifications of type `String`. When the input receives an event, the label displays the event string value as the text.
    ///
    /// You can connect an output of type `Output<String>` to this input.
    ///
    /// - localizationKey: Label.stringInput
    public private(set) lazy var stringInput = Input<String>({ [weak self] value in
        self?.text = value
    })
    
    // MARK: Initialization
    override func initialize() {
        super.initialize()
    }
    
    override func createLiveComponent() {
        liveLabel = LiveLabel(component: self)
    }
    
    // MARK: Messaging
    private func updateLiveComponent(_ eventName: Label.EventName, value: PlaygroundValue?) {
        guard Process.isUser, let playgroundValue = value else { return }
        let event = ComponentEvent(name: eventName.rawValue, value: playgroundValue)
        event.send(to: self, in: .live)
    }
    
    public override func receive(_ event: ComponentEvent, from origin: ComponentMessageOrigin) {
        super.receive(event, from: origin)
        //PBLog("event: \(event.name) sent from: \(origin) received by: \(name) (\(type(of: self))) in Label")

        if let eventName = Label.EventName(rawValue: event.name) {
            switch eventName {
            case .shadowColorChanged:
                guard let newShadowColor = UIColor.from(event.value) as? UIColor else { return }
                _shadowColor = newShadowColor
                liveLabel?.shadowColor = newShadowColor
            case .shadowOffsetChanged:
                guard let newOffset = Size.from(event.value) as? Size else { return }
                _shadowOffset = newOffset
                liveLabel?.shadowOffset = newOffset.cgSize
            case .textSizeModeChanged:
                guard case .integer(let rawValue) = event.value, let newValue = TextSizeMode(rawValue: rawValue) else { return }
                _textSizeMode = newValue
                liveLabel?.update()
            case .multiLineChanged:
                guard case .boolean(let newValue) = event.value else { return }
                _multiLine = newValue
                liveLabel?.update()
            case .textChanged:
                guard case .string(let newValue) = event.value else { return }
                _text = newValue
                liveLabel?.text = newValue
            case .textStyleChanged:
                guard let newValue = TextStyle.from(event.value) as? TextStyle else { return }
                _textStyle = newValue
                liveLabel?.textStyle = newValue
            }
        }
        
        if let placeableComponentEventName = PlaceableComponentEventName(rawValue: event.name) {
            if placeableComponentEventName == .sizeChanged {
                liveLabel?.update()
            }
        }

    }
}

class LiveLabel: UILabel, LiveComponent {
    weak var component: Component?
    
    override var text: String? {
        didSet {
            update()
        }
    }
    
    var textStyle: TextStyle? {
        get {
            guard
                let fontName = FontName(rawValue: font.familyName),
                let textAlignment = TextAlignment(rawValue: textAlignment.rawValue)
                else { return nil }
            return TextStyle(fontName, fontSize: Double(font.pointSize), color: textColor, alignment: textAlignment)
        }
        set {
            guard let textStyle = newValue else { return }
            font = textStyle.font
            textColor = textStyle.color
            textAlignment = textStyle.alignment
            update()
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    required init(component: Component) {
        self.component = component
        super.init(frame: CGRect.zero)
        clipsToBounds = true
        textAlignment = .center
        setAccessibilityInfo()
    }
    
    func update() {
        numberOfLines = 0
        lineBreakMode = .byTruncatingTail

        guard let labelComponent = component as? Label else { return }
        
        numberOfLines = labelComponent.multiLine ? 0 : 1
        lineBreakMode = labelComponent.multiLine ? .byWordWrapping : .byTruncatingTail

        sizeToFit()
        guard let placeableComponent = component as? PlaceableComponent else { return }
        placeableComponent.size = Size(bounds.size)
    }
    
    override func sizeThatFits(_ size: CGSize) -> CGSize {
        var fitsSize = super.sizeThatFits(size)
        guard let labelComponent = component as? Label else { return fitsSize }
        
        switch labelComponent.textSizeMode {
        case .auto:
            fitsSize = super.sizeThatFits(size)
        case .fixedWidth:
            guard labelComponent.size.width > 0 else { break }
            let checkSize = super.sizeThatFits(CGSize(width: labelComponent.size.cgSize.width, height: CGFloat.greatestFiniteMagnitude))
            fitsSize.width = labelComponent.size.cgSize.width
            fitsSize.height = checkSize.height
        case .fixedHeight:
            guard labelComponent.size.height > 0 else { break }
            let checkSize = super.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: labelComponent.size.cgSize.height))
            fitsSize.width = checkSize.width
            fitsSize.height = labelComponent.size.cgSize.height
        case .fixed:
            fitsSize = labelComponent.size.cgSize
        }
        return fitsSize
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
//        guard let placeableComponent = component as? PlaceableComponent else { return }
    }
}
