//
//  LinearLayoutView.swift
//  Astro
//
//  Created by Jeremy Wiebe on 2015-08-24.
//  Copyright (c) 2015 Mobify Research & Development Inc. All rights reserved.
//

import Foundation

private struct HiddenViewMomento {
    let heightConstraint: NSLayoutConstraint?
    let clipsToBounds: Bool
}

private extension HiddenViewMomento {
    init(from view: UIView) {
        self.heightConstraint = view.heightConstraint
        self.clipsToBounds = view.clipsToBounds
    }

    func apply(to view: UIView) {
        if let originalHeightConstraint = self.heightConstraint {
            view.addConstraint(originalHeightConstraint)
        }
        view.clipsToBounds = self.clipsToBounds
    }
}

class LinearLayoutView: UIView {
    let direction: UIViewLayoutDirection
    private let zeroHeightConstraint: NSLayoutConstraint
    private let linearContainerView = AstroContainerView()
    private var hiddenViewMomentos = [UIView: HiddenViewMomento]()

    @objc var backgroundView: UIView? {
        willSet {
            self.backgroundView?.removeFromSuperview()
        }
        didSet {
            insertBackgroundView()
        }
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init(direction: UIViewLayoutDirection) {
        self.direction = direction
        // Added and removed depending on whether or not the linearContainerView has subviews
        zeroHeightConstraint = NSLayoutConstraint(item: linearContainerView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 0)

        super.init(frame: .zero)

        translatesAutoresizingMaskIntoConstraints = false
        super.addSubview(linearContainerView)
        linearContainerView.pinToSuperviewEdges()
    }

    override class var requiresConstraintBasedLayout: Bool {
        return true
    }

    override func updateConstraints() {
        super.updateConstraints()

        if linearContainerView.subviews.count == 0 {
            NSLayoutConstraint.activate([zeroHeightConstraint])
        }
    }

    override func addSubview(_ view: UIView) {
        NSLayoutConstraint.deactivate([zeroHeightConstraint])

        view.translatesAutoresizingMaskIntoConstraints = false
        linearContainerView.addSubview(view)

        updateSubviewConstraints()
        setNeedsUpdateConstraints()
    }

    @objc func removeAllSubviews() {
        for view in linearContainerView.subviews {
            view.removeFromSuperview()
        }
        setNeedsUpdateConstraints()
    }

    @objc func toggleSubview(_ view: UIView, animated: Bool, animationDuration: TimeInterval) {
        if !hasSubview(view) {
            return
        }

        // Existence of a height constraint with a constant of 0 constitutues "hidden"
        if isSubviewHidden(view) {
            showSubview(view, animated: animated, animationDuration: animationDuration, respond: nil)
        } else {
            hideSubview(view, animated: animated, animationDuration: animationDuration)
        }
    }

    func showSubview(_ view: UIView, animated: Bool, animationDuration: TimeInterval, respond: RPCMethodCallback?) {
        if !hasSubview(view) || !isSubviewHidden(view) {
            respond?(RPCMethodResult.result("Complete"))
            return
        }

        if let positionConstraint = view.findPositioningConstraint(inParent: linearContainerView, direction: direction) {
            let originalIndex = linearContainerView.subviews.firstIndex(of: view)
            linearContainerView.insertSubview(view, at: 0)

            view.removeHeightConstraint()

            hiddenViewMomentos[view]?.apply(to: view)
            hiddenViewMomentos[view] = nil

            // Causes view to be resized to it's required height, which is 
            // needed to properly calculate the starting vertical offset for the animation.
            UIView.performWithoutAnimation({ view.layoutIfNeeded() })

            let originalConstant: CGFloat = positionConstraint.constant
            positionConstraint.constant = -(view.frame.height - originalConstant)

            // For view to jump to it's new (pre-animation) position
            UIView.performWithoutAnimation({ view.layoutIfNeeded() })

            // Now make the change we want to animate
            positionConstraint.constant = originalConstant

            if self.isHidden {
                self.layoutIfNeeded()
                updateConstraintsWhenUpdatingHiddenContainerView()

                if let originalIndex = originalIndex {
                    self.linearContainerView.insertSubview(view, at: originalIndex)
                }
                respond?(RPCMethodResult.result("Complete"))
                return
            }

            UIView.animate(withDuration: animated ? animationDuration : 0,
                animations: { self.layoutIfNeeded() },
                completion: { _ in
                    if let originalIndex = originalIndex {
                        self.linearContainerView.insertSubview(view, at: originalIndex)
                    }
                    respond?(RPCMethodResult.result("Animation complete"))
            })
        }
    }

    @objc func hideSubview(_ view: UIView, animated: Bool, animationDuration: TimeInterval) {
        if !hasSubview(view) || isSubviewHidden(view) {
            return
        }

        // Make sure view is at bottom of stack so it slides behind other views
        let originalIndex = linearContainerView.subviews.firstIndex(of: view)
        linearContainerView.insertSubview(view, at: 0)

        // find the constraint
        if let positionConstraint = view.findPositioningConstraint(inParent: linearContainerView, direction:
            direction) {
            UIView.performWithoutAnimation({ view.layoutIfNeeded() })

            let originalConstant: CGFloat = positionConstraint.constant
            positionConstraint.constant = -view.frame.height - originalConstant

            UIView.animate(withDuration: animated ? animationDuration : 0,
                animations: {
                    self.layoutIfNeeded()
                },
                completion: { _ in

                    // Move the view back to it's original position
                    positionConstraint.constant = originalConstant

                    self.hideSubview(view)

                    if let originalIndex = originalIndex {
                        self.linearContainerView.insertSubview(view, at: originalIndex)
                    }
            })
        }
    }

    @objc func hideSubview(_ view: UIView) {
        if !hasSubview(view) || isSubviewHidden(view) {
            return
        }

        self.hiddenViewMomentos[view] = HiddenViewMomento(from: view)

        // ensure we aren't letting subviews show.
        view.clipsToBounds = true

        // Shrink to 0-height (so that we can leave it in the same position)
        view.removeHeightConstraint()
        view.addConstraint(NSLayoutConstraint(item: view, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: 0))
    }

    private func insertBackgroundView() {
        if let backgroundView = backgroundView {
            backgroundView.translatesAutoresizingMaskIntoConstraints = false
            insertSubview(backgroundView, at: 0)
            backgroundView.pinToSuperviewEdges()
        }
    }

    private func updateSubviewConstraints() {
        var viewKeys = [String]()
        var viewDictionary = [String: UIView]()

        let subviews = linearContainerView.subviews

        for (i, subview) in subviews.enumerated() {
            let viewKey = "v\(i)"

            viewKeys.append(viewKey)
            viewDictionary[viewKey] = subview

            // Removing and adding the subview is an easy way to remove all the superview-related constraints.
            subview.removeFromSuperview()
            linearContainerView.addSubview(subview)

            // Views stretch horizontally for both the .Up and .Down linear layout directions
            subview.pinToSuperviewEdgesHorizontally()
        }

        if direction == .up {
            viewKeys = viewKeys.reversed()
        }

        // build a string of the form "V:|[v1][v2][v3]...|"
        let visualFormatElements = viewKeys.map { "[\($0)]" }
        let visualFormatString = "V:|" +  visualFormatElements.joined(separator: "") + "|"

        linearContainerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: visualFormatString, options: [], metrics: nil, views: viewDictionary))
    }

    private func hasSubview(_ view: UIView) -> Bool {
        return linearContainerView.subviews.contains(view)
    }

    private func isSubviewHidden(_ view: UIView) -> Bool {
        return view.heightConstraint?.constant == 0
    }

    @discardableResult
    private func updateConstraintsWhenUpdatingHiddenContainerView() -> Bool {
        guard let superview = self.superview else {
            return false
        }

        guard let positionConstraint = self.findPositioningConstraint(inParent: superview, direction: self.direction) else {
            return false
        }

        superview.layoutIfNeeded()
        positionConstraint.constant = -self.frame.height
        return true
    }
}

private extension UIView {
    var heightConstraint: NSLayoutConstraint? {
        let constraints = self.constraints
        return constraints.first {
            $0.firstItem as! NSObject == self && $0.firstAttribute == .height
        }
    }

    func removeHeightConstraint() {
        if let heightConstraint = self.heightConstraint {
            self.removeConstraint(heightConstraint)
        }
    }
}
