// The MIT License (MIT) - Copyright (c) 2016 Carlos Vidal
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

/**
    Typealias of a closure with no parameters and `Bool`
    as returning type.
 
    This type of closure is used to evaluate whether an
    `Attribute` should be applied or not.
 */
public typealias Condition = () -> Bool

#if os(iOS)
/**
    Typealias of a closure with a `Context` struct as parameter and `Bool`
    as returning type.

    This type of closure is used to evaluate whether an `Attribute` should
    be applied or not.
 */
public typealias ContextualCondition = (Context) -> Bool
#endif

/**
    This class is the abstraction of `NSLayoutConstraint`
    objects used by **EasyPeasy** to create and update
    `UIView` constraints
 */
open class Attribute {
    
    /// This property aggregates the `NSLayoutRelation`,
    /// the constant and the multiplier of a layout 
    /// constraint
    open internal(set) var constant: Constant
    
    /// Priority level of the constraint
    open internal(set) var priority: Priority
    
    /// `Condition` to evaluate in order to apply
    /// (or not) the constraint. In iOS this 
    /// property may hold a closure of type
    /// `ContextualCondition`.
    open internal(set) var condition: Any?
    
    /// Target `UIView` of the constraint
    open internal(set) weak var createItem: Item?
    
    /// `Attribute` applied to the view
    open var createAttribute: ReferenceAttribute {
        debugPrint("This point shouldn't have been reached")
        return .width
    }
    
    /// Reference `UIView` of the constraint
    open internal(set) weak var referenceItem: AnyObject?
    
    /// Referencce `Attribute` of the constraint
    open internal(set) var referenceAttribute: ReferenceAttribute?
    
    /// Whether the `NSLayoutConstraint` generated by the
    /// `Attribute` is owned by the the `createItem`
    open var ownedByItem: Bool {
        return false
    }
    
    /// Equivalent `NSLayoutConstraint`. It's `nil` unless the method
    /// `createConstraints(for item:_)` is called
    private(set) var layoutConstraint: NSLayoutConstraint?
    
    /// Element identifying the node this attribute will be 
    /// stored in
    lazy var signature: String = {
        // Signature of the create `ReferenceAttribute` of
        // the passed `Attribute`
        var signature = self.createAttribute.signatureString
        
        // Signature of the `Relation` of the passed `Attribute`
        switch self.constant.relation {
        case .equal:
            signature += "eq_"
        case .greaterThanOrEqual:
            signature += "gt_"
        case .lessThanOrEqual:
            signature += "lt_"
        }
        
        // Signature of the `Priority` of the passed
        // `Attribute`
        signature += String(self.priority.layoutPriority())
        
        return signature
    }()
    
    /**
        Initializer which creates an `Attribute` instance
        with `constant = 0.0`, `multiplier = 1.0` and
        `relatedBy = .Equal`
        - returns: the `Attribute` instance created
     */
    public init() {
        self.constant = Constant(value: 0.0, relation: .equal, multiplier: 1.0)
        self.priority = .required
    }
    
    /**
        Initializer which creates an `Attribute` instance
        with `constant = value`, `multiplier = 1.0` and
        `relatedBy = .Equal`
        - parameter value: `constant` of the constraint
        - returns: the `Attribute` instance created
     */
    public init(_ value: CGFloat) {
        self.constant = Constant(value: value, relation: .equal, multiplier: 1.0)
        self.priority = .required
    }
    
    /**
        Initializer which creates an `Attribute` instance
        with the `constant`, `multiplier` and `relatedBy`
        specified by the `Constant` struct
        - parameter constant: `Constant` struct aggregating
        `constant`, `multiplier` and `relatedBy` properties
        - returns: the `Attribute` instance created
     */
    public init(_ constant: Constant) {
        self.constant = constant
        self.priority = .required
    }
    
    // MARK: Public methods
    
    /**
        Sets the `priority` of the constraint
        - parameter priority: `Priority` enum specifying the
        priority of the constraint
        - returns: the `Attribute` instance
     */
    @discardableResult open func with(_ priority: Priority) -> Self {
        self.priority = priority
        return self
    }
    
    /**
        Sets the `when` closure of the `Attribute`
        - parameter closure: `Closure` to be called before
        installing a constraint
        - returns: the `Attribute` instance
     */
    @discardableResult open func when(_ closure: Condition?) -> Self {
        self.condition = closure
        return self
    }
    
