import UIKit

@MainActor
final class CommitsViewController: RefreshingTableViewController {
    private enum Mode: Int { case history, files }

    private let context: AppContext
    private let owner: String
    private let repository: String
    private var page: CommitPage?
    private var contents: [RepositoryContentRow] = []
    private var branch: String?
    private var mode = Mode.history
    private var currentPage: UInt32 = 0

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

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

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
        let modeControl = UISegmentedControl(items: ["History", "Files"])
        modeControl.selectedSegmentIndex = mode.rawValue
        modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
        modeControl.accessibilityLabel = "Repository view"
        navigationItem.titleView = modeControl
        updateBranchMenu()
        loadContent(refreshing: false)
    }

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

    override func loadMoreContent() {
        guard mode == .history else { return }
        loadPage(currentPage + 1, refreshing: false)
    }

    private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
        if requestedPage == 1 {
            resetPagination()
            beginLoading(refreshing: refreshing)
        }
        loadingTask?.cancel()
        loadingTask = Task {
            do {
                switch mode {
                case .history:
                    page = try await context.core.commits(
                        owner: owner,
                        repository: repository,
                        branch: branch,
                        path: "",
                        pages: requestedPage
                    )
                    currentPage = requestedPage
                    finishPagination(hasMore: page?.hasMore ?? false)
                    updateBranchMenu()
                case .files:
                    contents = try await context.core.repositoryContents(
                        owner: owner,
                        repository: repository,
                        path: ""
                    )
                }
                tableView.reloadData()
                updateEmptyView()
            } catch {
                if !Task.isCancelled {
                    show(error: error)
                    failPagination()
                }
            }
            if requestedPage == 1 { endLoading() }
        }
    }

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

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        switch mode {
        case .history:
            let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
            if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
            return cell
        case .files:
            let cell = tableView.dequeueReusableCell(withIdentifier: "repository-content")
                ?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content")
            configureRepositoryContentCell(cell, row: contents[indexPath.row])
            return cell
        }
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        mode == .history ? 86 : UITableView.automaticDimension
    }

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

    @objc private func modeChanged(_ sender: UISegmentedControl) {
        guard let mode = Mode(rawValue: sender.selectedSegmentIndex), mode != self.mode else { return }
        self.mode = mode
        navigationItem.rightBarButtonItem = nil
        if mode == .history { updateBranchMenu() }
        tableView.reloadData()
        loadContent(refreshing: false)
    }

    private func updateEmptyView() {
        switch mode {
        case .history:
            tableView.backgroundView = page?.commits.isEmpty == true
                ? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
                : nil
        case .files:
            tableView.backgroundView = contents.isEmpty
                ? EmptyBackgroundView(title: "No files", detail: "This repository is empty.")
                : nil
        }
    }

    private func updateBranchMenu() {
        let branches = page?.branches ?? []
        let choices: [String?] = [nil] + branches.map(Optional.some)
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: branch ?? "All",
            menu: UIMenu(children: choices.map { choice in
                UIAction(
                    title: choice ?? "All",
                    state: choice == branch ? .on : .off
                ) { [weak self] _ in
                    self?.branch = choice
                    self?.updateBranchMenu()
                    self?.loadContent(refreshing: false)
                }
            })
        )
    }
}

final class CommitCell: UITableViewCell {
    private static let laneOrigin: CGFloat = 12
    private static let laneSpacing: CGFloat = 12

    private var row: CommitRow?
    private var laneCount: UInt32 = 0

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        accessoryType = .disclosureIndicator
        backgroundColor = .systemBackground
    }

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

    func configure(_ row: CommitRow, laneCount: UInt32) {
        self.row = row
        self.laneCount = laneCount
        var content = defaultContentConfiguration()
        content.directionalLayoutMargins.leading = laneCount == 0
            ? 0
            : CGFloat(laneCount) * Self.laneSpacing + 10
        content.text = row.title
        if let branch = row.branchLabel {
            let text = NSMutableAttributedString(string: "\(branch)\n\(row.detail)")
            text.addAttribute(
                .foregroundColor,
                value: UIColor.tintColor,
                range: NSRange(location: 0, length: (branch as NSString).length)
            )
            content.secondaryAttributedText = text
        } else {
            content.secondaryText = row.detail
        }
        content.secondaryTextProperties.numberOfLines = 2
        contentConfiguration = content
        setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let row, laneCount > 0, let context = UIGraphicsGetCurrentContext() else { return }
        let centerY = bounds.midY
        let nodeX = row.nodeLane.map(laneX)
        context.setLineWidth(3)
        context.setLineCap(.round)
        context.setLineJoin(.round)

        for lane in 0..<laneCount {
            if row.topLanes.contains(lane) {
                let start = CGPoint(x: laneX(lane), y: 0)
                if row.topConnections.contains(lane), let nodeX {
                    strokeCurve(
                        context,
                        from: start,
                        to: CGPoint(x: nodeX, y: centerY),
                        color: laneColor(lane)
                    )
                } else {
                    strokeLine(
                        context,
                        from: start,
                        to: CGPoint(x: start.x, y: centerY),
                        color: laneColor(lane)
                    )
                }
            }
            if row.bottomLanes.contains(lane) {
                let end = CGPoint(x: laneX(lane), y: bounds.height)
                if !row.bottomConnections.contains(lane) || row.topLanes.contains(lane) {
                    strokeLine(
                        context,
                        from: CGPoint(x: end.x, y: centerY),
                        to: end,
                        color: laneColor(lane)
                    )
                }
                if row.bottomConnections.contains(lane), let nodeX {
                    strokeCurve(
                        context,
                        from: CGPoint(x: nodeX, y: centerY),
                        to: end,
                        color: laneColor(lane)
                    )
                }
            }
        }
        if let lane = row.nodeLane, let nodeX {
            context.setFillColor(laneColor(lane).cgColor)
            context.fillEllipse(
                in: CGRect(x: nodeX - 5, y: centerY - 5, width: 10, height: 10)
            )
        }
    }

    private func laneX(_ lane: UInt32) -> CGFloat {
        Self.laneOrigin + CGFloat(lane) * Self.laneSpacing
    }

    private func strokeLine(
        _ context: CGContext,
        from start: CGPoint,
        to end: CGPoint,
        color: UIColor
    ) {
        context.setStrokeColor(color.cgColor)
        context.move(to: start)
        context.addLine(to: end)
        context.strokePath()
    }

    private func strokeCurve(
        _ context: CGContext,
        from start: CGPoint,
        to end: CGPoint,
        color: UIColor
    ) {
        let middleY = (start.y + end.y) / 2
        context.setStrokeColor(color.cgColor)
        context.move(to: start)
        context.addCurve(
            to: end,
            control1: CGPoint(x: start.x, y: middleY),
            control2: CGPoint(x: end.x, y: middleY)
        )
        context.strokePath()
    }

    private func laneColor(_ lane: UInt32) -> UIColor {
        UIColor(hue: CGFloat((Double(lane) * 137.508).truncatingRemainder(dividingBy: 360)) / 360,
                saturation: 0.78, brightness: 0.78, alpha: 1)
    }
}
