;; Display a calendar inside GNU Emacs.
;; Copyright (C) 1988 Free Software Foundation, Inc.

;; This file is part of GNU Emacs.

;; GNU Emacs 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 1, or (at your option)
;; any later version.

;; GNU Emacs 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 GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
;;
;; Comments, corrections, and improvements should be sent to
;;  Edward M. Reingold               Department of Computer Science
;;  (217) 333-6733                   University of Illinois at Urbana-Champaign
;;  reingold@a.cs.uiuc.edu           1304 West Springfield Avenue
;;                                   Urbana, Illinois 61801
;;
;; The author gratefully acknowledges the patient help of Richard Stallman
;; in making this function into a reasonable piece of code!
;;
;; Modification for month-offset arguments suggested and implemented by
;;  Constantine Rasmussen            Sun Microsystems, East Coast Division
;;  (617) 671-0404                   2 Federal Street;  Billerica, Ma.  01824
;;  ARPA: cdr@sun.com   USENET: {cbosgd,decvax,hplabs,seismo}!sun!suneast!cdr
;;
;; Modification to mark current day with stars suggested by
;;  Franklin Davis		     Thinking Machines Corp
;;  (617) 876-1111                   245 First Street, Cambridge, MA  02142
;;  fad@think.com

(defvar calendar-hook nil
  "List of functions called after the calendar buffer has been prepared with
the calendar of the current month.  This can be used, for example, to highlight
today's date with asterisks--a function star-date is included for this purpose.
The variable offset-calendar-hook is the list of functions called when the
calendar function was called for a past or future month.")

(defvar offset-calendar-hook nil
  "List of functions called after the calendar buffer has been prepared with
the calendar of a past or future month.  The variable calendar-hook is the
list of functions called when the calendar function was called for the
current month.")

(defun calendar (&optional month-offset)
  "Display three-month calendar in another window.
The three months appear side by side, with the current month in the middle
surrounded by the previous and next months.  The cursor is put on today's date.

An optional prefix argument ARG causes the calendar displayed to be
ARG months in the future if ARG is positive or in the past if ARG is
negative; in this case the cursor goes on the first day of the month.

The Gregorian calendar is assumed.

After preparing the calendar window, the hooks calendar-hook are run
when the calendar is for the current month--that is, the was no prefix
argument.  If the calendar is for a future or past month--that is, there
was a prefix argument--the hooks offset-calendar-hook are run.  Thus, for
example, setting calendar-hooks to 'star-date will cause today's date to be
replaced by asterisks to highlight it in the window."
  (interactive "P")
  (if month-offset (setq month-offset (prefix-numeric-value month-offset)))
  (let ((today (make-marker)))
    (save-excursion
      (set-buffer (get-buffer-create "*Calendar*"))
      (setq buffer-read-only t)
      (let*
	  ((buffer-read-only nil)
	   ;; Get today's date and extract the day, month and year.
	   (date (current-time-string))
	   (garbage (string-match
		      " \\([A-Z][a-z][a-z]\\) *\\([0-9]*\\) .* \\([0-9]*\\)$"
		      date))
	   (day (or (and month-offset 1) 
		    (string-to-int
		      (substring date (match-beginning 2) (match-end 2)))))
	   (month
	     (cdr (assoc
                    (substring date (match-beginning 1) (match-end 1))
                    '(("Jan" . 1) ("Feb" . 2)  ("Mar" . 3)  ("Apr" . 4)
                      ("May" . 5) ("Jun" . 6)  ("Jul" . 7)  ("Aug" . 8)
                      ("Sep" . 9) ("Oct" . 10) ("Nov" . 11) ("Dec" . 12)))))
	   (year (string-to-int
		   (substring date (match-beginning 3) (match-end 3)))))
	(erase-buffer)
	;; If user requested a month in the future or the past,
	;; advance the variables MONTH and YEAR to describe that one.
	(cond
          (month-offset
            (let ((year-month (+ (+ (* year 12) (- month 1)) month-offset)))
              (setq month (+ (% year-month 12) 1))
              (setq year (/ year-month 12)))))
	;; Generate previous month, starting at left margin.
	(generate-month;; previous month
	  (if (= month 1) 12 (1- month))
	  (if (= month 1) (1- year) year)
	  0)
	;; Generate this month, starting at column 24,
	;; and record where today's date appears, in the marker TODAY.
	(goto-char (point-min))
	(set-marker today (generate-month month year 24 day))
	;; Generate the following month, starting at column 48.
	(goto-char (point-min))
	(generate-month
	  (if (= month 12) 1 (1+ month))
	  (if (= month 12) (1+ year) year)
	  48)))
    ;; Display the buffer and put cursor on today's date.
    ;; Do it in another window, but if this buffer is already visible,
    ;; just select its window.
    (pop-to-buffer "*Calendar*")
    (goto-char (marker-position today))
    ;; Make TODAY point nowhere so it won't slow down buffer editing until GC.
    (set-marker today nil))
  ;; Make the window just tall enough for its contents.
  (let ((h (1- (window-height)))
        (l (count-lines (point-min) (point-max))))
    (or (= (+ (window-height (selected-window))
	      (window-height (minibuffer-window)))
	   (screen-height))
        (<= h l)
        (shrink-window (- h l))))
  (if month-offset
      (run-hooks 'offset-calendar-hook)
      (run-hooks 'calendar-hook)))

(defun leap-year-p (year)
  "Returns true if YEAR is a Gregorian leap year, and false if not."
  (or
    (and (=  (% year   4) 0)
         (/= (% year 100) 0))
    (= (% year 400) 0)))

(defun day-number (month day year)
  "Return day-number within year (origin-1) of the date MONTH DAY YEAR.
For example, (day-number 1 1 1987) returns the value 1,
while (day-number 12 31 1980) returns 366."
;;
;; an explanation of the calculation can be found in PascAlgorithms by
;; Edward and Ruth Reingold, Scott-foresman/Little, Brown, 1988.
;;
  (let ((day-of-year (+ day (* 31 (1- month)))))
    (if (> month 2)
        (progn
          (setq day-of-year (- day-of-year (/ (+ 23 (* 4 month)) 10)))
          (if (leap-year-p year)
              (setq day-of-year (1+ day-of-year)))))
    day-of-year))

(defun day-of-week (month day year)
  "Returns the day-of-the-week index of MONTH DAY, YEAR.
Value is 0 for Sunday, 1 for Monday, etc."
;;
;; Done by calculating the number of days elapsed since the (imaginary)
;; Gregorian date Sunday, December 31, 1 BC and taking that number mod 7.
;;
  (%
    (-
      (+ (day-number month day year)
         (* 365 (1- year))
         (/ (1- year) 4))
      (let ((correction (* (/ (1- year) 100) 3)))
        (if (= (% correction 4) 0)
            (/ correction 4)
            (1+ (/ correction 4)))))
    7))

(defun generate-month (month year indent &optional day)
  "Produce a calendar for MONTH, YEAR on the Gregorian calendar, inserted
in the buffer starting at the line on which point is currently located, but
indented INDENT spaces.  The position in the buffer of the optional
parameter DAY is returned.  The indentation is done from the first
character on the line and does not disturb the first INDENT characters on
the line."
  (let* ((first-day-of-month (day-of-week month 1 year))
         (first-saturday (- 7 first-day-of-month))
         (last-of-month
           (if (and (leap-year-p year) (= month 2))
               29
               (aref [31 28 31 30 31 30 31 31 30 31 30 31] (1- month))))
         (month-name
           (aref ["January" "February" "March" "April" "May" "June"
                  "July" "August" "September" "October" "November" "December"]
                  (1- month))))
    (insert-indented (format "   %s %d" month-name year) indent t)
    (insert-indented " S  M Tu  W Th  F  S" indent t)
    (insert-indented "" indent);; move point to appropriate spot on line
    (let ((i 0))               ;; add blank days before the first of the month
      (while (<= (setq i (1+ i)) first-day-of-month)
        (insert "   ")))
    (let ((i 0)
          (day-marker))        ;; put in the days of the month
      (while (<= (setq i (1+ i)) last-of-month)
        (insert (format "%2d " i))
        (and
          day
          (= i day)            ;; save the location of the specified day
          (setq day-marker (- (point) 2)))
        (and (= (% i 7) (% first-saturday 7))
             (/= i last-of-month)
             (insert-indented "" 0 t)        ;; force onto following line
             (insert-indented "" indent)))   ;; go to proper spot on line
      day-marker)))

(defun insert-indented (string indent &optional newline)
  "Insert STRING at column INDENT.
If the optional parameter NEWLINE is true, leave point at start of next
line, inserting a newline if there was no next line; otherwise, leave point
after the inserted text.  Value is always `t'."
  ;; Try to move to that column.
  (move-to-column indent)
  ;; If line is too short, indent out to that column.
  (if (< (current-column) indent)
      (indent-to indent))
  (insert string)
  ;; Advance to next line, if requested.
  (if newline
      (progn
	(end-of-line)
	(if (eobp)
            (newline)
	  (forward-line 1))))
  t)

(defun star-date ()
  "Replace today's date with asterisks in the calendar window.
This function can be used with the calendar-hook run after the
calendar window has been prepared."
  (let ((buffer-read-only nil))
    (forward-char 1)
    (delete-backward-char 2)
    (insert "**")
    (backward-char 1)))