    #if os(iOS)
    /**
        Sets the `when` closure of the `Attribute`
        - parameter closure: `Closure` to be called before installing a
        constraint
        - returns: the `Attribute` instance
     */
    @discardableResult open func when(_ closure: ContextualCondition?) -> Self {
        self.condition = closure
        return self
    }
    #endif
    
    // MARK: Internal methods (acting as protected)
    
    /** 
        This method creates the `NSLayoutConstraints` equivalent to the
        current `Attribute`. The resulting constraint is held by the 
        property `layoutConstraint`
        - parameter view: `UIView` in which the generated
        `NSLayoutConstraint` will be added
        - returns an `Array` of `NSLayoutConstraint` objects that will
        be installed on the `UIView` passed as parameter
     */
    @discardableResult func createConstraints(for item: Item) -> [NSLayoutConstraint] {
        guard self.ownedByItem || item.owningView != nil else {
            debugPrint("EasyPeasy Attribute cannot be applied to item \(item) as its superview/owningView is nil")
            return []
        }
        
        // Reference to the target view
        self.createItem = item
        
        // Build layout constraint
        let constantFactor: CGFloat = self.createAttribute.shouldInvertConstant ? -1 : 1
        let layoutConstraint = NSLayoutConstraint(
            item: item,
            attribute: self.createAttribute.layoutAttribute,
            relatedBy: self.constant.relation,
            toItem: self.referenceItem,
            attribute: self.referenceAttributeHelper().layoutAttribute,
            multiplier: self.constant.multiplier,
            constant: (self.constant.value * constantFactor)
        )
        
        // Set priority
        layoutConstraint.priority = self.priority.layoutPriority()
        
        // Reference resulting constraint
        self.layoutConstraint = layoutConstraint
        
        // Return the constraint
        return [layoutConstraint]
    }
    
    /**
        Determines whether the `Attribute` must be installed or not
        depending on the `Condition` closure
        - return boolean determining if the `Attribute` has to be
        applied
     */
    func shouldInstall() -> Bool {
        // If there is a ContextualCondition then create the context
        // struct and call the closure
        #if os(iOS)
        let item = self.createItem?.owningView ?? self.createItem
        if let contextualCondition = self.condition as? ContextualCondition, let view = item as? View {
            return contextualCondition(Context(with: view.traitCollection))
        }
        #endif
        
        // Evaluate condition result
        if let condition = self.condition as? Condition {
            return condition()
        }
        
        // Otherwise default to true
        return true
    }

    /**
        Determines which `ReferenceAttribute` must be taken as reference
        attribute for the actual Attribute class. Usually is the opposite
        of the one that is going to be installed
        - returns `ReferenceAttribute` to install
     */
    func referenceAttributeHelper() -> ReferenceAttribute {
        // If already set return
        if let attribute = self.referenceAttribute {
            return attribute
        }
        
        // If reference view is the superview then return same attribute
        // as `createAttribute`
        if let referenceItem = self.referenceItem, referenceItem === self.createItem?.owningView {
            return self.createAttribute
        }
        
        // Otherwise return the opposite of `createAttribute`
        return self.createAttribute.opposite
    }
    
}

/**
    Methods applicable to an `Array` of `Attributes`. The `Constants`
    will apply to each one of the `Attributes` within the `Array`
    overriding the values individually set.
 */
public extension Array where Element: Attribute {
   
    // MARK: Public methods
    
    /**
        Sets the `priority` of each constraint within the current `Array`
        of `Attributes`. If the priority was already set this method
        overrides it
        - parameter priority: `Priority` enum specifying the priority of 
        the constraint
        - returns: the `Array` of `Attributes`
     */
    @discardableResult public func with(_ priority: Priority) -> [Attribute] {
        for attribute in self {
            attribute.with(priority)
        }
        return self
    }
    
    /**
        Sets the `when` closure of each one of `Attributes` within the
        current `Array`. If the condition was already set this method
        overrides it
        - parameter closure: `Closure` to be called before installing
        each constraint
        - returns: the `Array` of `Attributes`
     */
    @discardableResult public func when(_ closure: Condition?) -> [Attribute] {
        for attribute in self {
            attribute.when(closure)
        }
        return self
    }
    
    #if os(iOS)
    /**
        Sets the `when` closure of each one of `Attributes` within the current
        `Array`. If the condition was already set this method overrides it
        - parameter closure: `Closure` to be called before installing each
        constraint
        - returns: the `Array` of `Attributes`
     */
    @discardableResult public func when(_ closure: ContextualCondition?) -> [Attribute] {
        for attribute in self {
            attribute.when(closure)
        }
        return self
    }
    #endif
    
}
