//
//  MultiSlider+Internal.swift
//  MultiSlider
//
//  Created by Yonat Sharon on 21/06/2019.
//

import UIKit

extension MultiSlider {
    func setup() {
        trackView.backgroundColor = actualTintColor
        updateTrackViewCornerRounding()
        slideView.layoutMargins = .zero
        setupOrientation()
        setupPanGesture()

        isAccessibilityElement = true
        accessibilityIdentifier = "multi_slider"
        accessibilityLabel = "slider"
        accessibilityTraits = [.allowsDirectInteraction]

        minimumView.isHidden = true
        maximumView.isHidden = true

        if #available(iOS 11.0, *) {
            valueLabelFormatter.addObserverForAllProperties(observer: self)
        }

        selectionFeedbackGenerator = UISelectionFeedbackGenerator()
    }

    private func setupPanGesture() {
        addConstrainedSubview(panGestureView)
        for edge: NSLayoutConstraint.Attribute in [.top, .bottom, .left, .right] {
            constrain(panGestureView, at: edge, diff: -edge.inwardSign * margin)
        }
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didDrag(_:)))
        panGesture.delegate = self
        panGestureView.addGestureRecognizer(panGesture)
    }

    func setupOrientation() {
        trackView.removeFromSuperview()
        trackView.removeConstraints(trackView.constraints)
        slideView.removeFromSuperview()
        minimumView.removeFromSuperview()
        maximumView.removeFromSuperview()
        switch orientation {
        case .vertical:
            let centerAttribute: NSLayoutConstraint.Attribute
            if #available(iOS 12, *) {
                centerAttribute = .centerX // iOS 12 doesn't like .topMargin, .rightMargin
            } else {
                centerAttribute = .centerXWithinMargins
            }
            addConstrainedSubview(trackView, constrain: .top, .bottom, centerAttribute)
            trackView.constrain(.width, to: trackWidth)
            trackView.addConstrainedSubview(slideView, constrain: .left, .right)
            constrainVerticalTrackViewToLayoutMargins()
            addConstrainedSubview(minimumView, constrain: .bottomMargin, centerAttribute)
            addConstrainedSubview(maximumView, constrain: .topMargin, centerAttribute)
        default:
            let centerAttribute: NSLayoutConstraint.Attribute
            if #available(iOS 12, *) {
                centerAttribute = .centerY // iOS 12 doesn't like .leftMargin, .rightMargin
            } else {
                centerAttribute = .centerYWithinMargins
            }
            addConstrainedSubview(trackView, constrain: .left, .right, centerAttribute)
            trackView.constrain(.height, to: trackWidth)
            trackView.addConstrainedSubview(slideView, constrain: .top, .bottom)
            constrainHorizontalTrackViewToLayoutMargins()
            addConstrainedSubview(minimumView, constrain: .leftMargin, centerAttribute)
            addConstrainedSubview(maximumView, constrain: .rightMargin, centerAttribute)
        }
        setupTrackLayoutMargins()
    }

    func setupTrackLayoutMargins() {
        let thumbSize = (thumbImage ?? defaultThumbImage)?.size ?? CGSize(width: 2, height: 2)
        let thumbDiameter = orientation == .vertical ? thumbSize.height : thumbSize.width
        let margin = (centerThumbOnTrackEnd || nil != snapImage)
            ? 0
            : thumbDiameter / 2 - 1 // 1 pixel for semi-transparent boundary
        if orientation == .vertical {
            trackView.layoutMargins = UIEdgeInsets(top: margin, left: 0, bottom: margin, right: 0)
            constrainVerticalTrackViewToLayoutMargins()
            constrain(.width, to: max(thumbSize.width, trackWidth), relation: .greaterThanOrEqual)
        } else {
            trackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin)
            constrainHorizontalTrackViewToLayoutMargins()
            constrain(.height, to: max(thumbSize.height, trackWidth), relation: .greaterThanOrEqual)
        }
    }

    /// workaround to a problem in iOS 12-13, of constraining to `leftMargin` and `rightMargin`.
    func constrainHorizontalTrackViewToLayoutMargins() {
        trackView.constrain(slideView, at: .left, diff: trackView.layoutMargins.left)
        trackView.constrain(slideView, at: .right, diff: -trackView.layoutMargins.right)
    }

    /// workaround to a problem in iOS 12-13, of constraining to `topMargin` and `bottomMargin`.
    func constrainVerticalTrackViewToLayoutMargins() {
        trackView.constrain(slideView, at: .top, diff: trackView.layoutMargins.top)
        trackView.constrain(slideView, at: .bottom, diff: -trackView.layoutMargins.bottom)
    }

    func repositionThumbViews() {
        thumbViews.forEach { $0.removeFromSuperview() }
        thumbViews = []
        valueLabels.forEach { $0.removeFromSuperview() }
        valueLabels = []
        adjustThumbCountToValueCount()
    }

    func adjustThumbCountToValueCount() {
        guard value.count != thumbViews.count else { return }
        thumbViews.removeAllViews()
        valueLabels.removeAllViews()
        for _ in value {
            addThumbView()
        }
        updateOuterTrackViews()
    }

    func updateOuterTrackViews() {
        outerTrackViews.removeAllViews()
        outerTrackViews.removeAll()
        guard nil != outerTrackColor else { return }
        guard let lastThumb = thumbViews.last else { return }
        outerTrackViews = [outerTrackView(constraining: .bottom(in: orientation), to: lastThumb)]
        guard let firstThumb = thumbViews.first, firstThumb != lastThumb else { return }
        outerTrackViews += [outerTrackView(constraining: .top(in: orientation), to: firstThumb)]
    }

    private func outerTrackView(constraining: NSLayoutConstraint.Attribute, to thumbView: UIView) -> UIView {
        let view = UIView()
        view.backgroundColor = outerTrackColor
        trackView.addConstrainedSubview(view, constrain: .top, .bottom, .left, .right)
        trackView.removeFirstConstraint { $0.firstItem === view && $0.firstAttribute == constraining }
        trackView.constrain(view, at: constraining, to: thumbView, at: .center(in: orientation))
        trackView.sendSubviewToBack(view)

        view.layer.cornerRadius = trackView.layer.cornerRadius
        if #available(iOS 11.0, *) {
            view.layer.maskedCorners = .direction(constraining.opposite)
        }

        return view
    }

    func addSnapView(at snapValue: CGFloat) {
        let snapView = UIImageView(image: snapImage)
        snapView.tintColor = actualTintColor
        snapViews.append(snapView)
        slideView.addConstrainedSubview(snapView, constrain: NSLayoutConstraint.Attribute.center(in: orientation).perpendicularCenter)
        slideView.sendSubviewToBack(snapView)
        position(marker: snapView, at: snapValue)
    }

    private func addThumbView() {
        let i = thumbViews.count
        let thumbView = UIImageView(image: thumbImage ?? defaultThumbImage)
        thumbView.applyTint(color: thumbTintColor)
        thumbView.addShadow()
        thumbViews.append(thumbView)
        slideView.addConstrainedSubview(thumbView, constrain: NSLayoutConstraint.Attribute.center(in: orientation).perpendicularCenter)
        positionThumbView(i)
        thumbView.blur(disabledThumbIndices.contains(i))
        addValueLabel(i)
        updateThumbViewShadowVisibility()
    }

    func updateThumbViewShadowVisibility() {
        thumbViews.forEach {
            $0.layer.shadowOpacity = showsThumbImageShadow ? 0.25 : 0
        }
    }

    func addValueLabel(_ i: Int) {
        guard valueLabelPosition != .notAnAttribute else { return }
        let valueLabel = UITextField()
        valueLabel.borderStyle = .none
        slideView.addConstrainedSubview(valueLabel)
        valueLabel.textColor = valueLabelColor ?? valueLabel.textColor
        valueLabel.font = valueLabelFont ?? UIFont.preferredFont(forTextStyle: .footnote)
        if #available(iOS 10.0, *) {
            valueLabel.adjustsFontForContentSizeCategory = true
        }
        let thumbView = thumbViews[i]
        slideView.constrain(valueLabel, at: valueLabelPosition.perpendicularCenter, to: thumbView)
        let position = valueLabelAlternatePosition && (i % 2) == 0
            ? valueLabelPosition.opposite
            : valueLabelPosition
        slideView.constrain(
            valueLabel, at: position.opposite,
            to: thumbView, at: position,
            diff: -position.inwardSign * thumbView.diagonalSize / 4
        )
        valueLabels.append(valueLabel)
        updateValueLabel(i)
    }

    func updateValueLabel(_ i: Int) {
        let labelValue: CGFloat
        if isValueLabelRelative {
            labelValue = i > 0 ? value[i] - value[i - 1] : value[i] - minimumValue
        } else {
            labelValue = value[i]
        }
        valueLabels[i].text = valueLabelText(i, labelValue: labelValue)
    }

    func valueLabelText(_ i: Int, labelValue: CGFloat) -> String? {
        valueLabelTextForThumb?(i, labelValue)
            ?? valueLabelFormatter.string(from: NSNumber(value: Double(labelValue)))
    }

    func updateAllValueLabels() {
        for i in 0 ..< valueLabels.count {
            updateValueLabel(i)
        }
    }

    func updateValueLabelPosition() {
        valueLabels.removeAllViews()
        if valueLabelPosition != .notAnAttribute {
            for i in 0 ..< thumbViews.count {
                addValueLabel(i)
            }
        }
    }

    func updateValueCount(_ count: Int) {
        guard count != value.count else { return }
        isSettingValue = true
        defer { isSettingValue = false }

        if value.count < count {
            let appendCount = count - value.count
            value += snapValues.isEmpty
                ? value.distributedNewValues(count: appendCount, min: minimumValue, max: maximumValue)
                : value.distributedNewValues(count: appendCount, allowedValues: snapValues)
            value.sort()
        }
        if value.count > count { // don't add "else", since prev calc may add too many values in some cases
            value.removeLast(value.count - count)
        }
    }

    func adjustValuesToStepAndLimits() {
        var adjusted = value.sorted()
        for i in 0 ..< adjusted.count {
            adjusted[i] = snap.snap(value: adjusted[i])
        }

        isSettingValue = true
        value = adjusted
        isSettingValue = false

        for i in 0 ..< value.count {
            positionThumbView(i)
        }
    }

    func positionThumbView(_ i: Int) {
        position(marker: thumbViews[i], at: value[i])
    }

    private func position(marker: UIView, at value: CGFloat) {
        guard let containerView = marker.superview else { return }
        containerView.removeFirstConstraint { $0.firstItem === marker && $0.firstAttribute == .center(in: orientation) }
        let minMaxValueDifference = maximumValue - minimumValue
        let relativeDistanceToMax = minMaxValueDifference.isZero ? 0 : (maximumValue - value) / minMaxValueDifference
        if orientation == .horizontal {
            if relativeDistanceToMax < 1 {
                containerView.constrain(marker, at: .centerX, to: containerView, at: .right, ratio: CGFloat(1 - relativeDistanceToMax))
            } else {
                containerView.constrain(marker, at: .centerX, to: containerView, at: .left)
            }
        } else { // vertical orientation
            if relativeDistanceToMax.isNormal {
                containerView.constrain(marker, at: .centerY, to: containerView, at: .bottom, ratio: CGFloat(relativeDistanceToMax))
            } else {
                containerView.constrain(marker, at: .centerY, to: containerView, at: .top)
            }
        }
        UIView.animate(withDuration: 0.1) {
            containerView.updateConstraintsIfNeeded()
        }
    }

    func changePositionConstraint(for subview: UIView?, to constant: CGFloat) {
        guard let constraint = subview?.superview?.constraints.first(where: { $0.firstItem === subview && $0.firstAttribute == .center(in: orientation) }) else { return }
        constraint.constant = constant * constraint.secondAttribute.inwardSign
    }

    func layoutTrackEdge(toView: UIImageView, edge: NSLayoutConstraint.Attribute, superviewEdge: NSLayoutConstraint.Attribute) {
        removeFirstConstraint { $0.firstItem === self.trackView && ($0.firstAttribute == edge || $0.firstAttribute == superviewEdge) }
        if nil != toView.image {
            constrain(trackView, at: edge, to: toView, at: edge.opposite, diff: edge.inwardSign * 8)
        } else {
            constrain(trackView, at: edge, to: self, at: superviewEdge)
        }
    }

    func updateTrackViewCornerRounding() {
        trackView.layer.cornerRadius = hasRoundTrackEnds ? trackWidth / 2 : 1
        outerTrackViews.forEach { $0.layer.cornerRadius = trackView.layer.cornerRadius }
    }
}
