;;; citeproc-date.el --- CSL date rendering -*- lexical-binding: t; -*-

;; Copyright (C) 2017 András Simonyi

;; Author: András Simonyi <andras.simonyi@gmail.com>

;; 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 <http://www.gnu.org/licenses/>.

;; This file is not part of GNU Emacs.

;;; Commentary:

;; Structure type and functions to render CSL date elements.

;;; Code:

(require 'subr-x)
(require 'cl-lib)
(require 'dash)
(require 'let-alist)
(require 's)

(require 'citeproc-lib)
(require 'citeproc-rt)
(require 'citeproc-context)
(require 'citeproc-number)

(cl-defstruct (citeproc-date (:constructor citeproc-date-create))
  "Struct for representing dates.
Slots YEAR, MONTH, DAY are integers, while SEASON and CIRCA are
booleans. SEASON indicates whether the integer in slot MONTH is
to be interpreted as a season number."
  (year nil) (month nil) (day nil) (season nil) (circa nil))

(defun citeproc-date-parse (date-rep)
  "Parse CSL json date repr. DATE-REP into an internal one."
  (let-alist date-rep
    (--map (citeproc-date--conv it .season .circa) .date-parts)))

(defun citeproc-date--conv (dates &optional season circa)
  "Convert date-part list DATES to a citeproc-date struct.
Set the remaining slots to the values SEASON and CIRCA."
  (-let* ((numeric
	   (--map (if (stringp it) (string-to-number it) it) dates))
	  ((year month day) numeric))
    (citeproc-date-create :year year :month month :day day
			  :season season :circa circa)))

(defun citeproc-date--partattrs-for-sort (part-attrs)
  "Return a sort-key version of PART-ATTRS."
  (let (result)
    (when (assoc 'day part-attrs)
      (push '(day . ((form . "numeric-leading-zeros"))) result))
    (when (assoc 'month part-attrs)
      (push '(month . ((form . "numeric-leading-zeros"))) result))
    (when (assoc 'year part-attrs)
      (push '(year . ((form . "long"))) result))
    result))

(defun citeproc--date (attrs context &rest body)
  "Function corresponding to the date CSL element."
  (-let* (((&alist 'variable var
		   'form form)
	   attrs)
	  (var-sym (intern var))
	  (parsed-dates (citeproc-var-value var-sym context))
	  ((d1 d2) parsed-dates)
	  (result
	   (if d1
	       (progn
		 (when form
		   (let ((localized (citeproc-date--localized-attrs attrs body context)))
		     (setq attrs (car localized)
			   body (cdr localized))))
		 (when (eq (citeproc-context-render-mode context) 'sort)
		   (setq body (citeproc-date--partattrs-for-sort body)))
		 (if (citeproc-date--renders-with-attrs-p d1 body)
		     (progn
		       (push `(rendered-var . ,(intern var)) attrs)
		       (cons (if d2
				 (citeproc-date--render-range d1 d2 attrs body context)
			       (citeproc-date--render d1 attrs body context))
			     'present-var))
		   (cons nil 'empty-vars)))
	     (cons nil 'empty-vars))))
    ;; Handle `year' citation mode by stopping if needed
    (citeproc-context-maybe-stop-rendering 'issued context result var-sym)))

(defun citeproc--date-part (attrs _context &rest _body)
  "Function corresponding to the date-part CSL element."
  (cons (intern (alist-get 'name attrs))
	attrs))

(defun citeproc-date--renders-with-attrs-p (date part-attrs)
  "Whether DATE contains date-parts corresponding to PART-ATTRS."
  (let ((date-parts (mapcar #'car part-attrs)))
    (or (memq 'year date-parts)		; All dates contain a year
	(and (memq 'month date-parts) (citeproc-date-month date))
	(and (memq 'day date-parts) (citeproc-date-day date)))))

(defun citeproc-date--localized-attrs (attrs part-attrs context)
  "Return the localized date attrs merged with date ATTRS and date PART-ATTRS."
  (-let* (((&alist 'form form
		   'date-parts date-parts)
	   attrs)
	  ((loc-attrs . loc-part-attrs)
	   (if (string= form "text") (citeproc-context-date-text context)
	     (citeproc-context-date-numeric context))))
    (pcase (citeproc-lib-intern date-parts)
      ('year
       (setq loc-part-attrs
	     (--select (eq (car it) 'year) loc-part-attrs)))
      ('year-month
       (setq loc-part-attrs
	     (--select (memq (car it) '(year month)) loc-part-attrs))))
    (cons (-concat attrs loc-attrs)
	  (--map (cons (car it)
		       (-concat (alist-get (car it) part-attrs) (cdr it)))
		 loc-part-attrs))))

(defun citeproc-date--render (d attrs part-attrs context)
  "Render citeproc-date D according to formatting in ATTRS and PART-ATTRS.
Return a rich-text content."
  (if (citeproc-var-value 'suppress-date context)
      (citeproc-rt-format-single attrs "<suppressed-date>" context)
    (let ((rendered-date (citeproc-date--render-parts d part-attrs context)))
      (citeproc-rt-join-formatted attrs rendered-date context))))

(defun citeproc-date--render-parts (d part-attrs context &optional no-last-suffix)
  "Render the parts of citeproc-date D according to PART-ATTRS.
Return a list of rich-text contents. If optional NO-LAST-SUFFIX
is non-nil then remove the suffix attribute of the last rendered
element (used for date range rendering)."
  (let ((result (--map (pcase (car it)
			 ('year (citeproc-date--render-year d (cdr it) context))
			 ('month (citeproc-date--render-month d (cdr it) context))
			 ('day (citeproc-date--render-day d (cdr it) context)))
		       part-attrs)))
    (-if-let* ((n-l-s no-last-suffix)
	       (last (car (last result)))
	       (wo-suffix (and (consp last)
			       (cons (--remove (eq 'suffix (car it)) (car last))
				     (cdr last)))))
	(-snoc (butlast result) wo-suffix)
      result)))

(defun citeproc-date--render-range-parts (d1 d2 part-attrs sep context)
  "Render the parts of citeproc-dates D1 and D2 according to PART-ATTRS.
PART-ATTRS is a list containing either part-attrs or lists of part-attrs.
The formers are only rendered for D1, while the latters are rendered for both
D1 and D2. Return a list of rich-text contents."
  (--mapcat (pcase (car it)
	      ('year (list (citeproc-date--render-year d1 (cdr it) context)))
	      ('month (list (citeproc-date--render-month d1 (cdr it) context)))
	      ('day (list (citeproc-date--render-day d1 (cdr it) context)))
	      (_ (-concat (citeproc-date--render-parts d1 it context t)
			  (list sep)
			  (citeproc-date--render-parts d2 it context))))
	    part-attrs))

(defun citeproc-date--render-range (d1 d2 attrs part-attrs context)
  "Render the range given by dates D1 D2 according to attrs."
  (if (citeproc-var-value 'suppress-date context)
      (citeproc-rt-format-single attrs "" context)
    (let* ((gran (min (citeproc-date--gran d1)
		      (citeproc-date--attrs-gran part-attrs)))
	   (range-sep (or (alist-get 'range-delimiter
				     (alist-get (elt '(year month day) gran)
						part-attrs))
			  "–"))
	   (range-p-attrs
	    (cond ((/= (citeproc-date-year d1) (citeproc-date-year d2))
		   (list part-attrs))
		  ((/= (citeproc-date-month d1) (citeproc-date-month d2))
		   (let ((year-part (--find (eq 'year (car it))
					    part-attrs))
			 (attrs-wo-year
			  (--remove (eq 'year (car it))
				    part-attrs)))
		     (cond ((eq (caar part-attrs) 'year)
			    (list year-part attrs-wo-year))
			   ((eq (caar (last part-attrs)) 'year)
			    (list attrs-wo-year year-part))
			   (t (list attrs-wo-year)))))
		  (t (--map (if (eq (car it) 'day) (list it) it)
			    part-attrs))))
	   (rendered-range (citeproc-date--render-range-parts d1 d2 range-p-attrs range-sep
							      context)))
      (citeproc-rt-join-formatted attrs rendered-range context))))

(defun citeproc-date--attrs-gran (d-attrs)
  "Return the granularity (smallest unit) of date-attrs alist D-ATTRS.
The returned value is 0, 1 or 2, corresponding to a year, month
or day granularity."
  (cond ((assoc 'day d-attrs) 2)
	((assoc 'month d-attrs) 1)
	(t 0)))

(defun citeproc-date--gran (date)
  "Return the granularity (smallest unit) in citeproc-date struct DATE.
The returned value is 0, 1 or 2, corresponding to a year, month
or day granularity."
  (cond ((citeproc-date-day date) 2)
	((citeproc-date-month date) 1)
	(t 0)))

(defun citeproc-date--render-year (d attrs context)
  "Render the year in date D according to formatting in ATTRS.
D is a citeproc-date structure. Return a rich-text content."
  (-let* ((form (alist-get 'form attrs))
	  (year (citeproc-date-year d))
	  (s (number-to-string (abs year)))
	  (era
	   (cond ((> year 999) "")
		 ((> year 0) (citeproc-term-get-text "ad" context))
		 (t (citeproc-term-get-text "bc" context)))))

    (citeproc-rt-format-single attrs (concat (if (string= form "short")
						 (s-right 2 s)
					       s)
					     era)
			       context)))

(defun citeproc-date--render-month (d attrs context)
  "Render the month in date D according to formatting in ATTRS.
D is a citeproc-date structure. Return a rich-text content."
  (-if-let (month (citeproc-date-month d))
      (let ((form (alist-get 'form attrs))
	    (term-pref (if (citeproc-date-season d)
			   "season-" "month-")))
	(citeproc-rt-format-single
	 attrs
	 (pcase (citeproc-lib-intern form)
	   ('numeric (number-to-string month))
	   ('numeric-leading-zeros (format "%02d" month))
	   ('short (citeproc-term-inflected-text
		    (concat term-pref (format "%02d" month))
		    'short nil context))
	   (_ (citeproc-term-inflected-text
	       (concat term-pref (format "%02d" month))
	       'long nil context)))
	 context))
    nil))

(defun citeproc-date--render-day (d attrs context)
  "Render the day in date D according to formatting in ATTRS.
D is a citeproc-date structure. Return a rich-text content."
  (-if-let (day (citeproc-date-day d))
      (let ((form (alist-get 'form attrs))
	    (month (citeproc-date-month d)))
	(citeproc-rt-format-single
	 attrs
	 (cond
	  ((string= form "numeric-leading-zeros")
	   (format "%02d" day))
	  ((and (string= form "ordinal")
		(or (= day 1)
		    (not (string= "true"
				  (alist-get 'limit-day-ordinals-to-day-1
					     (citeproc-context-locale-opts context))))))
	   (citeproc-number--format-as-ordinal (number-to-string day)
					       (concat "month-" (format "%02d" month))
					       context))
	  (t (number-to-string day)))
	 context))
    nil))

(provide 'citeproc-date)

;;; citeproc-date.el ends here
