;;; my-github.el -- Github client -*- lexical-binding: t -*-

;; Copyright (C) 2023 Free Software Foundation.

;; Author: Yuchen Pei <id@ypei.org>
;; Package-Requires: ((emacs "28.2"))

;; This file is part of dotfiles.

;; dotfiles is free software: you can redistribute it and/or modify it under
;; the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; dotfiles is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
;; Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with dotfiles.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Github client.

;;; Code:


(defun my-grok-github (url)
  "get github info of a project.
url is the url of the project
License; name; description; homepage; created at"
  (when (string-match "github.com\\(/[^/]+/[^/]+\\)/?.*$" url)
    (with-current-buffer (url-retrieve-synchronously
                          (concat "https://api.github.com/repos"
                                  (replace-regexp-in-string
                                   "\\.git$" "" (match-string 1 url))))
      (set-buffer-multibyte t)
      (my-delete-http-header)
      (my-grok-github-make-info (json-read)))))

(defun my-grok-github-make-info (raw)
  (list (cons "Title" (alist-get 'name raw))
        (cons "Description" (alist-get 'description raw))
        (cons "Source" (alist-get 'html_url raw))
        (cons "Website" (alist-get 'homepage raw))
        (cons "Released" (substring (alist-get 'created_at raw) 0 10))
        (cons "Pushed" (substring (alist-get 'pushed_at raw) 0 10))
        (cons "Subject" (string-join (alist-get 'topics raw) ", "))
        ;; FIXME: why did we comment this out?
        ;; (cons "License" (my-post-process-licensing-name
        ;;                  (alist-get 'spdx_id (alist-get 'license raw))))
        (cons "Developers" (my-grok-github-get-developer-name
                            (alist-get 'url (alist-get 'owner raw))))))

(defun my-github-api-repos (url)
  (when-let* ((urlobj (url-generic-parse-url url))
              (path (url-filename urlobj))
              (project-id
               (when (string-match "^/[^/]+/[^/]+" path)
                 (match-string 0 path))))
    (my-url-fetch-json
     (format "https://api.github.com/repos%s" project-id))))

(defun my-github-api-readme (url)
  (when-let* ((urlobj (url-generic-parse-url url))
              (path (url-filename urlobj))
              (project-id
               (when (string-match "^/[^/]+/[^/]+" path)
                 (match-string 0 path)))
              ;; so that the response of readme is in html format
              (url-request-extra-headers
               '(("Accept" . "application/vnd.github.html"))))
    (my-url-fetch-raw
     (format "https://api.github.com/repos%s/readme" project-id))))

(defun my-github-project-infobox (url)
  (interactive "sGithub repo url: ")
  (let ((info
         (append
          (my-github-api-repos url)
          `((readme . ,(my-github-api-readme url))))))
    (infobox-render
     (infobox-translate
      info my-github-project-info-specs)
     `(my-github-project-infobox ,url)
     (called-interactively-p 'interactive)))
  )

(defvar my-github-project-info-specs
  `((html_url . "Clone")
    (full_name . "Name")
    (description . "Description")
    (created_at . ("Created at" . my-gitlab-format-time-string))
    (pushed_at . ("Pushed at" . my-gitlab-format-time-string))
    (topics . ("Topics" . ,(lambda (xs)
                             (mapconcat #'identity xs "; "))))
    (stargazers_count . ("Stars" . number-to-string))
    (forks_count . ("Forks" . number-to-string))
    (readme . (body . ,(lambda (text)
                         (with-temp-buffer
                           (insert text)
                           (shr-render-region (point-min) (point-max))
                           (buffer-string)))))))

(defun my-grok-github-get-developer-name (url)
  (with-current-buffer (url-retrieve-synchronously url)
    (set-buffer-multibyte t)
    (my-delete-http-header)
    (alist-get 'name (json-read))))

;;; urls with github
;; TODO: generalise the following to common forges, including
;; cgit, gitlab etc.
(defun my-github-revision-url (revision file remote)
  "Returns the REMOTE github instance url of REVISION for FILE."
  (let ((repo-url (vc-git-repository-url file remote)))
    (format "%s/commit/%s" repo-url revision)))

(defun my-github-revision-url-default (&optional revision file remote)
  "Returns the REMOTE github instance url of REVISION for FILE.

Same as `my-github-revision-url', but all params can be nil, and
sensible defaults are set first."
  (setq file (or file (buffer-file-name) default-directory)
        revision (or revision (vc-working-revision file)))
  (my-github-revision-url revision file remote))

(defun my-github-branch-url (branch file remote)
  "Returns the REMOTE github instance url of BRANCH containing FILE.

https://github.com/MariaDB/server/commits/10.4"
  (let ((repo-url (vc-git-repository-url file remote)))
    (format "%s/commits/%s" repo-url branch)))

(defun my-github-branch-url-default (&optional branch file remote)
  "Returns the REMOTE github instance url of BRANCH containing FILE.

Same as `my-github-branch-url', but all params can be nil, and
sensible defaults are set first."
  (setq file (or file (buffer-file-name) default-directory)
        branch (or branch (car (vc-git-branches))))
  (my-github-branch-url branch file remote))

(defun my-github-file-url (file line-no revision remote)
  "Returns the REMOTE github instance url of the FILE at LINE-NO at REVISION.

https://github.com/MariaDB/server/blob/0254eb9307f58409f856517a90109d37ef9e10c4/storage/spider/mysql-test/spider/bugfix/r/mdev_31463.result"
  (let* ((repo-root (vc-git-root file))
         (path (file-relative-name file repo-root))
         (repo-url (vc-git-repository-url file remote)))
    (format "%s/blob/%s/%s" repo-url revision path
            (if line-no (format "#L%d" line-no) ""))))

(defun my-github-file-url-default (&optional file line-no revision remote)
  "Returns the REMOTE github instance url of the FILE at LINE-NO at REVISION.

Same as `my-github-file-url', but all params can be nil, and
sensible defaults are set first."
  (unless (setq file (or file (buffer-file-name)))
    (error "Please supply a file"))
  (setq revision (or revision (vc-working-revision file)))
  (my-github-file-url file line-no revision remote))

(defun my-github-raw-file-url (file revision remote)
  "Returns the REMOTE github instance url of the raw FILE at REVISION.

https://g.ypei.me/librejs.git/plain/main_background.js?id=e2230c23e4aa7a74ea34825856acf7edd8a61e04"
  (let* ((repo-root (vc-git-root file))
         (path (file-relative-name file repo-root))
         (repo-url (vc-git-repository-url file remote)))
    (format "%s/raw/%s/%s" repo-url path revision)))

(defun my-github-raw-file-url-default (&optional file revision remote)
  "Returns the REMOTE github instance url of the raw FILE at REVISION.

Same as `my-github-raw-file-url', but all params can be nil, and
sensible defaults are set first."
  (unless (setq file (or file (buffer-file-name)))
    (error "Please supply a file"))
  (setq (revision (or revision (vc-working-revision file))))
  (my-github-raw-file-url file revision remote))

(defun my-github-revision-to-url (revision)
  "Returns the remote github instance url of REVISION.

Uses default remote."
  (let ((file (or (buffer-file-name) default-directory)))
    (my-github-revision-url revision file nil)))

(defun my-github-file-to-url (file)
  "Returns the remote github instance url of FILE.

Uses default remote and currently checked out revision."
  (let ((revision (vc-working-revision file)))
    (my-github-file-url file nil revision nil)))

(defun my-github-current-file-to-url ()
  "Returns the remote github instance url of the current buffer file-loc.

Uses default remote and currently checked out revision."
  (let ((file (buffer-file-name))
        (line-no (1+ (current-line)))
        (revision (vc-working-revision file)))
    (unless file (error "Current buffer is not associated with any file"))
    (my-github-file-url file line-no revision nil)))

(defun my-github-file-to-raw-url (file)
  "Returns the remote github instance url of FILE.

Uses default remote and currently checked out revision."
  (let ((revision (vc-working-revision file)))
    (my-github-raw-file-url file revision nil)))

(defun my-github-current-file-to-raw-url ()
  "Returns the remote github instance url of the current buffer file.

Uses default remote and currently checked out revision."
  (let ((file (buffer-file-name))
        (revision (vc-working-revision file)))
    (unless file (error "Current buffer is not associated with any file"))
    (my-github-file-url file revision nil)))

;; FIXME: remove the following redundant functions
(defun my-github-revision-url (file revision)
  "Returns the github url of the upstream repo containing FILE at REVISION."
  (let ((repo-url (vc-git-repository-url file))
        (revision (or revision (vc-working-revision default-directory))))
    (format "%s/commit/%s" repo-url revision)))

(defun my-github-kill-revision-url (revision)
  "Kill the github revision url for REVISION."
  (kill-new (my-github-revision-url revision)))

(defun my-github-file-loc-url (file-loc &optional revision)
  "Convert a file location to a github url."
  (pcase-let* ((`(,file ,line-no) (split-string file-loc ":"))
               (revision (or revision (vc-working-revision file))))
    (my-github-file-loc-url-internal file line-no revision)))

(defun my-github-file-loc-url-internal (file line-no revision)
  "Convert a FILE location at LINE-NO to a github url.

REVISION is the commit hash."
  (let* ((repo-url (vc-git-repository-url file))
         (repo-root (vc-git-root file))
         (path (file-relative-name file repo-root)))
    (format "%s/blob/%s/%s#L%s" repo-url revision path line-no)))

(defun my-github-kill-current-file-loc-url ()
  "Kill the github url from where the point is at."
  (interactive)
  (kill-new (my-github-file-loc-url-internal
             (buffer-file-name)
             (1+ (current-line))
             (vc-working-revision (buffer-file-name)))))

(defun my-org-backtrace-to-github (bt &optional revision)
  (string-join
   (mapcar
    (lambda (link)
      (string-match "\\[\\[\\(.*\\)\\]\\[\\(.*\\)\\]\\]" link)
      (let ((target (match-string 1 link))
            (label (match-string 2 link)))
        (format "[[%s][%s]]"
                (my-github-file-loc-url target revision)
                label)))
    (split-string bt " > "))
   " > "))

(defun my-org-backtrace-to-github-region (beg end)
  (interactive "r")
  (kill-new
   (my-org-backtrace-to-github (buffer-substring-no-properties beg end))))

(defun my-org-backtrace-to-github-slack (beg end)
  (interactive "r")
  (let ((bt (buffer-substring-no-properties beg end))
        (revision (when current-prefix-arg
                    (read-string "Rrevision: ")))
        )
    (with-temp-buffer
      (insert "#+options: ^:nil
")
      (goto-char (point-max))
      (insert (my-org-backtrace-to-github bt revision))
      (org-md-export-as-markdown))))

(provide 'my-github)
;;; my-github.el ends here