import UIKit

public enum LNCyclePagerTransformLayoutType {
    case normal
    case linear
    case coverflow
}

public enum LNCyclePagerScrollDirection {
    case horizontal
    case vertical
}

public final class LNCyclePagerViewLayout {
    weak var pageView: UIView?

    public var itemSize: CGSize = .zero
    public var itemSpacing: CGFloat = 0
    public var sectionInset: UIEdgeInsets = .zero

    public var layoutType: LNCyclePagerTransformLayoutType = .normal
    public var scrollDirection: LNCyclePagerScrollDirection = .horizontal

    public var minimumScale: CGFloat = 0.8
    public var minimumAlpha: CGFloat = 1.0
    public var maximumAngle: CGFloat = 0.2

    public var isInfiniteLoop = true
    public var rateOfChange: CGFloat = 0.4
    public var adjustSpacingWhenScrolling = true

    public var itemVerticalCenter = true
    public var itemHorizontalCenter = false

    public init() {}

    public var onlyOneSectionInset: UIEdgeInsets {
        if scrollDirection == .horizontal {
            let leftSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.left
            let rightSpace = (pageView != nil && !isInfiniteLoop && itemHorizontalCenter) ? ((pageView!.bounds.width - itemSize.width) * 0.5) : sectionInset.right
            if itemVerticalCenter, let pageView {
                let vertical = (pageView.bounds.height - itemSize.height) * 0.5
                return UIEdgeInsets(top: vertical, left: leftSpace, bottom: vertical, right: rightSpace)
            }
            return UIEdgeInsets(top: sectionInset.top, left: leftSpace, bottom: sectionInset.bottom, right: rightSpace)
        }

        let topSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.top
        let bottomSpace = (pageView != nil && !isInfiniteLoop) ? ((pageView!.bounds.height - itemSize.height) * 0.5) : sectionInset.bottom
        let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
        return UIEdgeInsets(top: topSpace, left: horizontal, bottom: bottomSpace, right: horizontal)
    }

    public var firstSectionInset: UIEdgeInsets {
        if scrollDirection == .horizontal {
            if itemVerticalCenter, let pageView {
                let vertical = (pageView.bounds.height - itemSize.height) * 0.5
                return UIEdgeInsets(top: vertical, left: sectionInset.left, bottom: vertical, right: itemSpacing)
            }
            return UIEdgeInsets(top: sectionInset.top, left: sectionInset.left, bottom: sectionInset.bottom, right: itemSpacing)
        }

        let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
        return UIEdgeInsets(top: sectionInset.top, left: horizontal, bottom: itemSpacing, right: horizontal)
    }

    public var lastSectionInset: UIEdgeInsets {
        if scrollDirection == .horizontal {
            if itemVerticalCenter, let pageView {
                let vertical = (pageView.bounds.height - itemSize.height) * 0.5
                return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: sectionInset.right)
            }
            return UIEdgeInsets(top: sectionInset.top, left: 0, bottom: sectionInset.bottom, right: sectionInset.right)
        }

        let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
        return UIEdgeInsets(top: 0, left: horizontal, bottom: sectionInset.bottom, right: horizontal)
    }

    public var middleSectionInset: UIEdgeInsets {
        if scrollDirection == .horizontal {
            if itemVerticalCenter, let pageView {
                let vertical = (pageView.bounds.height - itemSize.height) * 0.5
                return UIEdgeInsets(top: vertical, left: 0, bottom: vertical, right: itemSpacing)
            }
            return sectionInset
        }

        let horizontal = pageView.map { ($0.bounds.width - itemSize.width) * 0.5 } ?? sectionInset.left
        return UIEdgeInsets(top: 0, left: horizontal, bottom: itemSpacing, right: horizontal)
    }
}

public protocol LNCyclePagerTransformLayoutDelegate: AnyObject {
    func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, initialize attributes: UICollectionViewLayoutAttributes)
    func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, apply attributes: UICollectionViewLayoutAttributes)
}

