//
//  ProfileHeaderViewController.swift
//  Mastodon
//
//  Created by MainasuK Cirno on 2021-3-29.
//

import os.log
import UIKit
import Combine
import PhotosUI
import AlamofireImage
import CropViewController
import MastodonMeta
import MetaTextKit
import MastodonAsset
import MastodonLocalization
import Tabman

protocol ProfileHeaderViewControllerDelegate: AnyObject {
    func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
}

final class ProfileHeaderViewController: UIViewController {

    static let segmentedControlHeight: CGFloat = 50
    static let headerMinHeight: CGFloat = segmentedControlHeight
    
    var disposeBag = Set<AnyCancellable>()
    weak var delegate: ProfileHeaderViewControllerDelegate?
    
    var viewModel: ProfileHeaderViewModel!
    
    let titleView: DoubleTitleLabelNavigationBarTitleView = {
        let titleView = DoubleTitleLabelNavigationBarTitleView()
        titleView.titleLabel.textColor = .white
        titleView.titleLabel.textAttributes[.foregroundColor] = UIColor.white
        titleView.titleLabel.alpha = 0
        titleView.subtitleLabel.textColor = .white
        titleView.subtitleLabel.alpha = 0
        titleView.layer.masksToBounds = true
        return titleView
    }()
    
    let profileHeaderView = ProfileHeaderView()
    
    let buttonBar: TMBar.ButtonBar = {
        let buttonBar = TMBar.ButtonBar()
        buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
        buttonBar.backgroundView.style = .clear
        buttonBar.layout.contentInset = .zero
        return buttonBar
    }()

    func customizeButtonBarAppearance() {
        // The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
        // Needs trigger update when `userInterfaceStyle` chagnes
        let userInterfaceStyle = traitCollection.userInterfaceStyle
        buttonBar.buttons.customize { button in
            switch userInterfaceStyle {
            case .dark:
                // Asset.Colors.Label.primary.color
                button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
                // Asset.Colors.Label.secondary.color
                button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
            default:
                // Asset.Colors.Label.primary.color
                button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
                // Asset.Colors.Label.secondary.color
                button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
            }
            
            button.backgroundColor = .clear
        }
    }

    private var isBannerPinned = false
    private var bottomShadowAlpha: CGFloat = 0.0

    // private var isAdjustBannerImageViewForSafeAreaInset = false
    private var containerSafeAreaInset: UIEdgeInsets = .zero
    
    private(set) lazy var imagePicker: PHPickerViewController = {
        var configuration = PHPickerConfiguration()
        configuration.filter = .images
        configuration.selectionLimit = 1

        let imagePicker = PHPickerViewController(configuration: configuration)
        imagePicker.delegate = self
        return imagePicker
    }()
    private(set) lazy var imagePickerController: UIImagePickerController = {
        let imagePickerController = UIImagePickerController()
        imagePickerController.sourceType = .camera
        imagePickerController.delegate = self
        return imagePickerController
    }()
    
    private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
        let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image])
        documentPickerController.delegate = self
        return documentPickerController
    }()

    deinit {
        os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
    }
    
}

