import Foundation
import UIKit
import AsyncDisplayKit
import Display
import AppBundle

let maxInteritemSpacing: CGFloat = 240.0
let sectionInsetTop: CGFloat = 40.0
let sectionInsetBottom: CGFloat = 0.0
let zOffset: CGFloat = -60.0

let perspectiveCorrection: CGFloat = -1.0 / 1000.0
let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2

extension CATransform3D {
    func interpolate(other: CATransform3D, progress: CGFloat) -> CATransform3D {
        var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
        vectors[0]  = self.m11 + (other.m11 - self.m11) * progress
        vectors[1]  = self.m12 + (other.m12 - self.m12) * progress
        vectors[2]  = self.m13 + (other.m13 - self.m13) * progress
        vectors[3]  = self.m14 + (other.m14 - self.m14) * progress
        vectors[4]  = self.m21 + (other.m21 - self.m21) * progress
        vectors[5]  = self.m22 + (other.m22 - self.m22) * progress
        vectors[6]  = self.m23 + (other.m23 - self.m23) * progress
        vectors[7]  = self.m24 + (other.m24 - self.m24) * progress
        vectors[8]  = self.m31 + (other.m31 - self.m31) * progress
        vectors[9]  = self.m32 + (other.m32 - self.m32) * progress
        vectors[10] = self.m33 + (other.m33 - self.m33) * progress
        vectors[11] = self.m34 + (other.m34 - self.m34) * progress
        vectors[12] = self.m41 + (other.m41 - self.m41) * progress
        vectors[13] = self.m42 + (other.m42 - self.m42) * progress
        vectors[14] = self.m43 + (other.m43 - self.m43) * progress
        vectors[15] = self.m44 + (other.m44 - self.m44) * progress
        
        return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
    }
}


private func angle(for origin: CGFloat, itemCount: Int, bounds: CGRect, contentHeight: CGFloat?) -> CGFloat {
    var rotationAngle = rotationAngleAt0(itemCount: itemCount)
    
    var contentOffset = bounds.origin.y
    if contentOffset < 0.0 {
        contentOffset *= 2.0
    }
//    } else if let contentHeight = contentHeight, bounds.maxY > contentHeight {
////        let maxContentOffset = contentHeight - bounds.height
////        let delta = contentOffset - maxContentOffset
////        contentOffset = maxContentOffset + delta / 2.0
//    }
    
    var yOnScreen = origin - contentOffset - sectionInsetTop
    if yOnScreen < 0 {
        yOnScreen = 0
    } else if yOnScreen > bounds.height {
        yOnScreen = bounds.height
    }
    
    let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
    rotationAngle += (maxRotationVariance / bounds.height) * yOnScreen

    return rotationAngle
}

private func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, bounds: CGRect) -> CATransform3D {
    var transform = CATransform3DIdentity
    transform.m34 = perspectiveCorrection
    
    let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, bounds: bounds, contentHeight: contentHeight)
    var effectiveRotationAngle = rotationAngle
    if let additionalAngle = additionalAngle {
        effectiveRotationAngle += additionalAngle
    }
    
    let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
    
    let zTranslation = r * sin(rotationAngle)
    let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
    
    let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
    
    let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
    
    return rotateTransform
}

private func interitemSpacing(itemCount: Int, bounds: CGRect) -> CGFloat {
    var interitemSpacing = maxInteritemSpacing
    if itemCount > 0 {
        interitemSpacing = (bounds.height - sectionInsetTop - sectionInsetBottom) / CGFloat(min(itemCount, 5))
    }
    return interitemSpacing
}

private func frameForIndex(index: Int, size: CGSize, itemCount: Int, bounds: CGRect) -> CGRect {
    let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
    let y = sectionInsetTop + spacing * CGFloat(index)
    let origin = CGPoint(x: 0, y: y)
    
    return CGRect(origin: origin, size: size)
}

private func rotationAngleAt0(itemCount: Int) -> CGFloat {
    let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
    return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
}

private let shadowImage: UIImage? = {
    return generateImage(CGSize(width: 1.0, height: 640.0), rotatedContext: { size, context in
        let bounds = CGRect(origin: CGPoint(), size: size)
        context.clear(bounds)
        
        let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
        
        var locations: [CGFloat] = [0.0, 0.65, 1.0]
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
        context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
    })
}()

class StackItemContainerNode: ASDisplayNode {
    private let node: ASDisplayNode
    private let shadowNode: ASImageNode
    
    var tapped: (() -> Void)?
    var highlighted: ((Bool) -> Void)?
    