public final class LNCyclePagerTransformLayout: UICollectionViewFlowLayout {
    public weak var transformDelegate: LNCyclePagerTransformLayoutDelegate?

    public var layoutConfig: LNCyclePagerViewLayout? {
        didSet {
            guard let layoutConfig else { return }
            layoutConfig.pageView = collectionView
            itemSize = layoutConfig.itemSize
            minimumInteritemSpacing = layoutConfig.itemSpacing
            minimumLineSpacing = layoutConfig.itemSpacing
            scrollDirection = layoutConfig.scrollDirection == .horizontal ? .horizontal : .vertical
            invalidateLayout()
        }
    }

    private enum ItemDirection {
        case leading
        case center
        case trailing
    }

    public override init() {
        super.init()
        scrollDirection = .horizontal
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        scrollDirection = .horizontal
    }

    public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        guard let layoutConfig else {
            return super.shouldInvalidateLayout(forBoundsChange: newBounds)
        }
        return layoutConfig.layoutType == .normal ? super.shouldInvalidateLayout(forBoundsChange: newBounds) : true
    }

    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let attrs = super.layoutAttributesForElements(in: rect), let layoutConfig else {
            return super.layoutAttributesForElements(in: rect)
        }

        if transformDelegate == nil, layoutConfig.layoutType == .normal {
            return attrs
        }

        let copied = attrs.compactMap { $0.copy() as? UICollectionViewLayoutAttributes }
        let visibleRect = CGRect(origin: collectionView?.contentOffset ?? .zero, size: collectionView?.bounds.size ?? .zero)
        for attr in copied where attr.representedElementCategory == .cell {
            guard visibleRect.intersects(attr.frame) else { continue }
            if let transformDelegate {
                transformDelegate.pagerTransformLayout(self, apply: attr)
            } else {
                applyTransform(to: attr, type: layoutConfig.layoutType)
            }
        }
        return copied
    }

    public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let attr = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            return nil
        }

        guard let layoutConfig else { return attr }
        if let transformDelegate {
            transformDelegate.pagerTransformLayout(self, initialize: attr)
        } else if layoutConfig.layoutType != .normal {
            initializeTransform(to: attr, type: layoutConfig.layoutType)
        }
        return attr
    }

    private func itemDirection(for center: CGPoint) -> ItemDirection {
        guard let collectionView else { return .center }
        if scrollDirection == .horizontal {
            let contentCenterX = collectionView.contentOffset.x + collectionView.bounds.width * 0.5
            if abs(center.x - contentCenterX) < 0.5 { return .center }
            return center.x < contentCenterX ? .leading : .trailing
        }

        let contentCenterY = collectionView.contentOffset.y + collectionView.bounds.height * 0.5
        if abs(center.y - contentCenterY) < 0.5 { return .center }
        return center.y < contentCenterY ? .leading : .trailing
    }

    private func initializeTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
        guard let layoutConfig else { return }
        switch type {
        case .linear:
            applyLinear(to: attributes, scale: layoutConfig.minimumScale, alpha: layoutConfig.minimumAlpha)
        case .coverflow:
            applyCoverflow(to: attributes, angle: layoutConfig.maximumAngle, alpha: layoutConfig.minimumAlpha)
        case .normal:
            break
        }
    }

    private func applyTransform(to attributes: UICollectionViewLayoutAttributes, type: LNCyclePagerTransformLayoutType) {
        switch type {
        case .linear:
            applyLinear(to: attributes)
        case .coverflow:
            applyCoverflow(to: attributes)
        case .normal:
            break
        }
    }

    private func applyLinear(to attributes: UICollectionViewLayoutAttributes) {
        guard let layoutConfig, let collectionView else { return }
        if scrollDirection == .horizontal {
            let width = collectionView.bounds.width
            guard width > 0 else { return }
            let centerX = collectionView.contentOffset.x + width * 0.5
            let delta = abs(attributes.center.x - centerX)
            let scale = max(1 - delta / width * layoutConfig.rateOfChange, layoutConfig.minimumScale)
            let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
            applyLinear(to: attributes, scale: scale, alpha: alpha)
            return
        }

        let height = collectionView.bounds.height
        guard height > 0 else { return }
        let centerY = collectionView.contentOffset.y + height * 0.5
        let delta = abs(attributes.center.y - centerY)
        let scale = max(1 - delta / height * layoutConfig.rateOfChange, layoutConfig.minimumScale)
        let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
        applyLinear(to: attributes, scale: scale, alpha: alpha)
    }

    private func applyLinear(to attributes: UICollectionViewLayoutAttributes, scale: CGFloat, alpha: CGFloat) {
        guard let layoutConfig else { return }

        var resolvedScale = scale
        var resolvedAlpha = alpha
        var transform = CGAffineTransform(scaleX: resolvedScale, y: resolvedScale)

        if layoutConfig.adjustSpacingWhenScrolling {
            let direction = itemDirection(for: attributes.center)
            let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
            var translate: CGFloat = 0

            switch direction {
            case .leading:
                translate = 1.15 * length * (1 - resolvedScale) * 0.5
            case .trailing:
                translate = -1.15 * length * (1 - resolvedScale) * 0.5
            case .center:
                resolvedScale = 1
                resolvedAlpha = 1
                transform = CGAffineTransform.identity
            }

            if direction != .center {
                if scrollDirection == .horizontal {
                    transform = transform.translatedBy(x: translate, y: 0)
                } else {
                    transform = transform.translatedBy(x: 0, y: translate)
                }
            }
        }

        attributes.transform = transform
        attributes.alpha = resolvedAlpha
    }

    private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes) {
        guard let layoutConfig, let collectionView else { return }

        if scrollDirection == .horizontal {
            let width = collectionView.bounds.width
            guard width > 0 else { return }
            let centerX = collectionView.contentOffset.x + width * 0.5
            let delta = abs(attributes.center.x - centerX)
            let angle = min(delta / width * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
            let alpha = max(1 - delta / width, layoutConfig.minimumAlpha)
            applyCoverflow(to: attributes, angle: angle, alpha: alpha)
            return
        }

        let height = collectionView.bounds.height
        guard height > 0 else { return }
        let centerY = collectionView.contentOffset.y + height * 0.5
        let delta = abs(attributes.center.y - centerY)
        let angle = min(delta / height * (1 - layoutConfig.rateOfChange), layoutConfig.maximumAngle)
        let alpha = max(1 - delta / height, layoutConfig.minimumAlpha)
        applyCoverflow(to: attributes, angle: angle, alpha: alpha)
    }

    private func applyCoverflow(to attributes: UICollectionViewLayoutAttributes, angle: CGFloat, alpha: CGFloat) {
        guard let layoutConfig else { return }

        var resolvedAngle = angle
        var resolvedAlpha = alpha
        let direction = itemDirection(for: attributes.center)

        var transform3D = CATransform3DIdentity
        transform3D.m34 = -0.002

        var translate: CGFloat = 0
        switch direction {
        case .leading:
            let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
            translate = (1 - cos(resolvedAngle * 1.2 * .pi)) * length
        case .trailing:
            let length = scrollDirection == .horizontal ? attributes.size.width : attributes.size.height
            translate = -(1 - cos(resolvedAngle * 1.2 * .pi)) * length
            resolvedAngle = -resolvedAngle
        case .center:
            resolvedAngle = 0
            resolvedAlpha = 1
        }

        if scrollDirection == .horizontal {
            transform3D = CATransform3DRotate(transform3D, .pi * resolvedAngle, 0, 1, 0)
        } else {
            transform3D = CATransform3DRotate(transform3D, .pi * (-resolvedAngle), 1, 0, 0)
        }

        if layoutConfig.adjustSpacingWhenScrolling {
            if scrollDirection == .horizontal {
                transform3D = CATransform3DTranslate(transform3D, translate, 0, 0)
            } else {
                transform3D = CATransform3DTranslate(transform3D, 0, translate, 0)
            }
        }

        attributes.transform3D = transform3D
        attributes.alpha = resolvedAlpha
    }
}