extension ProfileHeaderViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        customizeButtonBarAppearance()

        view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
        ThemeService.shared.currentTheme
            .receive(on: DispatchQueue.main)
            .sink { [weak self] theme in
                guard let self = self else { return }
                self.view.backgroundColor = theme.systemBackgroundColor
            }
            .store(in: &disposeBag)

        profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(profileHeaderView)
        NSLayoutConstraint.activate([
            profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
            profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
        profileHeaderView.preservesSuperviewLayoutMargins = true
    
        Publishers.CombineLatest(
            viewModel.viewDidAppear.eraseToAnyPublisher(),
            viewModel.isTitleViewContentOffsetSet.eraseToAnyPublisher()
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSet in
            guard let self = self else { return }
            self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0
            self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0
        }
        .store(in: &disposeBag)
        
        viewModel.needsSetupBottomShadow
            .receive(on: DispatchQueue.main)
            .sink { [weak self] needsSetupBottomShadow in
                guard let self = self else { return }
                self.setupBottomShadow()
            }
            .store(in: &disposeBag)
        
        Publishers.CombineLatest4(
            viewModel.$isEditing.eraseToAnyPublisher(),
            viewModel.displayProfileInfo.$avatarImageResource.eraseToAnyPublisher(),
            viewModel.editProfileInfo.$avatarImageResource.eraseToAnyPublisher(),
            viewModel.viewDidAppear.eraseToAnyPublisher()
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] isEditing, displayResource, editingResource, _ in
            guard let self = self else { return }
            
            let url = displayResource.url
            let image = editingResource.image
            
            self.profileHeaderView.avatarButton.avatarImageView.configure(
                configuration: AvatarImageView.Configuration(
                    url: isEditing && image != nil ? nil : url,
                    placeholder: image ?? UIImage.placeholder(color: Asset.Theme.Mastodon.systemGroupedBackground.color)
                )
            )
        }
        .store(in: &disposeBag)
        Publishers.CombineLatest4(
            viewModel.$isEditing,
            viewModel.displayProfileInfo.$name.removeDuplicates(),
            viewModel.editProfileInfo.$name.removeDuplicates(),
            viewModel.$emojiMeta
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] isEditing, name, editingName, emojiMeta in
            guard let self = self else { return }
            do {
                let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
                let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
                self.profileHeaderView.nameMetaText.configure(content: metaContent)
            } catch {
                assertionFailure()
            }
            self.profileHeaderView.nameTextField.text = isEditing ? editingName : name
        }
        .store(in: &disposeBag)
        
        let profileNote = Publishers.CombineLatest3(
            viewModel.$isEditing.removeDuplicates(),
            viewModel.displayProfileInfo.$note.removeDuplicates(),
            viewModel.editProfileInfoDidInitialized
        )
        .map { isEditing, displayNote, _ -> String? in
            if isEditing {
                return self.viewModel.editProfileInfo.note
            } else {
                return displayNote
            }
        }
        .eraseToAnyPublisher()

        Publishers.CombineLatest3(
            viewModel.$isEditing.removeDuplicates(),
            profileNote.removeDuplicates(),
            viewModel.$emojiMeta.removeDuplicates()
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] isEditing, note, emojiMeta in
            guard let self = self else { return }
            
            self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
            
            if isEditing {
                let metaContent = PlaintextMetaContent(string: note ?? "")
                self.profileHeaderView.bioMetaText.configure(content: metaContent)
            } else {
                let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
                do {
                    let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
                    self.profileHeaderView.bioMetaText.configure(content: metaContent)
                } catch {
                    assertionFailure()
                    self.profileHeaderView.bioMetaText.reset()
                }
            }
        }
        .store(in: &disposeBag)
        
        profileHeaderView.bioMetaText.delegate = self

        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] notification in
                guard let self = self else { return }
                guard let textField = notification.object as? UITextField else { return }
                self.viewModel.editProfileInfo.name = textField.text
            }
            .store(in: &disposeBag)
        
        profileHeaderView.editAvatarButton.menu = createAvatarContextMenu()
        profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        viewModel.viewDidAppear.value = true

        // set display after view appear
        profileHeaderView.setupAvatarOverlayViews()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
        setupBottomShadow()
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        customizeButtonBarAppearance()
    }
    
}

extension ProfileHeaderViewController {
    private func createAvatarContextMenu() -> UIMenu {
        var children: [UIMenuElement] = []
        let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
            guard let self = self else { return }
            os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function)
            self.present(self.imagePicker, animated: true, completion: nil)
        }
        children.append(photoLibraryAction)
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
                guard let self = self else { return }
                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
                self.present(self.imagePickerController, animated: true, completion: nil)
            })
            children.append(cameraAction)
        }
        let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
            guard let self = self else { return }
            os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
            self.present(self.documentPickerController, animated: true, completion: nil)
        }
        children.append(browseAction)
        
        return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
    }
    
    private func cropImage(image: UIImage, pickerViewController: UIViewController) {
        DispatchQueue.main.async {
            let cropController = CropViewController(croppingStyle: .default, image: image)
            cropController.delegate = self
            cropController.setAspectRatioPreset(.presetSquare, animated: true)
            cropController.aspectRatioPickerButtonHidden = true
            cropController.aspectRatioLockEnabled = true
            pickerViewController.dismiss(animated: true, completion: {
                self.present(cropController, animated: true, completion: nil)
            })
        }
    }
}

extension ProfileHeaderViewController {
    
    func updateHeaderContainerSafeAreaInset(_ inset: UIEdgeInsets) {
        containerSafeAreaInset = inset
    }
    
