import Highlighter
import QuickLook
import SwiftUI
import UIKit

@MainActor
final class RepositoryHistoryViewController: RefreshingTableViewController {
    private let context: AppContext
    private let owner: String
    private let repository: String
    private let path: String
    private let emptyDetail: String
    private let loadingIndicator = UIActivityIndicatorView(style: .medium)
    private var page: CommitPage?
    private var currentPage: UInt32 = 0

    init(
        context: AppContext,
        owner: String,
        repository: String,
        path: String,
        emptyDetail: String
    ) {
        self.context = context
        self.owner = owner
        self.repository = repository
        self.path = path
        self.emptyDetail = emptyDetail
        super.init()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
        loadContent(refreshing: false)
    }

    override func loadContent(refreshing: Bool) {
        loadPage(1, refreshing: refreshing)
    }

    override func loadMoreContent() {
        loadPage(currentPage + 1, refreshing: false)
    }

    private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
        if requestedPage == 1 {
            resetPagination()
            if !refreshing {
                loadingIndicator.startAnimating()
                tableView.backgroundView = loadingIndicator
            }
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                let page = try await context.core.commits(
                    owner: owner,
                    repository: repository,
                    branch: nil,
                    path: path,
                    pages: requestedPage
                )
                guard !Task.isCancelled else { return }
                self.page = page
                currentPage = requestedPage
                finishPagination(hasMore: page.hasMore)
                tableView.reloadData()
                updateEmptyView()
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if requestedPage == 1 {
                loadingIndicator.stopAnimating()
                refreshControl?.endRefreshing()
            }
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        page?.commits.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
        if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
        return cell
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        86
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard let commit = page?.commits[indexPath.row] else { return }
        navigationController?.pushViewController(
            FilesViewController(
                context: context,
                owner: owner,
                repository: repository,
                sha: commit.sha,
                branch: commit.branchLabel
            ),
            animated: true
        )
    }

    private func updateEmptyView() {
        tableView.backgroundView = page?.commits.isEmpty == true
            ? EmptyBackgroundView(title: "No commits", detail: emptyDetail)
            : nil
    }
}

final class CodeScrollView: UIScrollView {
    private let textView = UITextView(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)
        translatesAutoresizingMaskIntoConstraints = false
        alwaysBounceVertical = true
        alwaysBounceHorizontal = true
        isDirectionalLockEnabled = true
        showsHorizontalScrollIndicator = true
        contentInsetAdjustmentBehavior = .never
        textView.textContainer.lineBreakMode = .byClipping
        textView.textContainer.widthTracksTextView = false
        textView.isEditable = false
        textView.isScrollEnabled = false
        textView.isSelectable = true
        addSubview(textView)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func display(_ text: NSAttributedString) {
        let text = NSMutableAttributedString(attributedString: text)
        let paragraph = NSMutableParagraphStyle()
        paragraph.lineBreakMode = .byClipping
        text.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: text.length))
        textView.textStorage.setAttributedString(text)
        setNeedsLayout()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        textView.textContainer.size = CGSize(
            width: CGFloat.greatestFiniteMagnitude,
            height: CGFloat.greatestFiniteMagnitude
        )
        textView.layoutManager.ensureLayout(for: textView.textContainer)
        let textSize = textView.layoutManager.usedRect(for: textView.textContainer).size
        let size = CGSize(
            width: max(
                bounds.width,
                ceil(textSize.width + textView.textContainerInset.left + textView.textContainerInset.right)
            ),
            height: max(
                bounds.height,
                ceil(textSize.height + textView.textContainerInset.top + textView.textContainerInset.bottom)
            )
        )
        textView.frame = CGRect(origin: .zero, size: size)
        contentSize = size
    }
}

