;;; -*- lexical-binding: t; -*-
;;; eide-compare.el --- Emacs-IDE: Comparison of files with ediff

;; Copyright © 2008-2026 Cédric Marie

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

;; This program 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 General Public License for
;; more details.

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

;;; Code:

(provide 'eide-compare)

(require 'ediff)
(require 'hideshow) ; for hs-minor-mode

(require 'eide-config)
(require 'eide-keys)
(require 'eide-menu)
(require 'eide-project)
(require 'eide-windows)

(defvar eide-compare-other-project-name nil)
(defvar eide-compare-other-project-directory nil)

(defvar eide-compare-buffer-name nil)
(defvar eide-compare-current-point nil)
(defvar eide-compare-other-buffer-name nil)

(defvar eide-compare-external-diff-command nil)
(defvar eide-compare-user-ediff-split-window-function nil)

;; Ediff
(copy-face 'default 'ediff-even-diff-face-A)
(set-face-background 'ediff-even-diff-face-A "wheat")
(set-face-foreground 'ediff-even-diff-face-A "black")

(copy-face 'default 'ediff-even-diff-face-B)
(set-face-background 'ediff-even-diff-face-B "wheat")
(set-face-foreground 'ediff-even-diff-face-B "black")

(copy-face 'default 'ediff-odd-diff-face-A)
(set-face-background 'ediff-odd-diff-face-A "wheat")
(set-face-foreground 'ediff-odd-diff-face-A "black")

(copy-face 'default 'ediff-odd-diff-face-B)
(set-face-background 'ediff-odd-diff-face-B "wheat")
(set-face-foreground 'ediff-odd-diff-face-B "black")

;; Current difference: what is common or only in one buffer
(copy-face 'default 'ediff-current-diff-face-A)
(set-face-background 'ediff-current-diff-face-A "pink")
(set-face-foreground 'ediff-current-diff-face-A "black")

(copy-face 'default 'ediff-current-diff-face-B)
(set-face-background 'ediff-current-diff-face-B "pink")
(set-face-foreground 'ediff-current-diff-face-B "black")

;; Current difference: what really differs
(copy-face 'default 'ediff-fine-diff-face-A)
(set-face-background 'ediff-fine-diff-face-A "plum")
(set-face-foreground 'ediff-fine-diff-face-A "black")

(copy-face 'default 'ediff-fine-diff-face-B)
(set-face-background 'ediff-fine-diff-face-B "plum")
(set-face-foreground 'ediff-fine-diff-face-B "black")

(defcustom eide-custom-external-diff-command ""
  "External diff command. Use Emacs internal (ediff) if empty."
  :tag "External diff command"
  :type 'string
  :set 'eide-i-compare-custom-set-external-diff-command
  :initialize 'custom-initialize-default
  :group 'eide-compare)

(defgroup eide-override-compare nil "Compare settings."
  :tag "Compare"
  :group 'eide-emacs-settings)
(defcustom eide-custom-diff-side-by-side t
  "Compare buffers side-by-side (ediff-split-window-function
\\='split-window-horizontally)."
  :tag "Compare buffers side-by-side"
  :type '(choice (const :tag "Don't override" nil)
                 (const :tag "Side-by-side" t))
  :set (lambda (param value) (set-default param value) (eide-i-config-apply-emacs-settings))
  :initialize 'custom-initialize-default
  :group 'eide-override-compare)

;; ----------------------------------------------------------------------------
;; CUSTOMIZATION FUNCTIONS
;; ----------------------------------------------------------------------------

(defun eide-i-compare-custom-set-external-diff-command (param value)
  "Set external diff command.
Arguments:
- param: customization parameter.
- value: customization value."
  (set-default param value)
  (when eide-config-ready
    (if (string-equal value "")
        (setq eide-compare-external-diff-command nil)
      (setq eide-compare-external-diff-command value))))

;; ----------------------------------------------------------------------------
;; INTERNAL FUNCTIONS
;; ----------------------------------------------------------------------------

(defun eide-i-compare-ediff-mode-start ()
  "Start ediff mode."
  (eide-windows-hide-ide-windows)
  (eide-windows-save-and-unbuild-layout)
  (eide-keys-configure-for-ediff)
  (advice-remove 'select-window #'select-window--advice-after))

(defun eide-i-compare-ediff-mode-stop ()
  "Stop ediff mode."
  (eide-keys-configure-for-editor)
  (eide-windows-restore-layout)
  (eide-windows-show-ide-windows)
  (advice-add 'select-window :after #'select-window--advice-after))

(defun eide-i-compare-ediff-quit-hook ()
  "Hook for exiting ediff: Close temporary buffer, and restore display."
  ;; Call default hook
  (ediff-cleanup-mess)
  ;; Restore default hook
  (setq ediff-quit-hook 'ediff-cleanup-mess)
  (eide-i-compare-ediff-mode-stop)
  ;; Restore cursor position in the buffer that has been compared
  (set-buffer eide-compare-buffer-name)
  (goto-char eide-compare-current-point)
  ;; Back to current buffer
  (switch-to-buffer eide-current-buffer)
  (kill-buffer eide-compare-other-buffer-name))

(defun eide-i-compare-ediff-buffer-and-file (p-other-buffer-filename p-other-buffer-name-prefix p-buffer-in-left-window-flag p-force-major-mode-flag)
  "Compare a buffer and a file.
Arguments:
- p-other-buffer-filename: name of file to compare.
- p-other-buffer-name-prefix: prefix to add before file buffer name.
- p-buffer-in-left-window-flag: t = buffer | file, nil = file | buffer.
- p-force-major-mode-flag: t = force syntax highlighting for file (necessary for
  \".ref\" or \".new\" files)"
  (eide-i-compare-ediff-mode-start)
  (setq ediff-quit-hook 'eide-i-compare-ediff-quit-hook)
  ;; Hide IDE windows
  (eide-windows-hide-ide-windows)
  ;; Save current position in buffer to compare
  (set-buffer eide-compare-buffer-name)
  (setq eide-compare-current-point (point))

  (if p-force-major-mode-flag
      (let ((l-auto-mode-alist auto-mode-alist))
        ;; Add .ref and .new files in auto-mode-alist (with current buffer major
        ;; mode)
        (push (cons "\\.ref\\'" major-mode) auto-mode-alist)
        (push (cons "\\.new\\'" major-mode) auto-mode-alist)
        (eide-windows-find-file-without-advice p-other-buffer-filename)
        ;; Restore auto-mode-alist
        (setq auto-mode-alist l-auto-mode-alist)
        ;; Turn hide/show mode off, because if emacs is closed before this
        ;; temporary buffer is closed, it will be loaded next time, with an error
        ;; because default major mode is Fundamental
        (when hs-minor-mode
          (hs-minor-mode)))
    (eide-windows-find-file-without-advice p-other-buffer-filename))

  (setq eide-compare-other-buffer-name (concat p-other-buffer-name-prefix eide-compare-buffer-name))
  (rename-buffer eide-compare-other-buffer-name)
  (if p-buffer-in-left-window-flag
      (ediff-buffers eide-compare-buffer-name eide-compare-other-buffer-name)
    (ediff-buffers eide-compare-other-buffer-name eide-compare-buffer-name)))

(defun eide-i-compare-select-control-window ()
  "Select ediff control window (before calling ediff command)."
  (let ((l-control-window nil))
    (with-current-buffer "*Ediff Control Panel*"
      (setq l-control-window ediff-control-window))
    (select-window l-control-window)))

;; ----------------------------------------------------------------------------
;; FUNCTIONS
;; ----------------------------------------------------------------------------

(defun eide-compare-save-emacs-settings ()
  "Save Emacs settings (for compare)."
  (setq eide-compare-user-ediff-split-window-function ediff-split-window-function))

(defun eide-compare-apply-emacs-settings ()
  "Apply Emacs settings (for compare)."
  (if (and eide-custom-override-emacs-settings eide-custom-diff-side-by-side)
      (setq ediff-split-window-function 'split-window-horizontally)
    (setq ediff-split-window-function eide-compare-user-ediff-split-window-function)))

(defun eide-compare-apply-customization ()
  "Apply compare customization."
  (eide-i-compare-custom-set-external-diff-command 'eide-custom-external-diff-command eide-custom-external-diff-command))

(defun eide-compare-select-another-project (p-project-name p-project-directory)
  "Select another project for comparison. Unselect it if both arguments are nil.
Arguments:
- p-project-name: project name.
- p-project-directory: project directory."
  (setq eide-compare-other-project-name p-project-name)
  (setq eide-compare-other-project-directory p-project-directory))

(defun eide-compare-with-ref-file (p-buffer-name)
  "Compare selected file (\".new\" version) with \".ref\" version.
Argument:
- p-buffer-name: name of buffer to compare."
  (setq eide-compare-buffer-name p-buffer-name)
  (let ((l-buffer-name nil) (l-ref-buffer-name nil))
    (setq l-buffer-name (buffer-file-name (get-buffer eide-compare-buffer-name)))
    (setq l-ref-buffer-name (concat l-buffer-name ".ref"))
    (if eide-compare-external-diff-command
        (start-process-shell-command "ext-diff" nil (concat eide-compare-external-diff-command " " l-ref-buffer-name " " l-buffer-name))
      (eide-i-compare-ediff-buffer-and-file l-ref-buffer-name "* (REF) " nil t))))

(defun eide-compare-with-new-file (p-buffer-name)
  "Compare selected file (\".ref\" version) with \".new\" version.
Argument:
- p-buffer-name: name of buffer to compare."
  (setq eide-compare-buffer-name p-buffer-name)
  (let ((l-buffer-name nil) (l-new-buffer-name nil))
    (setq l-buffer-name (buffer-file-name (get-buffer eide-compare-buffer-name)))
    (setq l-new-buffer-name (concat l-buffer-name ".new"))
    (if eide-compare-external-diff-command
        (start-process-shell-command "ext-diff" nil (concat eide-compare-external-diff-command " " l-buffer-name " " l-new-buffer-name))
      (eide-i-compare-ediff-buffer-and-file l-new-buffer-name "* (NEW) " t t))))

(defun eide-compare-with-other-project (p-buffer-name)
  "Compare selected file with version in another project.
Argument:
- p-buffer-name: name of buffer to compare."
  (setq eide-compare-buffer-name p-buffer-name)
  (let ((l-this-file nil) (l-other-file nil))
    (setq l-this-file (buffer-file-name (get-buffer eide-compare-buffer-name)))
    (setq l-other-file (concat eide-compare-other-project-directory (substring l-this-file (length eide-root-directory))))
    (if (file-exists-p l-other-file)
        (if eide-compare-external-diff-command
            (start-process-shell-command "ext-diff" nil (concat eide-compare-external-diff-command " " l-other-file " " l-this-file))
          (eide-i-compare-ediff-buffer-and-file l-other-file (concat "* (" eide-compare-other-project-name ") ") nil nil))
      (eide-popup-message "This file doesn't exist in the other project."))))

(defun eide-compare-quit ()
  "Quit ediff session."
  (interactive)
  (eide-i-compare-select-control-window)
  (call-interactively 'ediff-quit))

(defun eide-compare-update ()
  "Update diffs."
  (interactive)
  (eide-i-compare-select-control-window)
  (ediff-update-diffs))

(defun eide-compare-go-to-previous-diff ()
  "Go to previous diff."
  (interactive)
  (eide-i-compare-select-control-window)
  (ediff-previous-difference))

(defun eide-compare-go-to-next-diff ()
  "Go to next diff."
  (interactive)
  (eide-i-compare-select-control-window)
  (ediff-next-difference))

(defun eide-compare-copy-a-to-b ()
  "Copy A to B."
  (interactive)
  (eide-i-compare-select-control-window)
  (call-interactively 'ediff-copy-A-to-B))

(defun eide-compare-copy-b-to-a ()
  "Copy B to A."
  (interactive)
  (eide-i-compare-select-control-window)
  (call-interactively 'ediff-copy-B-to-A))

;;; eide-compare.el ends here