    func setupBottomShadow() {
        guard viewModel.needsSetupBottomShadow.value else {
            view.layer.shadowColor = nil
            view.layer.shadowRadius = 0
            return
        }
        view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero)
    }
    
    private func updateHeaderBottomShadow(progress: CGFloat) {
        let alpha = min(max(0, 10 * progress - 9), 1)
        if bottomShadowAlpha != alpha {
            bottomShadowAlpha = alpha
            view.setNeedsLayout()
        }
    }
    
    func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
        // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
        updateHeaderBottomShadow(progress: progress)
                
        let bannerImageView = profileHeaderView.bannerImageView
        guard bannerImageView.bounds != .zero else {
            // wait layout finish
            return
        }
        
        let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
        let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
    
        // scroll from bottom to top: 1 -> 2 -> 3
        if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
            // 1
            // banner top pin to window top and expand
            bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
            bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
        } else if bannerContainerBottomOffset < containerSafeAreaInset.top {
            // 3
            // banner bottom pin to navigation bar bottom and
            // the `progress` growth to 1 then segmented control pin to top
            bannerImageView.frame.origin.y = -containerSafeAreaInset.top
            let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
            bannerImageView.frame.size.height = bannerImageHeight
        } else {
            // 2
            // banner move with scrolling from bottom to top until the
            // banner bottom higher than navigation bar bottom
            bannerImageView.frame.origin.y = -containerSafeAreaInset.top
            bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
        }
        
        // set title view offset
        let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
        let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
        let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
        let transformY = max(0, titleViewContentOffset)
        titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
        viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height

        if viewModel.viewDidAppear.value {
            viewModel.isTitleViewContentOffsetSet.value = true
        }
                
        // set avatar fade
        if progress > 0 {
            setProfileAvatar(alpha: 0)
        } else if progress > -abs(throttle) {
            // y = -(1/0.8T)x
            let alpha = -1 / abs(0.8 * throttle) * progress
            setProfileAvatar(alpha: alpha)
        } else {
            setProfileAvatar(alpha: 1)
        }
    }

    private func setProfileAvatar(alpha: CGFloat) {
        profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
        profileHeaderView.avatarButton.alpha = alpha
        profileHeaderView.editAvatarBackgroundView.alpha = alpha
    }
    
}

// MARK: - MetaTextDelegate
extension ProfileHeaderViewController: MetaTextDelegate {
    func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString)
        
        switch metaText {
        case profileHeaderView.bioMetaText:
            guard viewModel.isEditing else { break }
            viewModel.editProfileInfo.note = metaText.backedString
            let metaContent = PlaintextMetaContent(string: metaText.backedString)
            return metaContent
        default:
            assertionFailure()
        }

        return nil
    }
}

// MARK: - PHPickerViewControllerDelegate
extension ProfileHeaderViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        guard let result = results.first else { return }
        ItemProviderLoader.loadImageData(from: result)
            .sink { [weak self] completion in
                guard let _ = self else { return }
                switch completion {
                case .failure:
                    // TODO: handle error
                    break
                case .finished:
                    break
                }
            } receiveValue: { [weak self] file in
                guard let self = self else { return }
                guard let imageData = file?.data else { return }
                guard let image = UIImage(data: imageData) else { return }
                self.cropImage(image: image, pickerViewController: picker)
            }
            .store(in: &disposeBag)
    }
}

// MARK: - UIImagePickerControllerDelegate
extension ProfileHeaderViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)

        guard let image = info[.originalImage] as? UIImage else { return }
        cropImage(image: image, pickerViewController: picker)
    }
        
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
        picker.dismiss(animated: true, completion: nil)
    }
}

// MARK: - UIDocumentPickerDelegate
extension ProfileHeaderViewController: UIDocumentPickerDelegate {
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        guard let url = urls.first else { return }
        
        do {
            guard url.startAccessingSecurityScopedResource() else { return }
            defer { url.stopAccessingSecurityScopedResource() }
            let imageData = try Data(contentsOf: url)
            guard let image = UIImage(data: imageData) else { return }
            cropImage(image: image, pickerViewController: controller)
        } catch {
            os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
        }
    }
}

// MARK: - CropViewControllerDelegate
extension ProfileHeaderViewController: CropViewControllerDelegate {
    public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
        viewModel.editProfileInfo.avatarImage = image
        cropViewController.dismiss(animated: true, completion: nil)
    }
}