    init(node: ASDisplayNode) {
        self.node = node
        self.shadowNode = ASImageNode()
        self.shadowNode.displaysAsynchronously = false
        self.shadowNode.displayWithoutProcessing = true
        self.shadowNode.contentMode = .scaleToFill
        
        super.init()
        
        self.clipsToBounds = true
        self.cornerRadius = 10.0
        applySmoothRoundedCorners(self.layer)
        
        self.shadowNode.image = shadowImage
        
        self.addSubnode(self.node)
        self.addSubnode(self.shadowNode)
    }
    
    override func didLoad() {
        super.didLoad()
        
        let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
        recognizer.tapActionAtPoint = { point in
            return .waitForSingleTap
        }
        recognizer.highlight = { [weak self] point in
            if let point = point, point.x > 280.0 {
                self?.highlighted?(true)
            } else {
                self?.highlighted?(false)
            }
        }
        self.view.addGestureRecognizer(recognizer)
    }
    
    func animateIn() {
        self.shadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
    }
    
    @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
        switch recognizer.state {
        case .ended:
            if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
                switch gesture {
                case .tap:
                    self.tapped?()
                default:
                    break
                }
            }
        default:
            break
        }
    }

    override func layout() {
        super.layout()
        
        self.node.frame = self.bounds
        self.shadowNode.frame = self.bounds
    }
}

