import MarkdownUI
import SwiftUI
import UIKit

struct MarkdownContent: View {
    let source: String

    var body: some View {
        Markdown(source)
            .markdownTheme(.gitHub)
            .textSelection(.enabled)
            .frame(maxWidth: .infinity, alignment: .leading)
    }
}

@MainActor
func markdownView(_ source: String) -> UIView {
    UIHostingConfiguration {
        MarkdownContent(source: source)
            .fixedSize(horizontal: false, vertical: true)
    }
    .margins(.all, 0)
    .makeContentView()
}

func localDate(forUTCTimestamp timestamp: Int64) -> Date {
    var utc = Calendar(identifier: .gregorian)
    utc.timeZone = TimeZone(secondsFromGMT: 0)!
    let components = utc.dateComponents(
        [.year, .month, .day],
        from: Date(timeIntervalSince1970: TimeInterval(timestamp))
    )
    return Calendar.current.date(from: components) ?? Date()
}

func utcTimestamp(forLocalDate date: Date) -> Int64 {
    let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
    var utc = Calendar(identifier: .gregorian)
    utc.timeZone = TimeZone(secondsFromGMT: 0)!
    return Int64((utc.date(from: components) ?? date).timeIntervalSince1970)
}

func errorAlert(_ message: String) -> UIAlertController {
    let alert = UIAlertController(title: "Something went wrong", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default))
    return alert
}

extension UIViewController {
    func show(error: Error) {
        present(errorAlert(error.localizedDescription), animated: true)
    }

    func beginNavigationLoading(_ spinner: UIActivityIndicatorView) {
        guard navigationItem.rightBarButtonItem == nil else { return }
        spinner.startAnimating()
        navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
    }

    func endNavigationLoading(_ spinner: UIActivityIndicatorView) {
        spinner.stopAnimating()
        if navigationItem.rightBarButtonItem?.customView === spinner {
            navigationItem.rightBarButtonItem = nil
        }
    }
}

@MainActor
class RefreshingTableViewController: UITableViewController {
    private let spinner = UIActivityIndicatorView(style: .medium)
    var loadingTask: Task<Void, Never>?
    private lazy var moreButton = UIButton(
        configuration: .plain(),
        primaryAction: UIAction { [weak self] _ in self?.requestMoreContent() }
    )
    private var hasMoreContent = false
    private var loadingMore = false

    init() {
        super.init(style: .plain)
        tableView.backgroundColor = .systemGroupedBackground
        tableView.separatorInset = .zero
        tableView.refreshControl = UIRefreshControl()
        tableView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
        navigationItem.largeTitleDisplayMode = .never
    }

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

    deinit { loadingTask?.cancel() }

    func loadContent(refreshing: Bool) {}

    func loadMoreContent() {}

    func beginLoading(refreshing: Bool) {
        if !refreshing { beginNavigationLoading(spinner) }
    }

    func endLoading() {
        endNavigationLoading(spinner)
        refreshControl?.endRefreshing()
    }

    func resetPagination() {
        hasMoreContent = false
        loadingMore = false
        tableView.tableFooterView = nil
    }

    func finishPagination(hasMore: Bool) {
        hasMoreContent = hasMore
        loadingMore = false
        var configuration = moreButton.configuration
        configuration?.title = "Pull up or tap to load more"
        configuration?.showsActivityIndicator = false
        moreButton.configuration = configuration
        moreButton.accessibilityLabel = "Load more results"
        tableView.tableFooterView = hasMore ? paginationFooter() : nil
    }

    func failPagination() {
        loadingMore = false
        finishPagination(hasMore: hasMoreContent)
    }

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard scrollView.isDragging, hasMoreContent, !loadingMore else { return }
        let bottom = max(
            -scrollView.adjustedContentInset.top,
            scrollView.contentSize.height
                + scrollView.adjustedContentInset.bottom
                - scrollView.bounds.height
        )
        if scrollView.contentOffset.y > bottom + 60 { requestMoreContent() }
    }

    private func requestMoreContent() {
        guard hasMoreContent, !loadingMore else { return }
        loadingMore = true
        var configuration = moreButton.configuration
        configuration?.title = "Loading more…"
        configuration?.showsActivityIndicator = true
        moreButton.configuration = configuration
        moreButton.accessibilityLabel = "Loading more results"
        loadMoreContent()
    }

    private func paginationFooter() -> UIView {
        let footer = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 56))
        moreButton.frame = footer.bounds
        moreButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        footer.addSubview(moreButton)
        return footer
    }

    @objc private func refreshRequested() {
        loadContent(refreshing: true)
    }
}

final class EmptyBackgroundView: UIView {
    init(title: String, detail: String) {
        super.init(frame: .zero)
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.font = .preferredFont(forTextStyle: .title2)
        titleLabel.textAlignment = .center
        let detailLabel = UILabel()
        detailLabel.text = detail
        detailLabel.font = .preferredFont(forTextStyle: .body)
        detailLabel.textColor = .secondaryLabel
        detailLabel.textAlignment = .center
        detailLabel.numberOfLines = 0
        let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel])
        stack.axis = .vertical
        stack.spacing = 8
        stack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28),
            stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28),
            stack.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40),
        ])
    }

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

extension UIColor {
    convenience init?(hex: String) {
        let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
        guard value.count == 6, let rgb = Int(value, radix: 16) else { return nil }
        self.init(
            red: CGFloat((rgb >> 16) & 0xff) / 255,
            green: CGFloat((rgb >> 8) & 0xff) / 255,
            blue: CGFloat(rgb & 0xff) / 255,
            alpha: 1
        )
    }
}