@MainActor
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
    private enum Mode: String {
        case preview = "Preview"
        case source = "Source"
        case content = "Content"
        case history = "History"
    }

    private let context: AppContext
    private let owner: String
    private let repository: String
    private let path: String
    private let spinner = UIActivityIndicatorView(style: .medium)
    private let modeControl = UISegmentedControl()
    private var loadingTask: Task<Void, Never>?
    private var page: RepositoryFilePage?
    private var previewURL: URL?
    private var fileLanguage: String?
    private var historyController: RepositoryHistoryViewController?
    private var displayedController: UIViewController?
    private var displayedView: UIView?
    private var modes: [Mode] = []

    init(context: AppContext, owner: String, repository: String, path: String, name: String) {
        self.context = context
        self.owner = owner
        self.repository = repository
        self.path = path
        super.init(nibName: nil, bundle: nil)
        title = name
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        beginNavigationLoading(spinner)
        loadingTask = Task {
            do {
                let page = try await context.core.repositoryFile(
                    owner: owner,
                    repository: repository,
                    path: path
                )
                guard !Task.isCancelled else { return }
                self.page = page
                title = page.name
                fileLanguage = page.language.isEmpty ? nil : page.language
                configureModes(for: page)
            } catch {
                if !Task.isCancelled { show(error: error) }
            }
            endNavigationLoading(spinner)
        }
    }

    deinit {
        loadingTask?.cancel()
        if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
    }

    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
        previewURL == nil ? 0 : 1
    }

    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
        previewURL! as NSURL
    }

    private func configureModes(for page: RepositoryFilePage) {
        navigationItem.prompt = title
        modeControl.removeAllSegments()
        modes = page.kind == .markdown
            ? [.preview, .source, .history]
            : [.content, .history]
        for (index, mode) in modes.enumerated() {
            modeControl.insertSegment(withTitle: mode.rawValue, at: index, animated: false)
        }
        modeControl.selectedSegmentIndex = 0
        modeControl.addTarget(self, action: #selector(fileModeChanged(_:)), for: .valueChanged)
        modeControl.accessibilityLabel = "File view"
        navigationItem.titleView = modeControl
        showContent(page, mode: modes[0])
    }

    @objc private func fileModeChanged(_ sender: UISegmentedControl) {
        guard let page, modes.indices.contains(sender.selectedSegmentIndex) else { return }
        showContent(page, mode: modes[sender.selectedSegmentIndex])
    }

    private func showContent(_ page: RepositoryFilePage, mode: Mode) {
        switch mode {
        case .preview: showMarkdownPreview(page.text)
        case .source: showSource(page.text)
        case .content:
            if page.kind == .source {
                showSource(page.text)
            } else {
                do {
                    try showQuickLook(page.data, name: page.name)
                } catch {
                    show(error: error)
                }
            }
        case .history: showHistory()
        }
    }

    private func showHistory() {
        removeDisplayedView()
        let history = historyController ?? RepositoryHistoryViewController(
            context: context,
            owner: owner,
            repository: repository,
            path: path,
            emptyDetail: "No commits affect this file."
        )
        historyController = history
        addChild(history)
        history.view.frame = view.bounds
        history.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        displayedController = history
        displayedView = history.view
        view.addSubview(history.view)
        history.didMove(toParent: self)
    }

    private func showMarkdownPreview(_ source: String) {
        removeDisplayedView()
        let preview = UIHostingController(rootView: RepositoryMarkdownPreview(source: source))
        addChild(preview)
        preview.view.frame = view.bounds
        preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        displayedController = preview
        displayedView = preview.view
        view.addSubview(preview.view)
        preview.didMove(toParent: self)
    }

    private func showSource(_ source: String) {
        removeDisplayedView()
        let highlighter = Highlighter()
        highlighter?.setTheme(
            traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
        )
        highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
        let highlighted = highlighter?.highlight(source, as: fileLanguage)
            ?? NSAttributedString(string: source, attributes: [
                .font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
                .foregroundColor: UIColor.label,
            ])
        let codeView = CodeScrollView()
        codeView.display(highlighted)
        displayedView = codeView
        view.addSubview(codeView)
        NSLayoutConstraint.activate([
            codeView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            codeView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            codeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            codeView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])
    }

    private func removeDisplayedView() {
        displayedController?.willMove(toParent: nil)
        displayedView?.removeFromSuperview()
        displayedController?.removeFromParent()
        displayedController = nil
        displayedView = nil
    }

    private func showQuickLook(_ data: Data, name: String) throws {
        removeDisplayedView()
        if previewURL == nil {
            let url = FileManager.default.temporaryDirectory
                .appendingPathComponent("\(UUID().uuidString)-\(name)")
            try data.write(to: url, options: .atomic)
            previewURL = url
        }
        let preview = QLPreviewController()
        preview.dataSource = self
        addChild(preview)
        preview.view.frame = view.bounds
        preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        displayedController = preview
        displayedView = preview.view
        view.addSubview(preview.view)
        preview.didMove(toParent: self)
    }
}

struct RepositoryMarkdownPreview: View {
    let source: String

    var body: some View {
        ScrollView {
            MarkdownContent(source: source)
                .padding()
        }
        .background(Color(uiColor: .systemBackground))
    }
}