public class StackContainerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
    private let scrollNode: ASScrollNode
    private var nodes: [StackItemContainerNode]
    
    private var deleteGestureRecognizer: UIPanGestureRecognizer?
    private var offsetsForDeletingItems: [Int: CGPoint]?
    private var currentDeletingIndexPath: Int?
    private var deletingOffset: CGFloat?
    
    private var animatingIn = false
    
    private var validLayout: CGSize?
    
    override public init() {
        self.scrollNode = ASScrollNode()
        self.nodes = []
        
        super.init()
        
        self.backgroundColor = .black
        
        self.addSubnode(self.scrollNode)
    }
    
    override public func didLoad() {
        super.didLoad()
        
        if #available(iOS 11.0, *) {
            self.scrollNode.view.contentInsetAdjustmentBehavior = .never
        }
        
        self.scrollNode.view.delegate = self
        self.scrollNode.view.alwaysBounceVertical = true
        
        let deleteGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanToDelete(gestureRecognizer:)))
        deleteGestureRecognizer.delegate = self
        deleteGestureRecognizer.delaysTouchesBegan = true
        self.scrollNode.view.addGestureRecognizer(deleteGestureRecognizer)
        self.deleteGestureRecognizer = deleteGestureRecognizer
    }
    
    func item(forYPosition y: CGFloat) -> Int? {
        let itemCount = self.nodes.count
        let bounds = self.scrollNode.bounds
        
        let spacing = interitemSpacing(itemCount: itemCount, bounds: bounds)
        return max(0, min(Int(floor((y - sectionInsetTop) / spacing)), itemCount - 1))
    }
    
    public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
            return false
        }
        
        let touch = panGesture.location(in: gestureRecognizer.view)
        let velocity = panGesture.velocity(in: gestureRecognizer.view)
        
        if abs(velocity.x) > abs(velocity.y), let item = self.item(forYPosition: touch.y) {
            return item > 0
        }
        return false
    }
    
    @objc func didPanToDelete(gestureRecognizer: UIPanGestureRecognizer) {
        let scrollView = self.scrollNode.view
        
        switch gestureRecognizer.state {
            case .began:
                let touch = gestureRecognizer.location(in: scrollView)
                guard let item = self.item(forYPosition: touch.y) else { return }
                
                self.currentDeletingIndexPath = item
            case .changed:
                guard let _ = self.currentDeletingIndexPath else { return }
                
                var delta = gestureRecognizer.translation(in: scrollView)
                delta.y = 0
                
                if let offset = self.deletingOffset {
                    self.deletingOffset = offset + delta.x
                } else {
                    self.deletingOffset = delta.x
                }
                
                gestureRecognizer.setTranslation(.zero, in: scrollView)
            
                self.updateLayout()
            case .ended:
                if let _ = self.currentDeletingIndexPath {
                    if let offset = self.deletingOffset {
                        if offset < -self.frame.width / 2.0 {
                            self.deletingOffset = -self.frame.width
                        } else {
                            self.deletingOffset = nil
                            self.currentDeletingIndexPath = nil
                        }
                    }
                }
            
                UIView.animate(withDuration: 0.3) {
                    self.updateLayout()
                }
            case .cancelled, .failed:
                self.currentDeletingIndexPath = nil
                self.deletingOffset = nil
            default:
                break
        }
      }
    
    func setup() {
        let images: [UIImage] = [UIImage(bundleImageName: "Settings/test1")!, UIImage(bundleImageName: "Settings/test5")!, UIImage(bundleImageName: "Settings/test4")!, UIImage(bundleImageName: "Settings/test3")!, UIImage(bundleImageName: "Settings/test2")!]
        for i in 0 ..< 5 {
            let node = ASImageNode()
            node.image = images[i]
            
            let containerNode = StackItemContainerNode(node: node)
            containerNode.tapped = { [weak self] in
                self?.animateIn(index: i)
            }
            containerNode.highlighted = { [weak self] highlighted in
                self?.highlight(index: i, value: highlighted)
            }
            self.nodes.append(containerNode)
        }
        
        var index: Int = 0
        let bounds = self.scrollNode.view.bounds
        let itemCount = self.nodes.count
        
        for node in self.nodes {
            self.scrollNode.addSubnode(node)
            
            let size = CGSize(width: self.frame.width, height: self.frame.height)
            let frame = frameForIndex(index: index, size: size, itemCount: itemCount, bounds: bounds)
            node.frame = frame
            let transform = final3dTransform(for: frame.minY, size: frame.size, contentHeight: nil, itemCount: itemCount, bounds: bounds)
            node.transform = transform
            index += 1
        }
        
        if let lastFrame = self.nodes.last?.frame {
            self.scrollNode.view.contentSize = CGSize(width: self.frame.width, height: lastFrame.minY)
        }
    }
    
    public func animateIn(index: Int) {
        let node = self.nodes[index]
        
        self.animatingIn = true
        self.scrollNode.view.isUserInteractionEnabled = false
        node.animateIn()
        UIView.animate(withDuration: 0.3) {
            node.transform = CATransform3DIdentity
            node.position = CGPoint(x: self.scrollNode.frame.width / 2.0, y: self.scrollNode.frame.height / 2.0)
        }
        
        for i in 0 ..< index {
            let node = self.nodes[i]
            node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
        }
        
        for i in (index + 1) ..< self.nodes.count {
            let node = self.nodes[i]
            node.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 550.0), duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, mediaTimingFunction: nil, removeOnCompletion: false, additive: true, force: false, completion: nil)
        }
    }
    
    public func highlight(index: Int, value: Bool) {
        let node = self.nodes[index]
        
        let bounds = self.scrollNode.view.bounds
        let contentHeight = self.scrollNode.view.contentSize.height
        let itemCount = self.nodes.count
        
        UIView.animate(withDuration: 0.4) {
            let transform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, additionalAngle: value ? 0.04 : nil, bounds: bounds)
            node.transform = transform
        }
    }
    
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard !self.animatingIn else {
            return
        }
        self.updateLayout()
    }
    
    func updateLayout() {
        let bounds = self.scrollNode.view.bounds
        let contentHeight = self.scrollNode.view.contentSize.height
        let itemCount = self.nodes.count
        
        var index: Int = 0
        for node in self.nodes {
            let initialTransform = final3dTransform(for: node.frame.minY, size: node.frame.size, contentHeight: contentHeight, itemCount: itemCount, bounds: bounds)
            let initialFrame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount, bounds: bounds)
            
            var targetTransform: CATransform3D?
            var targetPosition: CGPoint?
            
            var finalPosition = initialFrame.center
            
            if let deletingIndex = self.currentDeletingIndexPath, let offset = self.deletingOffset {
                if deletingIndex == index {
                    finalPosition = CGPoint(x: self.frame.width / 2.0 + min(offset, 0.0), y: node.position.y)
                } else if index < deletingIndex {
                    let frame = frameForIndex(index: index, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
                    targetPosition = frame.center
                    
                    let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
                    targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
                } else {
                    let frame = frameForIndex(index: index - 1, size: node.frame.size, itemCount: itemCount - 1, bounds: bounds)
                    targetPosition = frame.center
                    
                    let spacing = interitemSpacing(itemCount: itemCount - 1, bounds: bounds)
                    targetTransform = final3dTransform(for: frame.minY, size: node.frame.size, contentHeight: contentHeight - node.frame.height - spacing, itemCount: itemCount - 1, bounds: bounds)
                }
            } else {
                node.position = initialFrame.center
            }
            
            var finalTransform = initialTransform
            if let targetTransform = targetTransform, let offset = self.deletingOffset {
                let progress = min(1.0, abs(offset / (self.frame.width)))
                finalTransform = initialTransform.interpolate(other: targetTransform, progress: progress)
            }
            
            if let targetPosition = targetPosition, let offset = self.deletingOffset {
                let progress = min(1.0, abs(offset / (self.frame.width)))
                finalPosition = CGPoint(x: finalPosition.x + (targetPosition.x - finalPosition.x) * progress, y: finalPosition.y + (targetPosition.y - finalPosition.y) * progress)
            }
            
            node.transform = finalTransform
            node.position = finalPosition
            
            index += 1
        }
    }
    
    public func update(size: CGSize) {
        let hadValidLayout = self.validLayout != nil
        self.validLayout = size
        
        self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
        
        if !hadValidLayout {
            self.setup()
        }
    }
}