 import UIKit
 import QuartzCore
 
-public class MarqueeLabel: UILabel {
-    
+@IBDesignable
+open class MarqueeLabel: UILabel, CAAnimationDelegate {
+
     /**
      An enum that defines the types of `MarqueeLabel` scrolling
-     
-     - leftRight: Scrolls left first, then back right to the original position.
-     - rightLeft: Scrolls right first, then back left to the original position.
-     - continuous: continuously scrolls left (with a pause at the original position if animationDelay is set).
-     - continuousReverse: continuously scrolls right (with a pause at the original position if animationDelay is set).
-     */
-    public enum `Type` {
+
+     - Left: Scrolls left after the specified delay, and does not return to the original position.
+     - LeftRight: Scrolls left first, then back right to the original position.
+     - Right: Scrolls right after the specified delay, and does not return to the original position.
+     - RightLeft: Scrolls right first, then back left to the original position.
+     - Continuous: Continuously scrolls left (with a pause at the original position if animationDelay is set).
+     - ContinuousReverse: Continuously scrolls right (with a pause at the original position if animationDelay is set).
+     */
+    public enum MarqueeType {
+        case left
         case leftRight
+        case right
         case rightLeft
         case continuous
         case continuousReverse
     }
-    
+
     //
     // MARK: - Public properties
     //
-    
+
     /**
      Defines the direction and method in which the `MarqueeLabel` instance scrolls.
-     `MarqueeLabel` supports four types of scrolling: `leftRight`, `rightLeft`, `continuous`, and `continuousReverse`.
-     
+     `MarqueeLabel` supports six default types of scrolling: `Left`, `LeftRight`, `Right`, `RightLeft`, `Continuous`, and `ContinuousReverse`.
+
      Given the nature of how text direction works, the options for the `type` property require specific text alignments
      and will set the textAlignment property accordingly.
-     
-     - `leftRight` type is ONLY compatible with a label text alignment of `NSTextAlignmentLeft`.
-     - `rightLeft` type is ONLY compatible with a label text alignment of `NSTextAlignmentRight`.
-     - `continuous` does not require a text alignment (it is effectively centered).
-     - `continuousReverse` does not require a text alignment (it is effectively centered).
-     
-     Defaults to `continuous`.
-     
+
+     - `LeftRight` and `Left` types are ONLY compatible with a label text alignment of `NSTextAlignment.left`.
+     - `RightLeft` and `Right` types are ONLY compatible with a label text alignment of `NSTextAlignment.right`.
+     - `Continuous` and `ContinuousReverse` allow the use of `NSTextAlignment.left`, `.right`, or `.center` alignments,
+     however the text alignment only has an effect when label text is short enough that scrolling is not required.
+     When scrolling, the labels are effectively center-aligned.
+
+     Defaults to `Continuous`.
+
+     - Note: Note that any `leadingBuffer` value will affect the text alignment location relative to the frame position,
+     including  with `.center` alignment, where the center alignment location will be shifted left (for `.continuous`) or
+     right (for `.continuousReverse`) by one-half (1/2) the `.leadingBuffer` amount. Use the `.trailingBuffer` property to
+     add a buffer between text "loops" without affecting alignment location.
+
      - SeeAlso: textAlignment
-     */
-    public var type: Type = .continuous {
+     - SeeAlso: leadingBuffer
+     */
+    open var type: MarqueeType = .continuous {
         didSet {
-            guard type != oldValue else { return }
+            if type == oldValue {
+                return
+            }
             updateAndScroll()
         }
     }
-    
+
+    /**
+     An optional custom scroll "sequence", defined by an array of `ScrollStep` or `FadeStep` instances. A sequence
+     defines a single scroll/animation loop, which will continue to be automatically repeated like the default types.
+
+     A `type` value is still required when using a custom sequence. The `type` value defines the `home` and `away`
+     values used in the `ScrollStep` instances, and the `type` value determines which way the label will scroll.
+
+     When a custom sequence is not supplied, the default sequences are used per the defined `type`.
+
+     `ScrollStep` steps are the primary step types, and define the position of the label at a given time in the sequence.
+     `FadeStep` steps are secondary steps that define the edge fade state (leading, trailing, or both) around the `ScrollStep`
+     steps.
+
+     Defaults to nil.
+
+     - Attention: Use of the `scrollSequence` property requires understanding of how MarqueeLabel works for effective
+     use. As a reference, it is suggested to review the methodology used to build the sequences for the default types.
+
+     - SeeAlso: type
+     - SeeAlso: ScrollStep
+     - SeeAlso: FadeStep
+     */
+    open var scrollSequence: Array<MarqueeStep>?
+
     /**
      Specifies the animation curve used in the scrolling motion of the labels.
      Allowable options:
-     
+
      - `UIViewAnimationOptionCurveEaseInOut`
      - `UIViewAnimationOptionCurveEaseIn`
      - `UIViewAnimationOptionCurveEaseOut`
      - `UIViewAnimationOptionCurveLinear`
-     
+
      Defaults to `UIViewAnimationOptionCurveEaseInOut`.
      */
-    public var animationCurve: UIView.AnimationCurve = .linear
-    
+    open var animationCurve: UIView.AnimationCurve = .linear
+
     /**
      A boolean property that sets whether the `MarqueeLabel` should behave like a normal `UILabel`.
-     
+
      When set to `true` the `MarqueeLabel` will behave and look like a normal `UILabel`, and  will not begin any scrolling animations.
      Changes to this property take effect immediately, removing any in-flight animation as well as any edge fade. Note that `MarqueeLabel`
      will respect the current values of the `lineBreakMode` and `textAlignment`properties while labelized.
-     
+
      To simply prevent automatic scrolling, use the `holdScrolling` property.
-     
+
      Defaults to `false`.
-     
+
      - SeeAlso: holdScrolling
      - SeeAlso: lineBreakMode
-     @warning The label will not automatically scroll when this property is set to `true`.
-     @warning The UILabel default setting for the `lineBreakMode` property is `NSLineBreakByTruncatingTail`, which truncates
+     - Note: The label will not automatically scroll when this property is set to `true`.
+     - Warning: The UILabel default setting for the `lineBreakMode` property is `NSLineBreakByTruncatingTail`, which truncates
      the text adds an ellipsis glyph (...). Set the `lineBreakMode` property to `NSLineBreakByClipping` in order to avoid the
      ellipsis, especially if using an edge transparency fade.
      */
-    @IBInspectable public var labelize: Bool = false {
+    @IBInspectable open var labelize: Bool = false {
         didSet {
-            guard labelize != oldValue else { return }
-            updateAndScroll()
-        }
-    }
-    
+            if labelize != oldValue {
+                updateAndScroll()
+            }
+        }
+    }
+
     /**
      A boolean property that sets whether the `MarqueeLabel` should hold (prevent) automatic label scrolling.
-     
+
      When set to `true`, `MarqueeLabel` will not automatically scroll even its text is larger than the specified frame,
      although the specified edge fades will remain.
-     
+
      To set `MarqueeLabel` to act like a normal UILabel, use the `labelize` property.
-     
+
      Defaults to `false`.
-     
+
+     - Note: The label will not automatically scroll when this property is set to `true`.
      - SeeAlso: labelize
-     @warning The label will not automatically scroll when this property is set to `true`.
-     */
-    @IBInspectable public var holdScrolling: Bool = false {
+     */
+    @IBInspectable open var holdScrolling: Bool = false {
         didSet {
-            guard holdScrolling != oldValue else { return }
-            if oldValue == true && !(awayFromHome || labelize || tapToScroll ) && labelShouldScroll() {
-                beginScroll()
-            }
-        }
-    }
-    
+            if holdScrolling != oldValue {
+                if oldValue == true && !(awayFromHome || labelize ) && labelShouldScroll() {
+                    updateAndScroll()
+                }
+            }
+        }
+    }
+
     /**
      A boolean property that sets whether the `MarqueeLabel` should only begin a scroll when tapped.
-     
+
      If this property is set to `true`, the `MarqueeLabel` will only begin a scroll animation cycle when tapped. The label will
      not automatically being a scroll. This setting overrides the setting of the `holdScrolling` property.
-     
+
      Defaults to `false`.
-     
-     @warning The label will not automatically scroll when this property is set to `false`.
+
+     - Note: The label will not automatically scroll when this property is set to `false`.
      - SeeAlso: holdScrolling
      */
-    @IBInspectable public var tapToScroll: Bool = false {
+    @IBInspectable open var tapToScroll: Bool = false {
         didSet {
-            guard tapToScroll != oldValue else { return }
-            if tapToScroll {
-                let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(MarqueeLabel.labelWasTapped(_:)))
-                self.addGestureRecognizer(tapRecognizer)
-                isUserInteractionEnabled = true
-            } else {
-                if let recognizer = self.gestureRecognizers!.first as UIGestureRecognizer? {
-                    self.removeGestureRecognizer(recognizer)
+            if tapToScroll != oldValue {
+                if tapToScroll {
+                    let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(MarqueeLabel.labelWasTapped(_:)))
+                    self.addGestureRecognizer(tapRecognizer)
+                    isUserInteractionEnabled = true
+                } else {
+                    if let recognizer = self.gestureRecognizers!.first as UIGestureRecognizer? {
+                        self.removeGestureRecognizer(recognizer)
+                    }
+                    isUserInteractionEnabled = false
                 }
-                isUserInteractionEnabled = false
-            }
-        }
-    }
-    
+            }
+        }
+    }
+
     /**
      A read-only boolean property that indicates if the label's scroll animation has been paused.
-     
+
      - SeeAlso: pauseLabel
      - SeeAlso: unpauseLabel
      */