func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) {
    var content = cell.defaultContentConfiguration()
    content.text = title
    content.secondaryText = detail
    content.secondaryTextProperties.numberOfLines = 2
    content.image = image
    content.imageProperties.tintColor = .tintColor
    cell.contentConfiguration = content
    cell.backgroundColor = .systemBackground
}

func filterMenuImage(_ symbol: String, active: Bool) -> UIImage? {
    let image = UIImage(systemName: symbol)
    return active
        ? image?.withTintColor(.systemBlue, renderingMode: .alwaysOriginal)
        : image
}

func configureRepositoryContentCell(_ cell: UITableViewCell, row: RepositoryContentRow) {
    let directory = row.kind == .directory
    let detail = directory
        ? "Folder"
        : ByteCountFormatter.string(fromByteCount: row.size, countStyle: .file)
    configureTextCell(
        cell,
        title: row.name,
        detail: detail,
        image: UIImage(systemName: directory ? "folder.fill" : "doc")
    )
    cell.accessoryType = .disclosureIndicator
}

@MainActor
func showRepositoryContent(
    _ row: RepositoryContentRow,
    context: AppContext,
    owner: String,
    repository: String,
    navigationController: UINavigationController?
) {
    let destination: UIViewController = row.kind == .directory
        ? RepositoryDirectoryViewController(
            context: context,
            owner: owner,
            repository: repository,
            path: row.path,
            name: row.name
        )
        : RepositoryFileViewController(
            context: context,
            owner: owner,
            repository: repository,
            path: row.path,
            name: row.name
        )
    navigationController?.pushViewController(destination, animated: true)
}

func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
    let output = NSMutableAttributedString()
    if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
        let attachment = NSTextAttachment(image: image)
        attachment.bounds = CGRect(x: 0, y: -2, width: font.pointSize, height: font.pointSize)
        output.append(NSAttributedString(attachment: attachment))
        output.append(NSAttributedString(string: "  "))
    }
    output.append(NSAttributedString(string: text, attributes: [
        .font: font,
        .foregroundColor: color,
    ]))
    return output
}

func configureIssueStateIcon(
    _ icon: UIImageView,
    state: WorkItemState,
    textStyle: UIFont.TextStyle
) {
    configureOpenClosedStateIcon(icon, state: state, subject: "issue", textStyle: textStyle)
}

func configureOpenClosedStateIcon(
    _ icon: UIImageView,
    state: WorkItemState,
    subject: String,
    textStyle: UIFont.TextStyle
) {
    let symbol: String
    let color: UIColor
    let accessibilityLabel: String
    switch state {
    case .open:
        (symbol, color, accessibilityLabel) = (
            "exclamationmark.circle.fill", .systemGreen, "Open \(subject)"
        )
    case .closed:
        (symbol, color, accessibilityLabel) = (
            "checkmark.circle.fill", .systemPurple, "Closed \(subject)"
        )
    case .unknown:
        (symbol, color, accessibilityLabel) = (
            "questionmark.circle.fill", .systemGray, "Unknown \(subject) state"
        )
    }
    icon.image = UIImage(systemName: symbol)
    icon.tintColor = color
    icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: textStyle)
    icon.setContentHuggingPriority(.required, for: .horizontal)
    icon.setContentCompressionResistancePriority(.required, for: .horizontal)
    icon.isAccessibilityElement = true
    icon.accessibilityLabel = accessibilityLabel
}

func issueLabelView(_ label: LabelRow) -> UILabel {
    let view = UILabel()
    view.text = "  \(label.name)  "
    view.font = .preferredFont(forTextStyle: .caption2)
    view.adjustsFontForContentSizeCategory = true
    view.textColor = label.light ? .black : .white
    view.backgroundColor = UIColor(hex: label.color)
    view.layer.cornerRadius = 9
    view.clipsToBounds = true
    view.accessibilityLabel = "Label \(label.name)"
    return view
}

final class IssueLabelsView: UIView {
    private let labels: [UILabel]
    private var contentHeight: CGFloat
    private let spacing: CGFloat = 6

    init(_ rows: [LabelRow]) {
        labels = rows.map(issueLabelView)
        contentHeight = labels.first?.intrinsicContentSize.height ?? 0
        super.init(frame: .zero)
        labels.forEach(addSubview)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }

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

    override var intrinsicContentSize: CGSize {
        CGSize(width: UIView.noIntrinsicMetric, height: contentHeight)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        guard bounds.width > 0 else { return }
        var origin = CGPoint.zero
        var rowHeight: CGFloat = 0
        for label in labels {
            let size = label.sizeThatFits(
                CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
            )
            if origin.x > 0, origin.x + size.width > bounds.width {
                origin.x = 0
                origin.y += rowHeight + spacing
                rowHeight = 0
            }
            label.frame = CGRect(origin: origin, size: size)
            origin.x += size.width + spacing
            rowHeight = max(rowHeight, size.height)
        }
        let height = origin.y + rowHeight
        if contentHeight != height {
            contentHeight = height
            invalidateIntrinsicContentSize()
        }
    }

    @objc private func contentSizeCategoryDidChange() {
        labels.forEach { $0.font = .preferredFont(forTextStyle: .caption2) }
        contentHeight = labels.first?.intrinsicContentSize.height ?? 0
        invalidateIntrinsicContentSize()
        setNeedsLayout()
    }
}

func separator() -> UIView {
    let line = UIView()
    line.backgroundColor = .separator
    line.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
    return line
}
