;;;;
;;;;;; Devel (project/langs/tags/build/debug)
;;;;

(require 'json)

;;
;; SECTION: project
;; global project management - src and build directories
;;

(defvar eaw-src-home (concat (getenv "HOME") "/src"))

(defvar eaw-build-map nil)

(defun eaw-expand-src-dir (dir)
  (expand-file-name (concat eaw-src-home "/" dir "/")))

(defun eaw-find-likely-proj-root ()
  (or
   (eaw-find-file-down-heirarchy ".gitignore" eaw-src-home)
   (eaw-find-file-up-heirarchy "compile_commands.json" eaw-src-home)
   (concat eaw-src-home "/")))

(defvar eaw-proj-map (make-sparse-keymap))
(define-key global-map (kbd "C-x g") eaw-proj-map)
(defvar eaw-proj-rootdir "")
(defvar eaw-proj-change-hook nil)
(defun eaw-proj-go (proj)
  (interactive
   (list (read-directory-name "proj root: "
                              (eaw-find-likely-proj-root)
                              (eaw-find-likely-proj-root)
                              t)))
  (if (not (unless (string= (expand-file-name proj) eaw-proj-rootdir)))
      (let* ((projdir (expand-file-name (concat proj "/")))
             (nonsrc (substring projdir (length (expand-file-name (concat eaw-src-home "/"))) -1))
             dirname)
        (message "switching to %s" projdir)
        (setq eaw-proj-rootdir projdir)
        (run-hooks 'eaw-proj-change-hook))))
(define-key eaw-proj-map "g" 'eaw-proj-go)

(defun eaw-proj-vc-go ()
  (interactive)
  (let ((default-directory eaw-proj-rootdir))
    (magit-status)))
(define-key eaw-proj-map "j" 'eaw-proj-vc-go)

(defun eaw-find-dir-in-build-map (curdir)
  (let (found)
    (dolist (mapval eaw-build-map)
      (unless found
        (let ((mapdir (eaw-expand-src-dir (car mapval))))
          (if (or (string= curdir mapdir)
                  (and (> (length curdir) (length mapdir))
                   (string= (substring curdir (length mapdir) -1) mapdir)))
              (setq found (cdr mapval))))))
    found))

(defun eaw-find-likely-build-list ()
  (let ((found nil))
    (setq found (eaw-find-dir-in-build-map eaw-proj-rootdir))
    (unless found (setq found (eaw-find-dir-in-build-map (eaw-find-likely-proj-root))))
    (cond
     ((stringp found) (eaw-expand-src-dir found))
     ((listp found) (mapcar 'eaw-expand-src-dir found))
     (t default-directory))))

(defun eaw-find-likely-build-dir ()
  "Return the likely build directory for the given project. First
consult `eaw-proj-rootdir', then `eaw-find-likely-proj-root'.
Consult each against `eaw-build-map'. If the build map is nil, or
they aren't in the build map, just return `default-directory'. If
the build map is non-nil, then car is a source directory and cdr
is either a build directory (string) or a list of build
directories."
  (let ((fulldirs (eaw-find-likely-build-list)))
    (if fulldirs
        (if (stringp fulldirs)
            fulldirs
          (reduce '(lambda (a b)
                     (if (string-prefix-p (file-truename b) (file-truename default-directory)) b a))
                  fulldirs))
      eaw-proj-rootdir)))

(defvar eaw-compile-list
  '(("build.ninja" . "ninja -j 4 %s")
    ("Makefile" . "make -j 4 %s")))

(require 'cl)
(defun eaw-compile-likely-dir (&optional tgt)
  (interactive)
  (let ((default-directory (eaw-find-likely-build-dir))
        (target (or tgt "")))
    (dolist (elt eaw-compile-list)
      (when (file-exists-p (car elt))
        (compile
         (cond
          ((stringp (cdr elt)) (format (cdr elt) target))
          ((functionp (cdr elt)) (funcall (cdr elt) target))
          (t (error))))
        (return)))))
(define-key eaw-proj-map "b" 'eaw-compile-likely-dir)

(defun eaw-clean-likely-dir ()
  (interactive)
  (eaw-compile-likely-dir "clean"))
(define-key eaw-proj-map "c" 'eaw-clean-likely-dir)

(defun eaw-proj-whereami ()
  (interactive)
  (if eaw-proj-rootdir (message eaw-proj-rootdir) (message "No project currently selected")))
(define-key eaw-proj-map "w" 'eaw-proj-whereami)

(defun eaw-project-shell ()
  (interactive)
  (let* ((default-directory eaw-proj-rootdir)
         (projname (file-name-nondirectory (directory-file-name default-directory))))
    (shell
     (concat "*shell-" projname "*"))))
(define-key eaw-proj-map "z" 'eaw-project-shell)

(defun eaw-project-eshell ()
  (interactive)
  (let* ((default-directory eaw-proj-rootdir)
         (projname (file-name-nondirectory (directory-file-name default-directory)))
         (eshell-buffer-name (concat "*eshell-" projname "*"))
         (orig-esh-hist-fn (default-value 'eshell-history-file-name))
         (new-esh-hist-fn (expand-file-name (concat "history-" projname) eshell-directory-name)))
    (if (get-buffer eshell-buffer-name)
        (pop-to-buffer eshell-buffer-name))
    (setq-default eshell-history-file-name new-esh-hist-fn)
    (eshell)
    (setq-default eshell-history-file-name orig-esh-hist-fn)))
(define-key eaw-proj-map "e" 'eaw-project-eshell)

(defun eaw-ios-sdk-ver (&optional platform)
  (interactive)
  (let ((choices
         (seq-map
          (lambda (elt) (alist-get 'canonicalName elt))
          (seq-filter
           (lambda (elt) (string= (or platform "iphonesimulator") (alist-get 'platform elt)))
           (json-read-from-string (shell-command-to-string "xcodebuild -showsdks -json"))))))
    (cond
     ((= 0 (length choices)) (error "No iOS SDK versions available"))
     ((= 1 (length choices)) (car choices))
     (t (ido-completing-read "iOS SDK version: " choices)))))

(defun eaw-ios-sim-ver ()
  (interactive)
  (let ((choices
         (seq-map
          (lambda (elt) (alist-get 'version elt))
          (seq-filter
           (lambda (elt) (string-prefix-p "iOS " (alist-get 'name elt)))
           (alist-get
            'runtimes
            (json-read-from-string (shell-command-to-string "xcrun simctl list runtimes available --json")))))))
    (cond
     ((= 0 (length choices)) (error "No iOS Simulators available"))
     ((= 1 (length choices)) (car choices))
     (t (ido-completing-read "iOS Simulator version: " choices)))))

(setq eaw-ios-sim-dev "iPhone 12 Pro")
(defun eaw-choose-ios-sim-dev ()
  (interactive)
  (let ((choices
         (seq-map
          (lambda (elt) (alist-get 'name elt))
          (seq-mapcat
           'cdr
           (alist-get
            'devices
            (json-read-from-string (shell-command-to-string "xcrun simctl list devices iOS --json")))))))
    (setq eaw-ios-sim-dev (ido-completing-read "iOS Simulator device: " choices nil t nil nil eaw-ios-sim-dev))
    (shell-command "xcrun simctl shutdown booted")))

(setq eaw-ios-workspace nil
      eaw-ios-scheme nil
      eaw-ios-project nil)

(defun eaw-ios-build-cmd-base ()
  (concat "xcodebuild "
          (if eaw-ios-project
              (concat "-project " eaw-ios-project " ")
            (concat
             "-derivedDataPath DerivedData "
             "-workspace " eaw-ios-workspace " "
             "-scheme " eaw-ios-scheme " "
             ))
          "-configuration Debug "
          ;; "-sdk " (eaw-ios-sdk-ver "iphoneos") " "
          ;; "-destination 'generic/platform=iOS' "
          "-sdk " (eaw-ios-sdk-ver) " "
          "-destination 'platform=iOS Simulator,name=" eaw-ios-sim-dev ",OS=" (eaw-ios-sim-ver) "' "
          "-parallelizeTargets -jobs 6 "))

(defun eaw-ios-build (&optional target)
  (interactive)
  (concat
   (eaw-ios-build-cmd-base)
   target
   (if (executable-find "xcpretty")
       " | xcpretty --no-color --no-utf"
     "-quiet ")))

(defun eaw-ios-find-workspace ()
  (interactive)
  (let* ((default-directory eaw-proj-rootdir)
         (choices (sort
                   (cond
                    ((file-expand-wildcards "*.xcworkspace"))
                    ((file-expand-wildcards "*.xcodeproj"))
                    ((file-expand-wildcards "*/*.xcworkspace"))
                    ((file-expand-wildcards "*/*.xcodeproj")))
                   'string<))
         (choice
          (cond
           ((= 1 (length choices)) (car choices))
           ((< 1 (length choices)) (ido-completing-read "XCode Workspace/Project file: " choices)))))
    (if (and choice (string-match-p "xcodeproj\\'" choice))
        (setq eaw-ios-workspace nil
              eaw-ios-scheme nil
              eaw-ios-project choice)
      (setq eaw-ios-workspace choice
            ;; XXX need to set eaw-ios-scheme here somehow
            eaw-ios-project nil))
    (unless (or (not choice) (assoc choice eaw-compile-list))
      (add-to-list 'eaw-compile-list `(,choice . eaw-ios-build)))))

(defun eaw-ios-xcode ()
  (interactive)
  (if eaw-ios-workspace
      (shell-command (concat "open " eaw-proj-rootdir eaw-ios-workspace))))

(if (executable-find "xcodebuild")
    (progn
      (define-key eaw-proj-map "d" 'eaw-choose-ios-sim-dev)
      (define-key eaw-proj-map "x" 'eaw-ios-xcode)
      (add-hook 'eaw-proj-change-hook 'eaw-ios-find-workspace)))

;;
;; SECTION: langs
;; c-like, c++, obj-c, as, js, xml, groovy
;;

;; Make a non-standard key binding.  We can put this in
;; c-mode-base-map because c-mode-map, c++-mode-map, and so on,
;; inherit from it.
(defun my-c-initialization-hook ()
  (define-key c-mode-base-map "\C-m" 'c-context-line-break)
  (define-key c-mode-base-map (kbd "C-c C-c") 'comment-or-uncomment-region))
(add-hook 'c-initialization-hook 'my-c-initialization-hook)

;; Create my personal style.
(defconst my-c-style
  '((c-block-comment-prefix     . "* ")
    (c-cleanup-list             . (brace-else-brace
                                   brace-elseif-brace
                                   brace-catch-brace
                                   defun-close-semi
                                   list-close-comma
                                   scope-operator
                                   comment-close-slash))
    (c-basic-offset             . 4)
    (c-offsets-alist            . ((inline-open . 0)
                                   (statement . 0)
                                   (statement-cont . +)
                                   (statement-block-intro . +)
                                   (statement-case-intro  . +)
                                   (statement-case-open  . 0)
                                   (substatement . +)
                                   (substatement-open . 0)
                                   (member-init-intro . *)
                                   (member-init-cont . c-lineup-multi-inher)
                                   (access-label . -)
                                   (case-label . 0)
                                   (label . /)
                                   (friend . 0)
                                   (extern-lang-open . 0)
                                   (inextern-lang . 0)
                                   (extern-lang-close . 0)
                                   (namespace-open . 0)
                                   (innamespace . 0)
                                   (namespace-close . 0)
                                   (knr-argdecl . 0)
                                   (knr-argdecl-intro . +)))
    (c-hanging-braces-alist     . ((brace-list-open)
                                   (brace-list-close)
                                   (brace-entry-open)
                                   (statement-cont)
                                   (substatement-open after)
                                   (block-close)
                                   (defun-close)
                                   (extern-lang-open after)
                                   (namespace-open after)
                                   (module-open after)
                                   (composition-open after)
                                   (inexpr-class-open after)
                                   (inexpr-class-close before)))
    (fill-column . 80)
    (c-comment-only-line-offset . 0))
  "My C Programming Style")
(c-add-style "sane" my-c-style)

;; opening a .h file that's actually c++ should use c++ mode.
(defun eaw-cpp-header ()
  (let ((name (or buffer-file-name (buffer-name))))
    (and name
         (string-match "\\.h\\'" name)
         (re-search-forward "\\W\\(class\\|template\\|namespace\\)\\W" nil t))))
(add-to-list 'magic-mode-alist '(eaw-cpp-header . c++-mode))
(defun eaw-objc-header ()
  (let ((name (or buffer-file-name (buffer-name))))
    (and name
         (string-match "\\.h\\'" name)
         (re-search-forward "\\W@\\(interface\\|property\\|end\\)\\W" nil t))))
(add-to-list 'magic-mode-alist '(eaw-objc-header . objc-mode))
(add-to-list 'auto-mode-alist '("\\.mm\\'" . objc-mode))
(if (require 'grep nil t)
    (progn
      (add-to-list 'grep-files-aliases '("mm" . "*.m *.mm *.cc *.cxx *.cpp *.C *.CC *.c++"))
      (add-to-list 'grep-files-aliases '("mmhh" . "*.m *.mm *.cc *.cxx *.cpp *.C *.CC *.c++ *.hxx *.hpp *.[Hh] *.HH *.h++"))))

(defvar eaw-anything-c-source-objc-headline
  '((name . "Objective-C Headline")
    (headline  "^[-+@]\\|^#pragma mark")))
(if (require 'anything nil t)
    (progn
      (require 'anything-config)
      (defun eaw-objc-headline ()
        (interactive)
        ;; Set to 500 so it is displayed even if all methods are not narrowed down.
        (let ((anything-candidate-number-limit 500))
          (anything-other-buffer '(eaw-anything-c-source-objc-headline)
                                 "*ObjC Headline*")))
      (global-set-key (kbd "C-c o") 'eaw-objc-headline)))

(setenv "CTAGS" "----langmap=ObjectiveC:.m.h")

;; Customizations for all modes in CC Mode.
(defun my-c-mode-common-hook ()
  ;; set my personal style for the current buffer
  (c-set-style "sane")
  ;; preferred minor modes
  (c-toggle-electric-state 1)
  (c-toggle-auto-newline 1)
  (c-toggle-hungry-state -1)
  (if (fboundp 'c-subword-mode)
      (c-subword-mode 1)
    (subword-mode 1))
  (if (fboundp 'hs-minor-mode)
      (hs-minor-mode 1))
  (c-toggle-syntactic-indentation 1)
  ;; tab should format, not insert tab (counter-intuitively perhaps)
  (setq c-tab-always-indent t)
  (setq c-electric-pound-behavior '(alignleft))
;;  (if (not buffer-read-only)
;;      (flymake-mode 1))
  )
(add-hook 'c-mode-common-hook 'my-c-mode-common-hook)

;; javascript
(if (require 'js2-mode nil t)
    (progn
      (add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))
      (add-hook 'js2-mode-hook '(lambda () (whitespace-mode 1)))
      (setq js2-highlight-level 3
            js2-bounce-indent-p t)
      (define-key js2-mode-map (kbd "C-m") 'newline-and-indent)

;; After js2 has parsed a js file, we look for jslint globals decl comment ("/* global Fred, _, Harry */") and
;; add any symbols to a buffer-local var of acceptable global vars
;; Note that we also support the "symbol: true" way of specifying names via a hack (remove any ":true"
;; to make it look like a plain decl, and any ':false' are left behind so they'll effectively be ignored as
;; you can;t have a symbol called "someName:false"
      (defun eaw-setup-js2-externs ()
        (when (> (buffer-size) 0)
          (let ((btext (replace-regexp-in-string
                        ": *true" " "
                        (replace-regexp-in-string "[\n\t ]+" " " (buffer-substring-no-properties 1 (buffer-size)) t t))))
            (mapc (apply-partially 'add-to-list 'js2-additional-externs)
                  (split-string
                   (if (string-match "/\\* *global *\\(.*?\\) *\\*/" btext) (match-string-no-properties 1 btext) "")
                   " *, *" t))
            )))
      (add-hook 'js2-post-parse-callbacks 'eaw-setup-js2-externs)
      (setq js2-global-externs '("require" "module"))))

(defun eaw-json-cleanup (start end)
  "Pretty-print JSON in region"
  (interactive (list (region-beginning) (region-end)))
  (shell-command-on-region start end "python -m json.tool" nil t))

(defun eaw-js-cleanup (start end)
  "Pretty-print JS in region"
  (interactive (list (region-beginning) (region-end)))
  (shell-command-on-region start end
                           (concat "java -jar " (getenv "HOME") "/var/compiler.jar "
                                   "--formatting PRETTY_PRINT --compilation_level WHITESPACE_ONLY --language_in ECMASCRIPT6") nil t))

(defun eaw-url-cleanup ()
  "Pretty-print percent-encoded URL param in region"
  (interactive)
  (let* ((start (region-beginning))
         (end (region-end))
         (encoded (buffer-substring start end)))
    (delete-region (min start end) (max start end))
    (insert (url-unhex-string encoded))))

(defun eaw-url-encode ()
  "Pretty-print percent-encoded URL param in region"
  (interactive)
  (let* ((start (region-beginning))
         (end (region-end))
         (decoded (buffer-substring start end)))
    (delete-region (min start end) (max start end))
    (insert (url-hexify-string decoded))))

;; nxml
(setq nxml-slash-auto-complete-flag t)
(add-hook 'nxml-mode-hook '(lambda () (define-key nxml-mode-map (kbd "C-c C-c") 'comment-or-uncomment-region)))

;; groovy
(autoload 'groovy-mode "groovy-mode" "Major mode for editing Groovy code." t)
(add-to-list 'auto-mode-alist '("\.groovy$" . groovy-mode))
(add-to-list 'interpreter-mode-alist '("groovy" . groovy-mode))
(add-hook 'groovy-mode-hook
          '(lambda ()
             (require 'groovy-electric)
             (groovy-electric-mode)))

;; c4 lua
(defun eaw-c4z-dec ()
  (interactive)
  (shell-command-on-region
   (point-min) (point-max)
   (concat "openssl smime -decrypt -inkey " (getenv "HOME") "/etc/c4/privatekey.pem -inform der")
   t t))

(defun eaw-c4i-dec ()
  (interactive)
  (shell-command-on-region
   (point) (mark)
   (concat "openssl aes-256-ctr -d -a -iv 00000000000000000000000000000001 -K 7db208c0699b14b46ef81b97f39afc9868dd158f1636f7f43866dfe55f19f498")
   t t))

;;
;; SECTION: LSP
;; eglot
;;

;; eglot
(defun eaw-project-try-vc (orig-fun dir)
  (let ((ptvc-dir (apply orig-fun (list dir))))
    (if (and ptvc-dir (string= (expand-file-name dir) (expand-file-name (cdr ptvc-dir))))
        ptvc-dir
      (eaw-project-try-vc orig-fun (cdr ptvc-dir)))))

(if (require 'project nil t)
    (progn
      (setq project-list-file (locate-user-emacs-file "var/projects"))
      (advice-add 'project-try-vc :around #'eaw-project-try-vc)
      (require 'eglot nil t)))

(defun eaw-ios-cc-update ()
  "Updates the compile_commands.json project information for iOS"
  (interactive)
  (let ((default-directory eaw-proj-rootdir))
    (compile
     (concat (eaw-ios-build-cmd-base) " clean build "
             " | "
             "xcpretty --no-color --no-utf -r json-compilation-database -o "
             eaw-proj-rootdir "compile_commands.json"
             " && perl -pi -e 's/ -gmodules / /g' " eaw-proj-rootdir "compile_commands.json"))))
(if (and (executable-find "xcodebuild")
         (executable-find "xcpretty"))
    (define-key eaw-proj-map "u" 'eaw-ios-cc-update))

;;
;; SECTION: build
;; emacs (flymake, compile), ant, cmake
;;

;; turn off flymake-mode immediately if a header can't find its master
(defadvice flymake-master-make-header-init (after eaw-flymake-after-master)
  (if (string= flymake-mode-line-e-w "!")
      (flymake-report-fatal-status "NOMASTER" "no master file for header")))
(ad-activate 'flymake-master-make-header-init)

;; clean up flymake timers
(defun eaw-cleanup-flymake-timers ()
  (dolist (tmr timer-list)
    (if (and (timer--args tmr)
             (nth 0 (timer--args tmr))
             (bufferp (nth 0 (timer--args tmr)))
             (or (not (buffer-live-p (nth 0 (timer--args tmr))))
                 (with-current-buffer (nth 0 (timer--args tmr)) buffer-read-only)))
        (cancel-timer tmr))))
(run-with-timer (* 30 60) (* 30 60) 'eaw-cleanup-flymake-timers)

;; when skipping to errors, show a few lines above
(setq compilation-context-lines 1)

;; scroll compilation buffer
(setq compilation-scroll-output t)

;; make sure ant's output is in a format emacs likes
(setenv "ANT_ARGS" "-emacs")

;; cmake
(autoload 'cmake-mode "cmake-mode")
(add-to-list 'auto-mode-alist '("CMakeLists\\.txt\\'" . cmake-mode))
(add-to-list 'auto-mode-alist '("\\.cmake\\'" . cmake-mode))

;;
;; SECTION: debug
;; gdb, lldb
;;

;; gdb should use many windows, to make it look like an IDE
(setq gdb-many-windows t
      gdb-max-frames 120)

;; lldb works better for some things than gdb
(require 'lldb nil t)

(provide 'ew-devel)