-    public var isPaused: Bool {
-        return sublabel.layer.speed == 0.0
-    }
-    
+    open var isPaused: Bool {
+        return (sublabel.layer.speed == 0.0)
+    }
+
     /**
      A boolean property that indicates if the label is currently away from the home location.
-     
+
      The "home" location is the traditional location of `UILabel` text. This property essentially reflects if a scroll animation is underway.
      */
-    public var awayFromHome: Bool {
-        guard let presentationLayer = sublabel.layer.presentation() else {
-            return false
-        }
-        return !(presentationLayer.position.x == homeLabelFrame.origin.x)
-    }
-    
+    open var awayFromHome: Bool {
+        if let presentationLayer = sublabel.layer.presentation() {
+            return !(presentationLayer.position.x == homeLabelFrame.origin.x)
+        }
+
+        return false
+    }
+
     /**
      The `MarqueeLabel` scrolling speed may be defined by one of two ways:
      - Rate(CGFloat): The speed is defined by a rate of motion, in units of points per second.
      - Duration(CGFloat): The speed is defined by the time to complete a scrolling animation cycle, in units of seconds.
-     
+
      Each case takes an associated `CGFloat` value, which is the rate/duration desired.
      */
     public enum SpeedLimit {
-        
         case rate(CGFloat)
         case duration(CGFloat)
-        
+
         var value: CGFloat {
             switch self {
             case .rate(let rate):

            sublabel.setValue(value, forKeyPath: prop)
        }
    }
    
    //
    // MARK: - MarqueeLabel Heavy Lifting
    //
    
    override public func layoutSubviews() {
        super.layoutSubviews()
        
        updateAndScroll(shouldBeginScroll: true)
    }
    
    override public func willMove(toWindow newWindow: UIWindow?) {
        guard newWindow == nil else { return }
        shutdownLabel()
    }
    
    override public func didMoveToWindow() {
        if self.window == nil {
            shutdownLabel()
        } else {
            updateAndScroll()
        }
    }
    
    private func updateAndScroll() {
        updateAndScroll(shouldBeginScroll: true)
    }
    
    private func updateAndScroll(shouldBeginScroll: Bool) {
        // Check if scrolling can occur
        guard labelReadyForScroll() else { return }
        
        // Calculate expected size
        let expectedLabelSize = sublabelSize()
        
        // Invalidate intrinsic size
        invalidateIntrinsicContentSize()
        
        // Move label to home
        returnLabelToHome()
        
        // Check if label should scroll
        // Note that the holdScrolling propery does not affect this
        if !labelShouldScroll() {
            // Set text alignment and break mode to act like a normal label
            sublabel.textAlignment = super.textAlignment
            sublabel.lineBreakMode = super.lineBreakMode
            
            var labelFrame = CGRect.zero
            
            switch type {
            case .continuousReverse, .rightLeft:
                labelFrame = bounds.divided(atDistance: leadingBuffer, from: .maxXEdge).remainder.integral
            default:
                labelFrame = CGRect(x: leadingBuffer, y: 0.0, width: bounds.size.width - leadingBuffer, height: bounds.size.height).integral
            }
            
            homeLabelFrame = labelFrame
            awayOffset = 0.0
            
            // Remove an additional sublabels (for continuous types)
            repliLayer?.instanceCount = 1;
            
            // Set the sublabel frame to calculated labelFrame
            sublabel.frame = labelFrame
            
            // Remove fade, as by definition none is needed in this case
            removeGradientMask()
            
            return
        }
        
        // Label DOES need to scroll
        
        // Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLength
        let minTrailing = max(max(leadingBuffer, trailingBuffer), fadeLength)
        
        switch type {
        case .continuous, .continuousReverse:
            if (type == .continuous) {
                homeLabelFrame = CGRect(x: leadingBuffer, y: 0.0, width: expectedLabelSize.width, height: bounds.size.height).integral
                awayOffset = -(homeLabelFrame.size.width + minTrailing)
            } else { // .continuousReverse
                homeLabelFrame = CGRect(x: bounds.size.width - (expectedLabelSize.width + leadingBuffer), y: 0.0, width: expectedLabelSize.width, height: bounds.size.height).integral
                awayOffset = (homeLabelFrame.size.width + minTrailing)
            }
            
            // Set frame and text
            sublabel.frame = homeLabelFrame
            
            // Configure replication
            repliLayer?.instanceCount = 2
            repliLayer?.instanceTransform = CATransform3DMakeTranslation(-awayOffset, 0.0, 0.0)
            
        case .rightLeft:
            homeLabelFrame = CGRect(x: bounds.size.width - (expectedLabelSize.width + leadingBuffer), y: 0.0, width: expectedLabelSize.width, height: bounds.size.height).integral
            awayOffset = (expectedLabelSize.width + trailingBuffer + leadingBuffer) - bounds.size.width
            
            // Set frame and text
            sublabel.frame = homeLabelFrame
            
            // Remove any replication
            repliLayer?.instanceCount = 1
            
            // Enforce text alignment for this type
            sublabel.textAlignment = .right
            
        case .leftRight:
            homeLabelFrame = CGRect(x: leadingBuffer, y: 0.0, width: expectedLabelSize.width, height: expectedLabelSize.height).integral
            awayOffset = bounds.size.width - (expectedLabelSize.width + leadingBuffer + trailingBuffer)
            
            // Set frame and text
            sublabel.frame = homeLabelFrame
            
            // Remove any replication
            repliLayer?.instanceCount = 1
            
            // Enforce text alignment for this type
            sublabel.textAlignment = .left
            
            // Default case not required
        }
        
        // Recompute the animation duration
        animationDuration = {
            switch self.speed {
            case .rate(let rate):
                return CGFloat(fabs(self.awayOffset) / rate)
            case .duration(let duration):
                return duration
            }
        }()
        
        // Configure gradient for current condition
        applyGradientMask(fadeLength, animated: !labelize)
        
        if !tapToScroll && !holdScrolling && shouldBeginScroll {
            beginScroll()
        }
    }
    
    func sublabelSize() -> CGSize {
        // Bound the expected size
        let maximumLabelSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        // Calculate the expected size
        var expectedLabelSize = sublabel.sizeThatFits(maximumLabelSize)
        
        #if os(tvOS)
            // Sanitize width to 16384.0 (largest width a UILabel will draw on tvOS)
            expectedLabelSize.width = min(expectedLabelSize.width, 16384.0)
        #else
            // Sanitize width to 5461.0 (largest width a UILabel will draw on an iPhone 6S Plus)
            expectedLabelSize.width = min(expectedLabelSize.width, 5461.0)
        #endif
        
        // Adjust to own height (make text baseline match normal label)
        expectedLabelSize.height = bounds.size.height
        return expectedLabelSize
    }
    
    override public func sizeThatFits(_ size: CGSize) -> CGSize {
        var fitSize = sublabel.sizeThatFits(size)
        fitSize.width += leadingBuffer
        return fitSize
    }
    
    //
    // MARK: - Animation Handling
    //
    
    private func labelShouldScroll() -> Bool {
        // Check for nil string
        if sublabel.text == nil {
            return false
        }
        
        // Check for empty string
        if sublabel.text!.isEmpty {
            return false
        }
        
        // Check if the label string fits
        let labelTooLarge = (sublabelSize().width + leadingBuffer) > self.bounds.size.width
        let animationHasDuration = speed.value > 0.0
        return (!labelize && labelTooLarge && animationHasDuration)
    }
    
    private func labelReadyForScroll() -> Bool {
        // Check if we have a superview
        if superview == nil {
            return false
        }
        
        // Check if we are attached to a window
        if window == nil {
            return false
        }
        
        // Check if our view controller is ready
        let viewController = firstAvailableViewController()
        if viewController != nil {

            CATransaction.commit()
        }
    }

    private func removeGradientMask() {
        self.layer.mask = nil
    }

    private func timingFunctionForAnimationCurve(_ curve: UIView.AnimationCurve) -> CAMediaTimingFunction {
        let timingFunction: CAMediaTimingFunctionName?

        switch curve {
        case .easeIn:
            timingFunction = .easeIn
        case .easeInOut:
            timingFunction = .easeInEaseOut
        case .easeOut:
            timingFunction = .easeOut
        default:
            timingFunction = .linear
        }

        return CAMediaTimingFunction(name: timingFunction!)
    }

    private func transactionDurationType(_ labelType: MarqueeType, interval: CGFloat, delay: CGFloat) -> TimeInterval {
        switch (labelType) {
        case .leftRight, .rightLeft:
            return TimeInterval(2.0 * (delay + interval))
