;;; icalendar.el --- iCalendar implementation -*- lexical-binding: t -*-

;; Copyright (C) 2002-2026 Free Software Foundation, Inc.

;; Author:         Ulf Jasper <ulf.jasper@web.de>
;; Maintainer:     emacs-devel@gnu.org
;; Created:        August 2002
;; Keywords:       calendar
;; Human-Keywords: calendar, diary, iCalendar, vCalendar
;; Old-Version:    0.19

;; 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 3 of the License, 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.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Most of the code in this file is now obsolete and has been marked as such.
;; For an overview of the new iCalendar library, see icalendar-shortdoc.el.
;; For the new implementation of diary import/export, see diary-icalendar.el.
;; Error handling code, global variables, and user options relevant for the
;; entire iCalendar library remain in this file.

;; The diary-icalendar feature is documented in the Emacs Manual.


;;; History:

;;  0.07 onwards: see commit logs and ../ChangeLog*.

;;  0.06:  (2004-10-06)
;;  - Bugfixes regarding icalendar-import-format-*.
;;  - Fix in icalendar-export-file -- thanks to Philipp Grau.

;;  0.05: (2003-06-19)
;;  - New import format scheme: Replaced icalendar-import-prefix-*,
;;    icalendar-import-ignored-properties, and
;;    icalendar-import-separator with icalendar-import-format(-*).
;;  - icalendar-import-file and icalendar-export-file
;;    have an extra parameter which should prevent them from
;;    erasing their target files (untested!).
;;  - Tested with Emacs 21.3.2

;;  0.04:
;;  - Bugfix: import: double quoted param values did not work
;;  - Read DURATION property when importing.
;;  - Added parameter icalendar-duration-correction.

;;  0.03: (2003-05-07)
;;  - Export takes care of european-calendar-style.
;;  - Tested with Emacs 21.3.2 and XEmacs 21.4.12

;;  0.02:
;;  - Should work in XEmacs now.  Thanks to Len Trigg for the XEmacs patches!
;;  - Added exporting from Emacs diary to ical.
;;  - Some bugfixes, after testing with calendars from https://icalshare.com.
;;  - Tested with Emacs 21.3.2 and XEmacs 21.4.12

;;  0.01: (2003-03-21)
;;  - First published version.  Trial version.  Alpha version.

;;; Code:

(eval-when-compile (require 'compile))
(eval-when-compile (require 'cl-lib))

;; ======================================================================
;; Customizables
;; ======================================================================
(defgroup icalendar nil
  "iCalendar support."
  :prefix "icalendar-"
  :group 'calendar)

(defcustom icalendar-import-format
  "%s%d%l%o"
  "Format for importing events from iCalendar into Emacs diary.
It defines how iCalendar events are inserted into diary file.
This may either be a string or a function.

In case of a formatting STRING the following specifiers can be used:
%c Class, see `icalendar-import-format-class'
%d Description, see `icalendar-import-format-description'
%l Location, see `icalendar-import-format-location'
%o Organizer, see `icalendar-import-format-organizer'
%s Summary, see `icalendar-import-format-summary'
%t Status, see `icalendar-import-format-status'
%u URL, see `icalendar-import-format-url'
%U UID, see `icalendar-import-format-uid'

A formatting FUNCTION will be called with a VEVENT as its only
argument.  It must return a string.  See
`icalendar-import-format-sample' for an example."
  :type '(choice
          (string :tag "String")
          (function :tag "Function")))

(make-obsolete-variable
 'icalendar-import-format
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-summary
  "%s"
  "Format string defining how the summary element is formatted.
This applies only if the summary is not empty! `%s' is replaced
by the summary."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-summary
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-description
  "\n Desc: %s"
  "Format string defining how the description element is formatted.
This applies only if the description is not empty! `%s' is
replaced by the description."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-description
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-location
  "\n Location: %s"
  "Format string defining how the location element is formatted.
This applies only if the location is not empty! `%s' is replaced
by the location."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-location
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-organizer
  "\n Organizer: %s"
  "Format string defining how the organizer element is formatted.
This applies only if the organizer is not empty! `%s' is
replaced by the organizer."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-organizer
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-url
  "\n URL: %s"
  "Format string defining how the URL element is formatted.
This applies only if the URL is not empty! `%s' is replaced by
the URL."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-url
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-uid
  "\n UID: %s"
  "Format string defining how the UID element is formatted.
This applies only if the UID is not empty! `%s' is replaced by
the UID."
  :type 'string
  :version "24.3")

(make-obsolete-variable
 'icalendar-import-format-uid
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-status
  "\n Status: %s"
  "Format string defining how the status element is formatted.
This applies only if the status is not empty! `%s' is replaced by
the status."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-status
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(defcustom icalendar-import-format-class
  "\n Class: %s"
  "Format string defining how the class element is formatted.
This applies only if the class is not empty! `%s' is replaced by
the class."
  :type 'string)

(make-obsolete-variable
 'icalendar-import-format-class
 "please use `diary-icalendar-vevent-format-function' for import
formatting instead."
 "31.1")

(define-obsolete-variable-alias
 'icalendar-recurring-start-year
 'diary-icalendar-recurring-start-year
 "31.1")

(define-obsolete-variable-alias
 'icalendar-export-hidden-diary-entries
 'diary-icalendar-export-nonmarking-entries
 "31.1")

(defcustom ical:uid-format
  "%h"
  "Format string for unique ID (UID) values for iCalendar components.

This string is used by `icalendar-make-uid' to generate UID values when
creating iCalendar components.

The following specifiers are available:
%c COUNTER, an integer value that is increased each time a uid is
   generated.  This may be necessary for systems which do not
   provide time-resolution finer than a second.
%h HASH, a hash value of the component's contents or system information,
%t TIMESTAMP, a unique creation timestamp,
%u USERNAME, the value of `user-login-name'.
%s (obsolete, ignored)

For example, a value of \"%h%t@mydomain.com\" will generate a UID code
for each entry composed of a hash of the event data, a creation
timestamp, and your personal domain name."
  :type 'string)

(defcustom ical:vcalendar-prodid
  (format "-//gnu.org//GNU Emacs %s//EN" emacs-version)
  "The value of the `icalendar-prodid' property for VCALENDAR objects
produced by this Emacs."
  :type 'string)

(defconst ical:vcalendar-version "2.0"
  "The current version of the VCALENDAR object, used in the
`icalendar-version' property. \"2.0\" is the version corresponding to
RFC5545.")

(define-obsolete-variable-alias
  'icalendar-export-sexp-enumeration-days
  'diary-icalendar-export-sexp-enumeration-days
  "31.1")

(define-obsolete-variable-alias
  'icalendar-export-sexp-enumerate-all
  'diary-icalendar-export-sexp-enumerate-all
  "31.1")

(defcustom icalendar-export-alarms
  nil
  "Determine if and how alarms are included in exported diary events."
  :version "25.1"
  :type '(choice (const :tag "Do not include alarms in export"
                        nil)
                 (list :tag "Create alarms in exported diary entries"
                       (integer :tag "Advance time (minutes)"
                                :value 10)
                       (set :tag "Alarm type"
                            (list :tag "Audio"
                                  (const :tag "Audio" audio))
                            (list :tag "Display"
                                  (const :tag "Display" display))
                            (list :tag "Email"
                                  (const email)
                                  (repeat :tag "Attendees"
                                          (string :tag "Email")))))))

(make-obsolete-variable
 'icalendar-export-alarms
 "please use the new format in `diary-icalendar-export-alarms' instead."
 "31.1")

(defcustom icalendar-debug-level 1
  "Minimum severity for logging iCalendar error messages.
A value of 2 only logs errors.
A value of 1 also logs warnings.
A value of 0 also logs debugging information."
  :type 'integer)

(defvar icalendar-debug nil
  "Enable icalendar debug messages.")

(make-obsolete-variable
 'icalendar-debug
 'icalendar-debug-level
 "31.1")

;; ======================================================================
;; NO USER SERVICEABLE PARTS BELOW THIS LINE
;; ======================================================================

(defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"])

(make-obsolete-variable
 'icalendar--weekday-array
 'icalendar-weekday-numbers
 "31.1")

;; ======================================================================
;; all the other libs we need
;; ======================================================================
(require 'calendar)
(require 'diary-lib)

;; ======================================================================
;; misc
;; ======================================================================
(defun icalendar--dmsg (&rest args)
  "Print message ARGS if `icalendar-debug' is non-nil."
  (declare (obsolete icalendar-warn "31.1"))
  (if (or icalendar-debug (= 0 icalendar-debug-level))
      (with-current-buffer (ical:error-buffer)
        (goto-char (point-max))
        (insert (apply #'format-message args))
        (insert "\n"))))

;; ======================================================================
;; Error handling
;; ======================================================================
(define-error 'ical:error "iCalendar error")

(defconst ical:error-buffer-name "*icalendar-errors*"
  "Name of buffer in which errors are listed when processing iCalendar data.")

(defun ical:error-buffer ()
  "Return the iCalendar errors buffer, creating it if necessary.
The buffer name is based on `icalendar-error-buffer-name'."
  (get-buffer-create ical:error-buffer-name))

(defvar ical:inhibit-error-erasure nil
  "When non-nil, `icalendar-init-error-buffer' will not erase the errors
buffer.")

(defun ical:init-error-buffer (&optional err-buffer)
  "Prepare ERR-BUFFER for iCalendar errors.
ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'.
Erases ERR-BUFFER and places it in `icalendar-errors-mode'."
  (with-current-buffer (or err-buffer (ical:error-buffer))
    (unless ical:inhibit-error-erasure
      (let ((inhibit-read-only t))
        (erase-buffer)))
    (if (not (eq major-mode 'icalendar-errors-mode))
        (icalendar-errors-mode))))

(defun ical:errors-p (&optional err-buffer)
  "Return non-nil if iCalendar errors have been reported in ERR-BUFFER.
ERR-BUFFER defaults to the buffer returned by `icalendar-error-buffer'."
  (with-current-buffer (or err-buffer (ical:error-buffer))
    (not (= (point-min) (point-max)))))

(defun ical:warn (msg &rest err-plist)
  "Write a warning to the `icalendar-error-buffer' without signaling an error."
  (plist-put err-plist :message msg)
  (unless (plist-get err-plist :severity)
    (plist-put err-plist :severity 1))
  (ical:handle-generic-error `(ical:warning . ,err-plist)))

(defconst ical:error-regexp
  (rx line-start
      (zero-or-one
       (group "("
              (or (group-n 3 "ERROR") (group-n 4 "WARNING") (group-n 5 "INFO"))
              ")"))
      (group-n 1 (zero-or-one " *UNFOLDED:") (zero-or-more (not ":"))) ":"
      (zero-or-one (group-n 2 (one-or-more digit)))
      ":")
  "Regexp to match iCalendar errors.

Group 1 contains the buffer name where the error originated.
Group 2 contains the buffer position.
Groups 3-5 match the severity:
  3 matches ERROR
  4 matches WARNING
  5 matches INFO")

(cl-defun ical:format-error (&rest error-plist
                             &key (message "Unknown error")
                                  severity
                                  buffer
                                  position
                                  &allow-other-keys)
  "Format iCalendar error data to a string.

MESSAGE should be a string; it defaults to \"Unknown error\".
BUFFER should be a buffer; POSITION should be a position in BUFFER.
SEVERITY can be 0 for debug information, or 1 for a warning; otherwise
a genuine error is reported.

The returned error message looks like

(LEVEL)BUFFER:POSITION: MESSAGE
DEBUG-INFO...

where LEVEL is derived from SEVERITY.  DEBUG-INFO contains any additional
data in ERROR-PLIST, if `icalendar-debug-level' is
0. `icalendar-error-regexp' matches the fields in such messages."
  (let ((name (copy-sequence (buffer-name buffer)))
        (pos (if (integer-or-marker-p position)
                 (format "%d" position)
               ""))
        (level (cond ((eq severity 0) "INFO")
                     ((eq severity 1) "WARNING")
                     (t "ERROR")))
        (debug-info (if (not (= 0 icalendar-debug-level))
                        ""
                      (mapconcat ;; (:key val...) => "Key: val\n..."
                       (lambda (val)
                         (if (keywordp val)
                             (capitalize (substring (symbol-name val) 1))
                           (format ": %s\n" val)))
                       error-plist))))
    ;; Make sure buffer name doesn't take too much space:
    (when (< 8 (length name))
      (if (equal " *UNFOLDED:" (substring name 0 11))
          (put-text-property 0 11 'display "..." name)
        (put-text-property 9 (length name) 'display "..." name)))
    (format "(%s)%s:%s: %s\n%s" level name pos message debug-info)))

(defun ical:handle-generic-error (err-data &optional err-buffer)
  "Log error data to ERR-BUFFER (default: the iCalendar error buffer).
ERR-DATA should be a list (ERROR-SYMBOL . SIGNAL-DATA) where
SIGNAL-DATA is a plist of error data."
  (let* ((signal-data (cdr err-data))
         (err-plist (when (plistp signal-data) signal-data))
         (err-symbol (car err-data))
         (severity (or (plist-get err-plist :severity) 2))
         (buf (current-buffer)))
    (when (<= ical:debug-level severity)
      (with-current-buffer (or err-buffer (ical:error-buffer))
        (goto-char (point-max))
        (let ((inhibit-read-only t))
          (unless (bolp) (insert "\n"))
          (insert (apply #'ical:format-error
                         (or err-plist
                             (list :buffer buf
                                   :message
                                   (format "Unhandled %s error: %s"
                                           err-symbol signal-data))))))))))

(defmacro ical:condition-case (var bodyform &rest handlers)
  "Like `condition-case', but with default handler for unhandled iCalendar errors.
If none of HANDLERS handles an error, it will be handled by
`icalendar-handle-generic-error'."
  `(condition-case ,var
       ,bodyform
     ,@handlers
     (ical:error (ical:handle-generic-error ,var))))

;;; Mode based on compilation-mode for navigating error buffer:
(defun ical:-buffer-from-error ()
  (when-let* ((name (match-string 1)))
    (or (get-buffer name)
        (find-buffer-visiting name))))

(defun ical:-filename-from-error ()
  (when-let* ((buf (ical:-buffer-from-error)))
    (buffer-file-name buf)))

(defun ical:-lineno-from-error ()
  (when-let* ((buf (ical:-buffer-from-error))
              (posstr (match-string 2))
              (pos (string-to-number posstr)))
    (with-current-buffer buf
      (line-number-at-pos pos))))

(defconst ical:error-regexp-alist
  (list (list icalendar-error-regexp
              #'ical:-filename-from-error
              #'ical:-lineno-from-error
              nil
              nil
              nil
              '(2 compilation-line-face)
              '(3 compilation-error-face)
              '(4 compilation-warning-face)
              '(5 compilation-info-face)))
  "Specifies how errors are parsed in `icalendar-errors-mode';
see `compilation-error-regexp-alist'.")

(define-compilation-mode ical:errors-mode "iCalendar Errors"
  "Mode for listing and visiting errors when processing iCalendar data."
  (setq-local compilation-error-regexp-alist ical:error-regexp-alist))

;; ======================================================================
;; UIDs
;; ======================================================================
(defvar ical:-uid-count 0
  "Internal counter for creating unique ids.")

(defun ical:make-uid (&optional contents _)
  "Construct a unique ID from `icalendar-uid-format'.

CONTENTS can be any object which represents the contents of the
iCalendar component for which the UID is generated.  If CONTENTS is a
string with the text property \\='uid, that property's value will be
used as the returned UID.

Otherwise, CONTENTS will be used to create the hash substituted for
\\='%h' in `icalendar-uid-format'.  If CONTENTS is not given, the hash
will be based on an internal counter, the system name, and the current
time in nanoseconds.

The second optional argument is for backward compatibility and is ignored."
  (cl-incf icalendar--uid-count)
  (let* ((uid icalendar-uid-format)
         (timestamp (format-time-string "%s%N"))
         (tohash (or contents
                     (format "%d%s%s" ical:-uid-count (system-name) timestamp))))
    (if (and (stringp contents) (get-text-property 0 'uid contents))
        ;; "Allow other apps (such as org-mode) to create its own uid"
        ;; FIXME: is this necessary?  If caller already has a UID, why
        ;; call this function at all?
	(setq uid (get-text-property 0 'uid contents))
      (progn
        (setq uid (replace-regexp-in-string
                   "%c" (format "%d" icalendar--uid-count) uid t t))
        (setq uid (replace-regexp-in-string
                   "%t" timestamp uid t t))
        (setq uid (replace-regexp-in-string
                   "%h" (format "%d" (abs (sxhash tohash))) uid t t))
        (setq uid (replace-regexp-in-string
                   "%u" (or user-login-name "UNKNOWN_USER") uid t t))
        ;; `%s' no longer used, but allowed for backward compatibility:
        (setq uid (replace-regexp-in-string "%s" "" uid t t))))
    uid))


;; Essentially everything beyond this point is obsoleted by the new
;; implementation in diary-icalendar.el.  Since the functions below call
;; each other and they still need to live in this file for now (see
;; Bug#74994), prevent byte compiler warnings when compiling this file:
(with-suppressed-warnings
    ((obsolete icalendar-import-format
               icalendar-import-format-summary
               icalendar-import-format-description
               icalendar-import-format-location
               icalendar-import-format-organizer
               icalendar-import-format-url
               icalendar-import-format-uid
               icalendar-import-format-status
               icalendar-import-format-class
               icalendar-recurring-start-year
               icalendar-export-hidden-diary-entries
               icalendar-export-sexp-enumeration-days
               icalendar-export-sexp-enumerate-all
               icalendar-export-alarms
               icalendar-debug nil
               icalendar--weekday-array
               icalendar--dmsg
               icalendar--get-unfolded-buffer
               icalendar--clean-up-line-endings
               icalendar--rris
               icalendar--read-element
               icalendar--get-event-property
               icalendar--get-event-property-attributes
               icalendar--get-event-properties
               icalendar--get-children
               icalendar--all-events
               icalendar--split-value
               icalendar--convert-tz-offset
               icalendar--parse-vtimezone
               icalendar--get-most-recent-observance
               icalendar--convert-all-timezones
               icalendar--find-time-zone
               icalendar--decode-isodatetime
               icalendar--decode-isoduration
               icalendar--add-decoded-times
               icalendar--datetime-to-american-date
               icalendar--datetime-to-european-date
               icalendar--datetime-to-iso-date
               icalendar--datetime-to-diary-date
               icalendar--datetime-to-colontime
               icalendar--get-month-number
               icalendar--get-weekday-number
               icalendar--get-weekday-numbers
               icalendar--get-weekday-abbrev
               icalendar--date-to-isodate
               icalendar--datestring-to-isodate
               icalendar--diarytime-to-isotime
               icalendar--convert-string-for-export
               icalendar--convert-string-for-import
               icalendar-export-file
               icalendar-export-region
               icalendar--create-uid
               icalendar--convert-to-ical
               icalendar--parse-summary-and-rest
               icalendar--create-ical-alarm
               icalendar--do-create-ical-alarm
               icalendar--convert-ordinary-to-ical
               icalendar-first-weekday-of-year
               icalendar--convert-weekly-to-ical
               icalendar--convert-yearly-to-ical
               icalendar--convert-sexp-to-ical
               icalendar--convert-block-to-ical
               icalendar--convert-float-to-ical
               icalendar--convert-date-to-ical
               icalendar--convert-cyclic-to-ical
               icalendar--convert-anniversary-to-ical
               icalendar-import-file
               icalendar-import-buffer
               icalendar--format-ical-event
               icalendar--convert-ical-to-diary
               icalendar--convert-recurring-to-diary
               icalendar--convert-non-recurring-all-day-to-diary
               icalendar--convert-non-recurring-not-all-day-to-diary
               icalendar--add-diary-entry
               icalendar-import-format-sample
               icalendar-version))

;; ======================================================================
;; Core functionality
;; Functions for parsing icalendars, importing and so on
;; ======================================================================

(defun icalendar--get-unfolded-buffer (folded-ical-buffer)
  "Return a new buffer containing the unfolded contents of a buffer.
Folding is the iCalendar way of wrapping long lines.  In the
created buffer all occurrences of CR LF BLANK are replaced by the
empty string.  Argument FOLDED-ICAL-BUFFER is the folded input
buffer."
  (declare (obsolete icalendar-unfolded-buffer-from-buffer "31.1"))
  (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
    (save-current-buffer
      (set-buffer unfolded-buffer)
      (erase-buffer)
      (insert-buffer-substring folded-ical-buffer)
      (icalendar--clean-up-line-endings)
      (goto-char (point-min))
      (while (re-search-forward "\r?\n[ \t]" nil t)
        (replace-match "" nil nil)))
    unfolded-buffer))

(defun icalendar--clean-up-line-endings ()
  "Replace DOS- and MAC-like line endings with unix line endings.
All occurrences of (CR LF) and (LF CF) are replaced with LF in
the current buffer.  This is necessary in buffers which contain a
mix of different line endings."
  (declare (obsolete nil "31.1"))
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward "\r\n\\|\n\r" nil t)
      (replace-match "\n" nil nil))))

(define-obsolete-function-alias 'icalendar--rris
  #'replace-regexp-in-string "27.1")

(defun icalendar--read-element (invalue inparams)
  "Recursively read the next iCalendar element in the current buffer.
INVALUE gives the current iCalendar element we are reading.
INPARAMS gives the current parameters.....
This function calls itself recursively for each nested calendar element
it finds.  The current buffer should be an unfolded buffer as returned
from `icalendar--get-unfolded-buffer'."
  (declare (obsolete "use `icalendar-parse' or one of `icalendar-parse-component',
`icalendar-parse-property', `icalendar-parse-params' instead." "31.1"))
  (let (element children line name params param param-name param-value
                value
                (continue t))
    (setq children '())
    (while (and continue
                (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t))
      (setq name (intern (match-string 1)))
      (backward-char 1)
      (setq params '())
      (setq line '())
      (while (looking-at ";")
        (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil)
        (setq param-name (intern (match-string 1)))
        (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
                           nil t)
        (backward-char 1)
        (setq param-value (or (match-string 2) (match-string 3)))
        (setq param (list param-name param-value))
        (while (looking-at ",")
          (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
                             nil t)
          (if (match-string 2)
              (setq param-value (match-string 2))
            (setq param-value (match-string 3)))
          (setq param (append param param-value)))
        (setq params (append params param)))
      (unless (looking-at ":")
        (error "Oops"))
      (forward-char 1)
      (let ((start (point)))
        (end-of-line)
        (setq value (buffer-substring start (point))))
      (setq line (list name params value))
      (cond ((eq name 'BEGIN)
             (setq children
                   (append children
                           (list (icalendar--read-element (intern value)
                                                          params)))))
            ((eq name 'END)
             (setq continue nil))
            (t
             (setq element (append element (list line))))))
    (if invalue
        (list invalue inparams element children)
      children)))

;; ======================================================================
;; helper functions for examining events
;; ======================================================================

;;(defsubst icalendar--get-all-event-properties (event)
;;  "Return the list of properties in this EVENT."
;;  (car (cddr event)))

(defun icalendar--get-event-property (event prop)
  "For the given EVENT return the value of the first occurrence of PROP."
  (declare (obsolete icalendar-with-component "31.1"))
  (catch 'found
    (let ((props (car (cddr event))) pp)
      (while props
        (setq pp (car props))
        (if (eq (car pp) prop)
            (throw 'found (car (cddr pp))))
        (setq props (cdr props))))
    nil))

(defun icalendar--get-event-property-attributes (event prop)
  "For the given EVENT return attributes of the first occurrence of PROP."
  (declare (obsolete icalendar-with-component "31.1"))
  (catch 'found
    (let ((props (car (cddr event))) pp)
      (while props
        (setq pp (car props))
        (if (eq (car pp) prop)
            (throw 'found (cadr pp)))
        (setq props (cdr props))))
    nil))

(defun icalendar--get-event-properties (event prop)
  "For the given EVENT return a list of all values of the property PROP."
  (declare (obsolete icalendar-with-component "31.1"))
  (let ((props (car (cddr event))) pp result)
    (while props
      (setq pp (car props))
      (if (eq (car pp) prop)
          (setq result (append (split-string (car (cddr pp)) ",") result)))
      (setq props (cdr props)))
    result))

;; (defun icalendar--set-event-property (event prop new-value)
;;   "For the given EVENT set the property PROP to the value NEW-VALUE."
;;   (catch 'found
;;     (let ((props (car (cddr event))) pp)
;;       (while props
;;         (setq pp (car props))
;;         (when (eq (car pp) prop)
;;           (setcdr (cdr pp) new-value)
;;           (throw 'found (car (cddr pp))))
;;         (setq props (cdr props)))
;;       (setq props (car (cddr event)))
;;       (setcar (cddr event)
;;               (append props (list (list prop nil new-value)))))))

(defun icalendar--get-children (node name)
  "Return all children of the given NODE which have a name NAME.
For instance the VCALENDAR node can have VEVENT children as well as VTODO
children."
  (declare (obsolete icalendar-ast-node-children "31.1"))
  (let ((result nil)
        (children (cadr (cddr node))))
    (when (eq (car node) name)
      (setq result node))
    ;;(message "%s" node)
    (when children
      (let ((subresult
             (delq nil
                   (mapcar (lambda (n)
                             (icalendar--get-children n name))
                           children))))
        (if subresult
            (if result
                (setq result (append result subresult))
              (setq result subresult)))))
    result))

;; private
(defun icalendar--all-events (icalendar)
  "Return the list of all existing events in the given ICALENDAR."
  (declare (obsolete icalendar-with-component "31.1"))
  (let ((result '()))
    (mapc (lambda (elt)
	    (setq result (append (icalendar--get-children elt 'VEVENT)
                                 result)))
	  (nreverse icalendar))
    result))

(defun icalendar--split-value (value-string)
  "Split VALUE-STRING at `;='."
  (declare (obsolete nil "31.1"))
  (let ((result '())
        param-name param-value)
    (when value-string
      (save-current-buffer
        (set-buffer (get-buffer-create " *icalendar-work*"))
        (set-buffer-modified-p nil)
        (erase-buffer)
        (insert value-string)
        (goto-char (point-min))
        (while
            (re-search-forward
             "\\([A-Za-z0-9-]+\\)=\\(\\([^;:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
             nil t)
          (setq param-name (intern (match-string 1)))
          (setq param-value (match-string 2))
          (setq result
                (append result (list (list param-name param-value)))))))
    result))

(defun icalendar--convert-tz-offset (alist dst-p)
  "Return a cons of two strings representing a timezone start.
ALIST is an alist entry from a VTIMEZONE, like STANDARD.
DST-P is non-nil if this is for daylight savings time.
The strings are suitable for assembling into a TZ variable."
  (declare (obsolete nil "31.1"))
  (let* ((offsetto (car (cddr (assq 'TZOFFSETTO alist))))
	 (offsetfrom (car (cddr (assq 'TZOFFSETFROM alist))))
	 (rrule-value (car (cddr (assq 'RRULE alist))))
         (rdate-p (and (assq 'RDATE alist) t))
	 (dtstart (car (cddr (assq 'DTSTART alist))))
	 (no-dst (or rdate-p (equal offsetto offsetfrom))))
    ;; FIXME: the presence of an RDATE is assumed to denote the first day of the year
    (when (and offsetto dtstart (or rrule-value no-dst))
      (let* ((rrule (icalendar--split-value rrule-value))
	     (freq (cadr (assq 'FREQ rrule)))
	     (bymonth (cadr (assq 'BYMONTH rrule)))
	     (byday (cadr (assq 'BYDAY rrule))))
	;; FIXME: we don't correctly handle WKST here.
	(if (or no-dst (and (string= freq "YEARLY") bymonth))
	    (cons
	     (concat
	      ;; Fake a name.
	      (if dst-p "DST" "STD")
	      ;; For TZ, OFFSET is added to the local time.  So,
	      ;; invert the values.
	      (if (eq (aref offsetto 0) ?-) "+" "-")
	      (substring offsetto 1 3)
	      ":"
	      (substring offsetto 3 5))
	     ;; The start time.
             (let* ((day (if no-dst
                             1
                           (icalendar--get-weekday-number (substring byday -2))))
                    (week (if no-dst
                              "1"
                            (if (eq day -1)
				byday
			      (substring byday 0 -2)))))
		 ;; "Translate" the iCalendar way to specify the last
		 ;; (sun|mon|...)day in month to the tzset way.
		 (if (string= week "-1")  ; last day as iCalendar calls it
		     (setq week "5"))     ; last day as tzset calls it
                 (when no-dst (setq bymonth "1"))
		 (concat "M" bymonth "." week "." (if (eq day -1) "0"
						    (int-to-string day))
			 ;; Start time.
			 "/"
			 (substring dtstart -6 -4)
			 ":"
			 (substring dtstart -4 -2)
			 ":"
			 (substring dtstart -2)))))))))

(defun icalendar--parse-vtimezone (alist)
  "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
Consider only the most recent date specification.
Return nil if timezone cannot be parsed."
  (declare (obsolete nil "31.1"))
  (let* ((tz-id (icalendar--convert-string-for-import
                 (icalendar--get-event-property alist 'TZID)))
	 (daylight (cadr (cdar (icalendar--get-most-recent-observance alist 'DAYLIGHT))))
	 (day (and daylight (icalendar--convert-tz-offset daylight t)))
	 (standard (cadr (cdar (icalendar--get-most-recent-observance alist 'STANDARD))))
	 (std (and standard (icalendar--convert-tz-offset standard nil))))
    (if (and tz-id std)
	(cons tz-id
	      (if day
		  (concat (car std) (car day)
			  "," (cdr day) "," (cdr std))
		(car std))))))

(defun icalendar--get-most-recent-observance (alist sub-comp)
  "Return the latest observance for SUB-COMP DAYLIGHT or STANDARD.
ALIST is a VTIMEZONE potentially containing historical records."
;FIXME?: "most recent" should be relative to a given date
  (declare (obsolete icalendar-recur-tz-observance-on "31.1"))
  (let ((components (icalendar--get-children alist sub-comp)))
    (list
     (car
      (sort components
            (lambda (a b)
              (let* ((get-recent (lambda (n)
                                   (car
                                    (sort
                                     (delq nil
                                           (mapcar (lambda (p)
                                                     (and (memq (car p) '(DTSTART RDATE))
                                                          (car (cddr p))))
                                                   n))
                                     #'string-greaterp))))
                     (a-recent (funcall get-recent (car (cddr a))))
                     (b-recent (funcall get-recent (car (cddr b)))))
                (string-greaterp a-recent b-recent))))))))

(defun icalendar--convert-all-timezones (icalendar)
  "Convert all timezones in the ICALENDAR into an alist.
Each element of the alist is a cons (ID . TZ-STRING),
like `icalendar--parse-vtimezone'."
  (declare (obsolete nil "31.1"))
  (let (result)
    (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE))
      (setq zone (icalendar--parse-vtimezone zone))
      (if zone
	  (setq result (cons zone result))))
    result))

(defun icalendar--find-time-zone (prop-list zone-map)
  "Return a timezone string for the time zone in PROP-LIST, or nil if none.
ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
  (declare (obsolete nil "31.1"))
  (let ((id (plist-get prop-list 'TZID)))
    (if id
	(cdr (assoc id zone-map)))))

(defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
                                                        source-zone
                                                        result-zone)
  "Return ISODATETIMESTRING in format like `decode-time'.
Converts from ISO-8601 to Emacs representation.  If
ISODATETIMESTRING specifies UTC time (trailing letter Z) the
decoded time is given in the local time zone!  If optional
parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
days.
SOURCE-ZONE, if provided, is the timezone for decoding the time,
in any format understood by `encode-time'.
RESULT-ZONE, if provided, is the timezone for encoding the result
in any format understood by `decode-time'.
FIXME: multiple comma-separated values should be allowed!"
  (declare (obsolete icalendar-read-date-time "31.1"))
  (icalendar--dmsg isodatetimestring)
  (if isodatetimestring
      ;; day/month/year must be present
      (let ((year  (read (substring isodatetimestring 0 4)))
            (month (read (substring isodatetimestring 4 6)))
            (day   (read (substring isodatetimestring 6 8)))
            (hour 0)
            (minute 0)
            (second 0))
        (when (> (length isodatetimestring) 12)
          ;; hour/minute present
          (setq hour (read (substring isodatetimestring 9 11)))
          (setq minute (read (substring isodatetimestring 11 13))))
        (when (> (length isodatetimestring) 14)
          ;; seconds present
          (setq second (read (substring isodatetimestring 13 15))))
	;; FIXME: Support subseconds.
        (when (> (length isodatetimestring) 15)
	  (pcase (aref isodatetimestring 15)
            (?Z
             (setq source-zone t))
	    ((or ?- ?+)
             (setq source-zone
                   (concat "UTC" (substring isodatetimestring 15))))))
        ;; shift if necessary
        (if day-shift
            (let ((mdy (calendar-gregorian-from-absolute
                        (+ (calendar-absolute-from-gregorian
                            (list month day year))
                           day-shift))))
              (setq month (nth 0 mdy))
              (setq day   (nth 1 mdy))
              (setq year  (nth 2 mdy))))
        ;; create the decoded date-time
        ;; FIXME!?!
	(let ((decoded-time (list second minute hour day month year
				  nil -1 source-zone)))
	  (condition-case nil
	      (decode-time (encode-time decoded-time) result-zone)
	    (error
	     (message "Cannot decode \"%s\"" isodatetimestring)
	     ;; Hope for the best....
	     decoded-time))))
    ;; isodatetimestring == nil
    nil))

(defun icalendar--decode-isoduration (isodurationstring
                                      &optional duration-correction)
  "Convert ISODURATIONSTRING into format provided by `decode-time'.
Converts from ISO-8601 to Emacs representation.  If ISODURATIONSTRING
specifies UTC time (trailing letter Z) the decoded time is given in
the local time zone!

Optional argument DURATION-CORRECTION shortens result by one day.

FIXME: TZID-attributes are ignored....!
FIXME: multiple comma-separated values should be allowed!"
  (declare (obsolete icalendar-read-dur-value "31.1"))
  (if isodurationstring
      (save-match-data
        (string-match
         (concat
          "^P[+-]?\\("
          "\\(\\([0-9]+\\)D\\)"         ; days only
          "\\|"
          "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
          "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)"  ; mand. time
          "\\|"
          "\\(\\([0-9]+\\)W\\)"         ; weeks only
          "\\)$") isodurationstring)
        (let ((seconds 0)
              (minutes 0)
              (hours 0)
              (days 0)
              (months 0)
              (years 0))
          (cond
           ((match-beginning 2)         ;days only
            (setq days (read (substring isodurationstring
                                        (match-beginning 3)
                                        (match-end 3))))
            (when duration-correction
              (setq days (1- days))))
           ((match-beginning 4)         ;days and time
            (if (match-beginning 5)
                (setq days (read (substring isodurationstring
                                            (match-beginning 6)
                                            (match-end 6)))))
            (if (match-beginning 7)
                (setq hours (read (substring isodurationstring
                                             (match-beginning 8)
                                             (match-end 8)))))
            (if (match-beginning 9)
                (setq minutes (read (substring isodurationstring
                                               (match-beginning 10)
                                               (match-end 10)))))
	    ;; FIXME: Support subseconds.
            (if (match-beginning 11)
                (setq seconds (read (substring isodurationstring
                                               (match-beginning 12)
                                               (match-end 12))))))
           ((match-beginning 13)        ;weeks only
            (setq days (* 7 (read (substring isodurationstring
                                             (match-beginning 14)
                                             (match-end 14)))))))
          (list seconds minutes hours days months years)))
    ;; isodatetimestring == nil
    nil))

(defun icalendar--add-decoded-times (time1 time2)
  "Add TIME1 to TIME2.
Both times must be given in decoded form.  One of these times must be
valid (year > 1900 or something)."
  (declare (obsolete icalendar-date-time-add "31.1"))
  ;; FIXME: does this function exist already?  Can we use decoded-time-add?
  (decode-time (encode-time
		;; FIXME: Support subseconds.
		(time-convert (time-add (decoded-time-second time1)
					(decoded-time-second time2))
			      'integer)
                (+ (decoded-time-minute time1) (decoded-time-minute time2))
                (+ (decoded-time-hour time1) (decoded-time-hour time2))
                (+ (decoded-time-day time1) (decoded-time-day time2))
                (+ (decoded-time-month time1) (decoded-time-month time2))
                (+ (decoded-time-year time1) (decoded-time-year time2))
                nil
                nil
                ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
                )))

(defun icalendar--datetime-to-american-date (datetime &optional separator)
  "Convert the decoded DATETIME to American-style format.
Optional argument SEPARATOR gives the separator between month,
day, and year.  If nil a blank character is used as separator.
American format: \"month day year\"."
  (declare (obsolete "use `icalendar-date/time-to-date' and
`diary-icalendar-format-date' instead." "31.1"))
  (if datetime
      (format "%d%s%d%s%d" (nth 4 datetime) ;month
              (or separator " ")
              (nth 3 datetime)          ;day
              (or separator " ")
              (nth 5 datetime))         ;year
    ;; datetime == nil
    nil))

(defun icalendar--datetime-to-european-date (datetime &optional separator)
  "Convert the decoded DATETIME to European format.
Optional argument SEPARATOR gives the separator between month,
day, and year.  If nil a blank character is used as separator.
European format: (day month year).
FIXME"
  (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
  (if datetime
      (format "%d%s%d%s%d" (nth 3 datetime) ;day
              (or separator " ")
              (nth 4 datetime)            ;month
              (or separator " ")
              (nth 5 datetime))           ;year
    ;; datetime == nil
    nil))

(defun icalendar--datetime-to-iso-date (datetime &optional separator)
  "Convert the decoded DATETIME to ISO format.
Optional argument SEPARATOR gives the separator between month,
day, and year.  If nil a blank character is used as separator.
ISO format: (year month day)."
  (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
  (if datetime
      (format "%d%s%d%s%d" (nth 5 datetime) ;year
              (or separator " ")
              (nth 4 datetime)            ;month
              (or separator " ")
              (nth 3 datetime))           ;day
    ;; datetime == nil
    nil))

(defun icalendar--datetime-to-diary-date (datetime &optional separator)
  "Convert the decoded DATETIME to diary format.
Optional argument SEPARATOR gives the separator between month,
day, and year.  If nil a blank character is used as separator.
Call icalendar--datetime-to-*-date according to the current
calendar date style."
  (declare (obsolete "use `icalendar-date/time-to-date' and `diary-icalendar-format-date' instead." "31.1"))
  (funcall (intern-soft (format "icalendar--datetime-to-%s-date"
                                calendar-date-style))
           datetime separator))

(defun icalendar--datetime-to-colontime (datetime)
  "Extract the time part of a decoded DATETIME into 24-hour format.
Note that this silently ignores seconds."
  (declare (obsolete diary-icalendar-format-time "31.1"))
  (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))

(defun icalendar--get-month-number (monthname)
  "Return the month number for the given MONTHNAME."
  (declare (obsolete nil "31.1"))
  (catch 'found
    (let ((num 1)
          (m (downcase monthname)))
      (mapc (lambda (month)
              (let ((mm (downcase month)))
                (if (or (string-equal mm m)
                        (string-equal (substring mm 0 3) m))
                    (throw 'found num))
                (setq num (1+ num))))
            calendar-month-name-array))
    ;; Error:
    -1))

(defun icalendar--get-weekday-number (abbrevweekday)
  "Return the number for the ABBREVWEEKDAY."
  (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
  (if abbrevweekday
      (catch 'found
        (let ((num 0)
              (aw (downcase abbrevweekday)))
          (mapc (lambda (day)
                  (let ((d (downcase day)))
                    (if (string-equal d aw)
                        (throw 'found num))
                    (setq num (1+ num))))
                icalendar--weekday-array)))
    ;; Error:
    -1))

(defun icalendar--get-weekday-numbers (abbrevweekdays)
  "Return the list of numbers for the comma-separated ABBREVWEEKDAYS."
  (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
  (when abbrevweekdays
    (let* ((num -1)
           (weekday-alist (mapcar (lambda (day)
                                    (progn
                                      (setq num (1+ num))
                                      (cons (downcase day) num)))
                                  icalendar--weekday-array)))
      (delq nil
            (mapcar (lambda (abbrevday)
                      (cdr (assoc abbrevday weekday-alist)))
                    (split-string (downcase abbrevweekdays) ","))))))

(defun icalendar--get-weekday-abbrev (weekday)
  "Return the abbreviated WEEKDAY."
  (declare (obsolete "see `icalendar-weekday-numbers'" "31.1"))
  (catch 'found
    (let ((num 0)
          (w (downcase weekday)))
      (mapc (lambda (day)
              (let ((d (downcase day)))
                (if (or (string-equal d w)
                        (string-equal (substring d 0 3) w))
                    (throw 'found (aref icalendar--weekday-array num)))
                (setq num (1+ num))))
            calendar-day-name-array))
    ;; Error:
    nil))

(defun icalendar--date-to-isodate (date &optional day-shift)
  "Convert DATE to iso-style date.
DATE must be a list of the form (month day year).
If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
  (declare (obsolete icalendar-print-date "31.1"))
  (let ((mdy (calendar-gregorian-from-absolute
              (+ (calendar-absolute-from-gregorian date)
                 (or day-shift 0)))))
    (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy))))


(defun icalendar--datestring-to-isodate (datestring &optional day-shift year-shift)
  "Convert diary-style DATESTRING to iso-style date.
If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
-- DAY-SHIFT must be either nil or an integer.  If YEAR-SHIFT is
non-nil, the result is shifted by YEAR-SHIFT years -- YEAR-SHIFT
must be either nil or an integer.  This function tries to figure
the date style from DATESTRING itself.  If that is not possible
it uses the current calendar date style."
  (declare (obsolete "use `diary-icalendar-parse-date-form' and `icalendar-print-date' instead." "31.1"))
  (let ((day -1) month year)
    (save-match-data
      (cond ( ;; iso-style numeric date
             (string-match (concat "\\s-*"
                                   "\\([0-9]\\{4\\}\\)[ \t/-]\\s-*"
                                   "0?\\([1-9][0-9]?\\)[ \t/-]\\s-*"
                                   "0?\\([1-9][0-9]?\\)")
                           datestring)
             (setq year (read (substring datestring (match-beginning 1)
                                            (match-end 1))))
             (setq month (read (substring datestring (match-beginning 2)
                                          (match-end 2))))
             (setq day (read (substring datestring (match-beginning 3)
                                        (match-end 3)))))
            ( ;; non-iso numeric date -- must rely on configured
              ;; calendar style
             (string-match (concat "\\s-*"
                                   "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
                                   "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
                                   "\\([0-9]\\{4\\}\\)")
                           datestring)
             (setq day (read (substring datestring (match-beginning 1)
                                        (match-end 1))))
             (setq month (read (substring datestring (match-beginning 2)
                                          (match-end 2))))
             (setq year (read (substring datestring (match-beginning 3)
                                         (match-end 3))))
             (if (eq calendar-date-style 'american)
                 (let ((x month))
                   (setq month day)
                   (setq day x))))
            ( ;; date contains month names -- iso style
             (string-match (concat "\\s-*"
                                   "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
                                   "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
                                   "0?\\([123]?[0-9]\\)")
                           datestring)
             (setq year (read (substring datestring (match-beginning 1)
                                        (match-end 1))))
             (setq month (icalendar--get-month-number
                          (substring datestring (match-beginning 2)
                                     (match-end 2))))
             (setq day (read (substring datestring (match-beginning 3)
                                        (match-end 3)))))
            ( ;; date contains month names -- european style
             (string-match (concat "\\s-*"
                                   "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
                                   "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
                                   "\\([0-9]\\{4\\}\\)")
                           datestring)
             (setq day (read (substring datestring (match-beginning 1)
                                        (match-end 1))))
             (setq month (icalendar--get-month-number
                          (substring datestring (match-beginning 2)
                                     (match-end 2))))
             (setq year (read (substring datestring (match-beginning 3)
                                         (match-end 3)))))
            ( ;; date contains month names -- american style
             (string-match (concat "\\s-*"
                                   "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
                                   "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
                                   "\\([0-9]\\{4\\}\\)")
                           datestring)
             (setq day (read (substring datestring (match-beginning 2)
                                        (match-end 2))))
             (setq month (icalendar--get-month-number
                          (substring datestring (match-beginning 1)
                                     (match-end 1))))
             (setq year (read (substring datestring (match-beginning 3)
                                         (match-end 3)))))
            (t
             nil)))
    (when year-shift
      (setq year (+ year year-shift)))

    (if (> day 0)
        (let ((mdy (calendar-gregorian-from-absolute
                    (+ (calendar-absolute-from-gregorian (list month day
                                                               year))
                       (or day-shift 0)))))
          (icalendar--dmsg (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
          (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
      nil)))

(defun icalendar--diarytime-to-isotime (timestring ampmstring)
  "Convert a time like 9:30pm to an iso-conform string like T213000.
In this example the TIMESTRING would be \"9:30\" and the
AMPMSTRING would be \"pm\".  The minutes may be missing as long
as the colon is missing as well, i.e. \"9\" is allowed as
TIMESTRING and has the same result as \"9:00\"."
  (declare (obsolete "use `diary-icalendar-parse-time' and `icalendar-print-date-time' instead." "31.1"))
  (if timestring
      (let* ((parts (save-match-data (split-string timestring ":")))
             (h (car parts))
             (m (if (cdr parts) (cadr parts)
                  (if (> (length h) 2) "" "00")))
             (starttimenum (read (concat h m))))
        ;; take care of am/pm style
        ;; Be sure *not* to convert 12:00pm - 12:59pm to 2400-2459
        (if (and ampmstring (string= "pm" ampmstring) (< starttimenum 1200))
            (setq starttimenum (+ starttimenum 1200)))
	;; Similar effect with 12:00am - 12:59am (need to convert to 0000-0059)
        (if (and ampmstring (string= "am" ampmstring) (>= starttimenum 1200))
            (setq starttimenum (- starttimenum 1200)))
        (format "T%04d00" starttimenum))
    nil))

(defun icalendar--convert-string-for-export (string)
  "Escape comma and other critical characters in STRING."
  (string-replace "," "\\," string))

(defun icalendar--convert-string-for-import (string)
  "Remove escape chars for comma, semicolon etc. from STRING."
  (string-replace
   "\\n" "\n " (string-replace
                "\\\"" "\"" (string-replace
                             "\\;" ";" (string-replace
                                        "\\," "," string)))))

;; ======================================================================
;; Export -- convert emacs-diary to iCalendar
;; ======================================================================

;;;###autoload
(defun icalendar-export-file (diary-filename ical-filename)
  "Export diary file to iCalendar format.
All diary entries in the file DIARY-FILENAME are converted to iCalendar
format.  The result is appended to the file ICAL-FILENAME."
  (declare (obsolete diary-icalendar-export-file "31.1"))
  (interactive "FExport diary data from file: \n\
Finto iCalendar file: ")
  (save-current-buffer
    (set-buffer (find-file diary-filename))
    (icalendar-export-region (point-min) (point-max) ical-filename)))

(defun icalendar--create-uid (entry-full contents)
  "Construct a unique iCalendar UID for a diary entry.
ENTRY-FULL is the full diary entry string.  CONTENTS is the
current iCalendar object, as a string.  Increase
`icalendar--uid-count'.  Returns the UID string."
  (declare (obsolete icalendar-make-uid "31.1"))
  (let ((uid icalendar-uid-format))
    (if
	;; Allow other apps (such as org-mode) to create its own uid
	(get-text-property 0 'uid entry-full)
	(setq uid (get-text-property 0 'uid entry-full))
      (setq uid (replace-regexp-in-string
                 "%c"
                 (format "%d" icalendar--uid-count)
                 uid t t))
      (setq icalendar--uid-count (1+ icalendar--uid-count))
      (setq uid (replace-regexp-in-string
                 "%t"
                 (format-time-string "%s%N")
                 uid t t))
      (setq uid (replace-regexp-in-string
                 "%h"
                 (format "%d" (abs (sxhash entry-full))) uid t t))
      (setq uid (replace-regexp-in-string
                 "%u" (or user-login-name "UNKNOWN_USER") uid t t))
      (let ((dtstart (if (string-match "^DTSTART[^:]*:\\([0-9]*\\)" contents)
                         (substring contents (match-beginning 1) (match-end 1))
                       "DTSTART")))
        (setq uid (replace-regexp-in-string "%s" dtstart uid t t))))
    ;; Return the UID string
    uid))

;;;###autoload
(defun icalendar-export-region (min max ical-filename)
  "Export region in diary file to iCalendar format.
All diary entries in the region from MIN to MAX in the current buffer are
converted to iCalendar format.  The result is appended to the file
ICAL-FILENAME.
This function attempts to return t if something goes wrong.  In this
case an error string which describes all the errors and problems is
written into the buffer `*icalendar-errors*'."
  (declare (obsolete diary-icalendar-export-region "31.1"))
  (interactive "r
FExport diary data into iCalendar file: ")
  (let ((result "")
        (entry-main "")
        (entry-rest "")
	(entry-full "")
        (header "")
        (contents)
        (alarm)
        (found-error nil)
        (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol)
                           "?"))
        (other-elements nil)
        (cns-cons-or-list nil))
    ;; prepare buffer with error messages
    (save-current-buffer
      (set-buffer (get-buffer-create "*icalendar-errors*"))
      (erase-buffer))

    ;; here we go
    (save-excursion
      (goto-char min)
      (while (re-search-forward
              ;; possibly ignore hidden entries beginning with "&"
              (if icalendar-export-hidden-diary-entries
                  "^\\([^ \t\n#].+\\)\\(\\(\n[ \t].*\\)*\\)"
                "^\\([^ \t\n&#].+\\)\\(\\(\n[ \t].*\\)*\\)")
              max t)
        (setq entry-main (match-string 1))
        (if (match-beginning 2)
            (setq entry-rest (match-string 2))
          (setq entry-rest ""))
	(setq entry-full (concat entry-main entry-rest))

        (condition-case error-val
            (progn
              (setq cns-cons-or-list
                    (icalendar--convert-to-ical nonmarker entry-main))
              (setq other-elements (icalendar--parse-summary-and-rest
				    entry-full))
              (mapc (lambda (contents-n-summary)
                      (setq contents (concat (car contents-n-summary)
                                             "\nSUMMARY:"
                                             (cdr contents-n-summary)))
                      (let ((cla (cdr (assoc 'cla other-elements)))
                            (des (cdr (assoc 'des other-elements)))
                            (loc (cdr (assoc 'loc other-elements)))
                            (org (cdr (assoc 'org other-elements)))
                            (sta (cdr (assoc 'sta other-elements)))
                            ;; (sum (cdr (assoc 'sum other-elements)))
                            (url (cdr (assoc 'url other-elements)))
                            (uid (cdr (assoc 'uid other-elements))))
                        (if cla
                            (setq contents (concat contents "\nCLASS:" cla)))
                        (if des
                            (setq contents (concat contents "\nDESCRIPTION:"
                                                   des)))
                        (if loc
                            (setq contents (concat contents "\nLOCATION:" loc)))
                        (if org
                            (setq contents (concat contents "\nORGANIZER:"
                                                   org)))
                        (if sta
                            (setq contents (concat contents "\nSTATUS:" sta)))
                        ;;(if sum
                        ;;    (setq contents (concat contents "\nSUMMARY:" sum)))
                        (if url
                            (setq contents (concat contents "\nURL:" url)))

                        (setq header (concat "\nBEGIN:VEVENT\nUID:"
                                             (or uid
                                                 (icalendar--create-uid
                                                  entry-full contents))))
                        (setq alarm (icalendar--create-ical-alarm
                                     (cdr contents-n-summary))))
                      (setq result (concat result header contents alarm
                                           "\nEND:VEVENT")))
                    (if (and (consp cns-cons-or-list)
                             (not (listp (cdr cns-cons-or-list))))
                        (list cns-cons-or-list)
                      cns-cons-or-list)))
          ;; handle errors
          (error
           (setq found-error t)
           (save-current-buffer
             (set-buffer (get-buffer-create "*icalendar-errors*"))
             (insert (format-message "Error in line %d -- %s: `%s'\n"
                                     (count-lines (point-min) (point))
                                     error-val
                                     entry-main))))))

      ;; we're done, insert everything into the file
      (save-current-buffer
        (let ((coding-system-for-write 'utf-8))
          (set-buffer (find-file ical-filename))
          (goto-char (point-max))
          (insert "BEGIN:VCALENDAR")
          (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
          (insert "\nVERSION:2.0")
          (insert result)
          (insert "\nEND:VCALENDAR\n")
          ;; save the diary file
          (save-buffer)
          (unless found-error
            (bury-buffer)))))
    found-error))

(defun icalendar--convert-to-ical (nonmarker entry-main)
  "Convert a diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (or
   (unless icalendar-export-sexp-enumerate-all
     (or
      ;; anniversaries -- %%(diary-anniversary ...)
      (icalendar--convert-anniversary-to-ical nonmarker entry-main)
      ;; cyclic events -- %%(diary-cyclic ...)
      (icalendar--convert-cyclic-to-ical nonmarker entry-main)
      ;; diary-date -- %%(diary-date ...)
      (icalendar--convert-date-to-ical nonmarker entry-main)
      ;; float events -- %%(diary-float ...)
      (icalendar--convert-float-to-ical nonmarker entry-main)
      ;; block events -- %%(diary-block ...)
      (icalendar--convert-block-to-ical nonmarker entry-main)))
   ;; other sexp diary entries
   (icalendar--convert-sexp-to-ical nonmarker entry-main)
   ;; weekly by day -- Monday 8:30 Team meeting
   (icalendar--convert-weekly-to-ical nonmarker entry-main)
   ;; yearly by day -- 1 May Tag der Arbeit
   (icalendar--convert-yearly-to-ical nonmarker entry-main)
   ;; "ordinary" events, start and end time given
   ;; 1 Feb 2003 blah
   (icalendar--convert-ordinary-to-ical nonmarker entry-main)
   ;; everything else
   ;; Oops! what's that?
   (error "Could not parse entry")))

(defun icalendar--parse-summary-and-rest (summary-and-rest)
  "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
Returns an alist."
  (declare (obsolete diary-icalendar-parse-entry "31.1"))
  (save-match-data
    (if (functionp icalendar-import-format)
        ;; can't do anything
        nil
      ;; split summary-and-rest
      (let* ((case-fold-search nil)
             (s icalendar-import-format)
             (p-cla (or (string-match "%c" icalendar-import-format) -1))
             (p-des (or (string-match "%d" icalendar-import-format) -1))
             (p-loc (or (string-match "%l" icalendar-import-format) -1))
             (p-org (or (string-match "%o" icalendar-import-format) -1))
             (p-sum (or (string-match "%s" icalendar-import-format) -1))
             (p-sta (or (string-match "%t" icalendar-import-format) -1))
             (p-url (or (string-match "%u" icalendar-import-format) -1))
             (p-uid (or (string-match "%U" icalendar-import-format) -1))
             (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) #'<))
	     (ct 0)
             pos-cla pos-des pos-loc pos-org pos-sta pos-url pos-uid) ;pos-sum
        (dotimes (i (length p-list))
	  ;; Use 'ct' to keep track of current position in list
          (cond ((and (>= p-cla 0) (= (nth i p-list) p-cla))
		 (setq ct (+ ct 1))
                 (setq pos-cla (* 2 ct)))
                ((and (>= p-des 0) (= (nth i p-list) p-des))
		 (setq ct (+ ct 1))
                 (setq pos-des (* 2 ct)))
                ((and (>= p-loc 0) (= (nth i p-list) p-loc))
		 (setq ct (+ ct 1))
                 (setq pos-loc (* 2 ct)))
                ((and (>= p-org 0) (= (nth i p-list) p-org))
		 (setq ct (+ ct 1))
                 (setq pos-org (* 2 ct)))
                ((and (>= p-sta 0) (= (nth i p-list) p-sta))
		 (setq ct (+ ct 1))
                 (setq pos-sta (* 2 ct)))
                ((and (>= p-sum 0) (= (nth i p-list) p-sum))
		 (setq ct (+ ct 1))
                 ;; (setq pos-sum (* 2 ct))
                 )
                ((and (>= p-url 0) (= (nth i p-list) p-url))
		 (setq ct (+ ct 1))
                 (setq pos-url (* 2 ct)))
                ((and (>= p-uid 0) (= (nth i p-list) p-uid))
		 (setq ct (+ ct 1))
                 (setq pos-uid (* 2 ct)))) )
        (mapc (lambda (ij)
                (setq s (replace-regexp-in-string (car ij) (cadr ij) s t t)))
              (list
               ;; summary must be first! because of %s
               (list "%s"
                     (concat "\\(" icalendar-import-format-summary "\\)??"))
               (list "%c"
                     (concat "\\(" icalendar-import-format-class "\\)??"))
               (list "%d"
                     (concat "\\(" icalendar-import-format-description "\\)??"))
               (list "%l"
                     (concat "\\(" icalendar-import-format-location "\\)??"))
               (list "%o"
                     (concat "\\(" icalendar-import-format-organizer "\\)??"))
               (list "%t"
                     (concat "\\(" icalendar-import-format-status "\\)??"))
               (list "%u"
                     (concat "\\(" icalendar-import-format-url "\\)??"))
               (list "%U"
                     (concat "\\(" icalendar-import-format-uid "\\)??"))))
	;; Need the \' regexp in order to detect multi-line items
        (setq s (concat "\\`"
                        (replace-regexp-in-string "%s" "\\([^z-a]*?\\)" s nil t)
                        "\\'"))
        (if (string-match s summary-and-rest)
            (let (cla des loc org sta url uid) ;; sum
              ;; (if (and pos-sum (match-beginning pos-sum))
              ;;     (setq sum (substring summary-and-rest
              ;;                          (match-beginning pos-sum)
              ;;                          (match-end pos-sum))))
              (if (and pos-cla (match-beginning pos-cla))
                  (setq cla (substring summary-and-rest
                                       (match-beginning pos-cla)
                                       (match-end pos-cla))))
              (if (and pos-des (match-beginning pos-des))
                  (setq des (substring summary-and-rest
                                       (match-beginning pos-des)
                                       (match-end pos-des))))
              (if (and pos-loc (match-beginning pos-loc))
                  (setq loc (substring summary-and-rest
                                       (match-beginning pos-loc)
                                       (match-end pos-loc))))
              (if (and pos-org (match-beginning pos-org))
                  (setq org (substring summary-and-rest
                                       (match-beginning pos-org)
                                       (match-end pos-org))))
              (if (and pos-sta (match-beginning pos-sta))
                  (setq sta (substring summary-and-rest
                                       (match-beginning pos-sta)
                                       (match-end pos-sta))))
              (if (and pos-url (match-beginning pos-url))
                  (setq url (substring summary-and-rest
                                       (match-beginning pos-url)
                                       (match-end pos-url))))
              (if (and pos-uid (match-beginning pos-uid))
                  (setq uid (substring summary-and-rest
                                       (match-beginning pos-uid)
                                       (match-end pos-uid))))
              (list (if cla (cons 'cla cla) nil)
                    (if des (cons 'des des) nil)
                    (if loc (cons 'loc loc) nil)
                    (if org (cons 'org org) nil)
                    (if sta (cons 'sta sta) nil)
                    ;;(if sum (cons 'sum sum) nil)
                    (if url (cons 'url url) nil)
                    (if uid (cons 'uid uid) nil))))))))

(defun icalendar--create-ical-alarm (summary)
  "Return VALARM blocks for the given SUMMARY."
  (declare (obsolete diary-icalendar-add-valarms "31.1"))
  (when icalendar-export-alarms
    (let* ((advance-time (car icalendar-export-alarms))
           (alarm-specs (cadr icalendar-export-alarms))
           (fun (lambda (spec)
                  (icalendar--do-create-ical-alarm advance-time spec summary))))
      (mapconcat fun alarm-specs ""))))

(defun icalendar--do-create-ical-alarm (advance-time alarm-spec summary)
  "Return a VALARM block.
Argument ADVANCE-TIME is a number giving the time when the alarm
fires (minutes before the respective event).  Argument ALARM-SPEC
is a list which must be one of (audio), (display) or
\(email (ADDRESS1 ...)), see `icalendar-export-alarms'.  Argument
SUMMARY is a string which contains a short description for the
alarm."
  (declare (obsolete diary-icalendar-add-valarms "31.1"))
  (let* ((action (car alarm-spec))
         (act (format "\nACTION:%s"
                      (cdr (assoc action '((audio . "AUDIO")
                                           (display . "DISPLAY")
                                           (email . "EMAIL"))))))
         (tri (format "\nTRIGGER:-PT%dM" advance-time))
         (des (if (memq action '(display email))
                  (format "\nDESCRIPTION:%s" summary)
                ""))
         (sum (if (eq action 'email)
                  (format "\nSUMMARY:%s" summary)
                ""))
         (att (if (eq action 'email)
                  (mapconcat (lambda (i)
                               (format "\nATTENDEE:MAILTO:%s" i))
                             (cadr alarm-spec) "")
                "")))

    (concat "\nBEGIN:VALARM" act tri des sum att "\nEND:VALARM")))

;; subroutines for icalendar-export-region
(defun icalendar--convert-ordinary-to-ical (nonmarker entry-main)
  "Convert \"ordinary\" diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match
       (concat nonmarker
               "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date
               "\\(\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?" ; start time
               "\\("
               "-\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?\\)?" ; end time
               "\\)?"
               "\\s-*\\(.*?\\) ?$")
       entry-main)
      (let* ((datetime (substring entry-main (match-beginning 1)
                                  (match-end 1)))
             (startisostring (icalendar--datestring-to-isodate
                              datetime))
             (endisostring (icalendar--datestring-to-isodate
                            datetime 1))
             (endisostring1)
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 3)
                                   (substring entry-main
                                              (match-beginning 3)
                                              (match-end 3))
                                 nil)
                               (if (match-beginning 5)
                                   (substring entry-main
                                              (match-beginning 5)
                                              (match-end 5))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)
                             (if (match-beginning 9)
                                 (substring entry-main
                                            (match-beginning 9)
                                            (match-end 9))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 10)
                                  (match-end 10)))))
        (icalendar--dmsg "ordinary %s" entry-main)

        (unless startisostring
          (error "Could not parse date"))

        ;; If only start-date is specified, then end-date is next day,
        ;; otherwise it is same day.
        (setq endisostring1 (if starttimestring
                                startisostring
                              endisostring))

        (when starttimestring
          (unless endtimestring
            (let ((time
                   (read (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (if (< time 230000)
                  ;; Case: ends on same day
              (setq endtimestring (format "T%06d"
                                              (+ 10000 time)))
                ;; Case: ends on next day
                (setq endtimestring (format "T%06d"
                                              (- time 230000)))
                (setq endisostring1 endisostring)) )))

        (cons (concat "\nDTSTART;"
                      (if starttimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      startisostring
                      (or starttimestring "")
                      "\nDTEND;"
                      (if endtimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      endisostring1
                      (or endtimestring ""))
              summary))
    ;; no match
    nil))

(defun icalendar-first-weekday-of-year (abbrevweekday year)
  "Find the first ABBREVWEEKDAY in a given YEAR.
Returns day number."
  (declare (obsolete icalendar-nth-weekday-in "31.1"))
  (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year)))
         (result (+ 1
                    (- (icalendar--get-weekday-number abbrevweekday)
                       day-of-week-jan01))))
    (cond ((<= result 0)
           (setq result (+ result 7)))
          ((> result 7)
           (setq result (- result 7))))
    result))

(defun icalendar--convert-weekly-to-ical (nonmarker entry-main)
  "Convert weekly diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (and (string-match (concat nonmarker
                                 "\\([a-z]+\\)\\s-+"
                                 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)"
                                 "\\([ap]m\\)?"
                                 "\\(-"
                                 "\\([0-9][0-9]?:[0-9][0-9]\\)"
                                 "\\([ap]m\\)?\\)?"
                                 "\\)?"
                                 "\\s-*\\(.*?\\) ?$")
                         entry-main)
           (icalendar--get-weekday-abbrev
            (substring entry-main (match-beginning 1)
                       (match-end 1))))
      (let* ((day (icalendar--get-weekday-abbrev
                   (substring entry-main (match-beginning 1)
                              (match-end 1))))
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 3)
                                   (substring entry-main
                                              (match-beginning 3)
                                              (match-end 3))
                                 nil)
                               (if (match-beginning 4)
                                   (substring entry-main
                                              (match-beginning 4)
                                              (match-end 4))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 6)
                                 (substring entry-main
                                            (match-beginning 6)
                                            (match-end 6))
                               nil)
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 8)
                                  (match-end 8)))))
        (icalendar--dmsg "weekly %s" entry-main)

        (when starttimestring
          (unless endtimestring
            (let ((time (read
                         (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (setq endtimestring (format "T%06d"
                                          (+ 10000 time))))))
        (cons (concat "\nDTSTART;"
                      (if starttimestring
                          "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      ;; Find the first requested weekday of the
                      ;; start year
                      (funcall 'format "%04d%02d%02d"
                               icalendar-recurring-start-year 1
                               (icalendar-first-weekday-of-year
                                day icalendar-recurring-start-year))
                      (or starttimestring "")
                      "\nDTEND;"
                      (if endtimestring
                          "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      (funcall 'format "%04d%02d%02d"
                               ;; end is non-inclusive!
                               icalendar-recurring-start-year 1
                               (+ (icalendar-first-weekday-of-year
                                   day icalendar-recurring-start-year)
                          (if endtimestring 0 1)))
                      (or endtimestring "")
                      "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
                      day)
              summary))
    ;; no match
    nil))

(defun icalendar--convert-yearly-to-ical (nonmarker entry-main)
  "Convert yearly diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker
                            (if (eq calendar-date-style 'european)
                                "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
                              "\\([a-z]+\\)\\s-+\\([0-9]+[0-9]?\\)\\s-+")
                            "\\*?\\s-*"
                            "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
                            "\\("
                            "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
                            "\\)?"
                            "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
                            )
                    entry-main)
      (let* ((daypos (if (eq calendar-date-style 'european) 1 2))
             (monpos (if (eq calendar-date-style 'european) 2 1))
             (day (read (substring entry-main
                                   (match-beginning daypos)
                                   (match-end daypos))))
             (month (icalendar--get-month-number
                     (substring entry-main
                                (match-beginning monpos)
                                (match-end monpos))))
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 4)
                                   (substring entry-main
                                              (match-beginning 4)
                                              (match-end 4))
                                 nil)
                               (if (match-beginning 5)
                                   (substring entry-main
                                              (match-beginning 5)
                                              (match-end 5))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)
                             (if (match-beginning 8)
                                 (substring entry-main
                                            (match-beginning 8)
                                            (match-end 8))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 9)
                                  (match-end 9)))))
        (icalendar--dmsg "yearly %s" entry-main)

        (when starttimestring
          (unless endtimestring
            (let ((time (read
                         (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (setq endtimestring (format "T%06d"
                                          (+ 10000 time))))))
        (cons (concat "\nDTSTART;"
                      (if starttimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      (format "1900%02d%02d" month day)
                      (or starttimestring "")
                      "\nDTEND;"
                      (if endtimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      ;; end is not included! shift by one day
                      (icalendar--date-to-isodate
                       (list month day 1900)
                       (if endtimestring 0 1))
                      (or endtimestring "")
                      "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
                      (format "%d" month)
                      ";BYMONTHDAY="
                      (format "%d" day))
              summary))
    ;; no match
    nil))

(defun icalendar--convert-sexp-to-ical (nonmarker entry-main &optional start)
  "Convert sexp diary entry to iCalendar format.
Enumerate the evaluated sexp entry for the next
`icalendar-export-sexp-enumeration-days' days.  NONMARKER is a
regular expression matching the start of non-marking entries.
ENTRY-MAIN is the first line of the diary entry.

Optional argument START determines the first day of the
enumeration, given as a Lisp time value -- used for test purposes."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (cond ((string-match (concat nonmarker
                               "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
                       entry-main)
         ;; simple sexp entry as generated by icalendar.el: strip off the
         ;; unnecessary (and)
         (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main)
         (icalendar--convert-to-ical
          nonmarker
          (concat "%%"
                  (substring entry-main (match-beginning 1) (match-end 1))
                  (substring entry-main (match-beginning 2) (match-end 2)))))
        ((string-match (concat nonmarker
                               "%%\\(([^)]+)\\)\\s-*\\(.*\\)")
                       entry-main)
         ;; regular sexp entry
         (icalendar--dmsg "diary-sexp %s" entry-main)
         (let* ((entry-main (substring entry-main 2))
                (res (read-from-string entry-main))
                (p1 (prin1-to-string (car res)))
                (p2 (substring entry-main (cdr res)))
                (now (or start (current-time))))
           (delete nil
                   (mapcar
                    (lambda (offset)
                      (let* ((day (decode-time (time-add now
							 (* 60 60 24 offset))))
                             (d (decoded-time-day day))
                             (m (decoded-time-month day))
                             (y (decoded-time-year day))
                             (se (diary-sexp-entry p1 p2 (list m d y)))
                             (see (cond ((stringp se) se)
                                        ((consp se) (cdr se))
                                        (t nil))))
                        (cond ((null see)
                               nil)
                              ((stringp see)
                               (let ((calendar-date-style 'iso))
                                 (icalendar--convert-ordinary-to-ical
                                  nonmarker (format "%4d/%02d/%02d %s" y m d see))))
                             (;TODO:
                              (error "Unsupported Sexp-entry: %s"
                                     entry-main)))))
                    (number-sequence
                     0 (- icalendar-export-sexp-enumeration-days 1))))))
        (t
         ;; no match
         nil)))

(defun icalendar--convert-block-to-ical (nonmarker entry-main)
  "Convert block diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker
                            "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
                            " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
                            "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
                            "\\("
                            "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
                            "\\)?"
                            "\\s-*\\(.*?\\) ?$")
                    entry-main)
      (let* ((startstring (substring entry-main
                                     (match-beginning 1)
                                     (match-end 1)))
             (endstring (substring entry-main
                                   (match-beginning 2)
                                   (match-end 2)))
             (startisostring (icalendar--datestring-to-isodate
                              startstring))
             (endisostring (icalendar--datestring-to-isodate
                            endstring))
             (endisostring+1 (icalendar--datestring-to-isodate
                              endstring 1))
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 4)
                                   (substring entry-main
                                              (match-beginning 4)
                                              (match-end 4))
                                 nil)
                               (if (match-beginning 5)
                                   (substring entry-main
                                              (match-beginning 5)
                                              (match-end 5))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)
                             (if (match-beginning 8)
                                 (substring entry-main
                                            (match-beginning 8)
                                            (match-end 8))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 9)
                                  (match-end 9)))))
        (icalendar--dmsg "diary-block %s" entry-main)
        (when starttimestring
          (unless endtimestring
            (let ((time
                   (read (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (setq endtimestring (format "T%06d"
                                          (+ 10000 time))))))
        (if starttimestring
            ;; with time -> write rrule
            (cons (concat "\nDTSTART;VALUE=DATE-TIME:"
                          startisostring
                          starttimestring
                          "\nDTEND;VALUE=DATE-TIME:"
                          startisostring
                          endtimestring
                          "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
                          endisostring)
                  summary)
          ;; no time -> write long event
          (cons (concat "\nDTSTART;VALUE=DATE:" startisostring
                        "\nDTEND;VALUE=DATE:" endisostring+1)
                summary)))
    ;; no match
    nil))

(defun icalendar--convert-float-to-ical (nonmarker entry-main)
  "Convert float diary entry to iCalendar format -- partially unsupported!

  FIXME!  DAY from `diary-float' yet unimplemented.

  NONMARKER is a regular expression matching the start of non-marking
  entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main)
      (with-temp-buffer
        (insert (match-string 1 entry-main))
        (goto-char (point-min))
        (let* ((sexp (read (current-buffer))) ;using `read' here
					      ;easier than regexp
					      ;matching, esp. with
					      ;different forms of
					      ;MONTH
               (month-exp (nth 1 sexp))
               (months (cond ((eq month-exp t) nil) ; don't add a BYMONTH clause
                             ((integerp month-exp) (list month-exp))
                             (t month-exp)))
               (dayname (nth 2 sexp))
               (n (nth 3 sexp))
               (day (nth 4 sexp))
               (dtstart
                ;; Start on the first day matching the rule in
                ;; icalendar-recurring-start-year:
                (calendar-nth-named-day n dayname
                                        (if months (apply #'min months) 1)
                                        icalendar-recurring-start-year))
               (summary
                (replace-regexp-in-string
		 "\\(^\s+\\|\s+$\\)" ""
		 (buffer-substring (point) (point-max)))))

          (when day
            (progn
              (icalendar--dmsg "diary-float %s" entry-main)
              (error "Don't know if or how to implement day in `diary-float'")))

          (cons (concat
                 "\nDTSTART;VALUE=DATE:"
                 (format "%04d%02d%02d"
                         (calendar-extract-year dtstart)
                         (calendar-extract-month dtstart)
                         (calendar-extract-day dtstart))
                 "\nRRULE:"
                 (if months
                     "FREQ=YEARLY;BYMONTH="
                   "FREQ=MONTHLY")
                 (when months
                   (mapconcat
                    (lambda (m) (number-to-string m))
                    months ","))
                 ";BYDAY="
                 (number-to-string n)
		 (aref icalendar--weekday-array dayname))
                summary)))
    ;; no match
    nil))

(defun icalendar--convert-date-to-ical (nonmarker entry-main)
  "Convert `diary-date' diary entry to iCalendar format -- unsupported!

FIXME!

NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker
                            "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
                    entry-main)
      (progn
        (icalendar--dmsg "diary-date %s" entry-main)
        (error "`diary-date' is not supported yet"))
    ;; no match
    nil))

(defun icalendar--convert-cyclic-to-ical (nonmarker entry-main)
  "Convert `diary-cyclic' diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker
                            "%%(diary-cyclic \\([^ ]+\\) +"
                            "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
                            "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
                            "\\("
                            "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
                            "\\)?"
                            "\\s-*\\(.*?\\) ?$")
                    entry-main)
      (let* ((frequency (substring entry-main (match-beginning 1)
                                   (match-end 1)))
             (datetime (substring entry-main (match-beginning 2)
                                  (match-end 2)))
             (startisostring (icalendar--datestring-to-isodate
                              datetime))
             (endisostring (icalendar--datestring-to-isodate
                            datetime))
             (endisostring+1 (icalendar--datestring-to-isodate
                              datetime 1))
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 4)
                                   (substring entry-main
                                              (match-beginning 4)
                                              (match-end 4))
                                 nil)
                               (if (match-beginning 5)
                                   (substring entry-main
                                              (match-beginning 5)
                                              (match-end 5))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)
                             (if (match-beginning 8)
                                 (substring entry-main
                                            (match-beginning 8)
                                            (match-end 8))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 9)
                                  (match-end 9)))))
        (icalendar--dmsg "diary-cyclic %s" entry-main)
        (when starttimestring
          (unless endtimestring
            (let ((time
                   (read (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (setq endtimestring (format "T%06d"
                                          (+ 10000 time))))))
        (cons (concat "\nDTSTART;"
                      (if starttimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      startisostring
                      (or starttimestring "")
                      "\nDTEND;"
                      (if endtimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      (if endtimestring endisostring endisostring+1)
                      (or endtimestring "")
                      "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
                      ;; strange: korganizer does not expect
                      ;; BYSOMETHING here...
                      )
              summary))
    ;; no match
    nil))

(defun icalendar--convert-anniversary-to-ical (nonmarker entry-main)
  "Convert `diary-anniversary' diary entry to iCalendar format.
NONMARKER is a regular expression matching the start of non-marking
entries.  ENTRY-MAIN is the first line of the diary entry."
  (declare (obsolete "use `diary-icalendar-parse-entry' and `icalendar-print-component-node' instead." "31.1"))
  (if (string-match (concat nonmarker
                            "%%(diary-anniversary \\([^)]+\\))\\s-*"
                            "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
                            "\\("
                            "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
                            "\\)?"
                            "\\s-*\\(.*?\\) ?$")
                    entry-main)
      (let* ((datetime (substring entry-main (match-beginning 1)
                                  (match-end 1)))
             (startisostring (icalendar--datestring-to-isodate
                              datetime nil 1))
             (endisostring (icalendar--datestring-to-isodate
                            datetime 1 1))
             (starttimestring (icalendar--diarytime-to-isotime
                               (if (match-beginning 3)
                                   (substring entry-main
                                              (match-beginning 3)
                                              (match-end 3))
                                 nil)
                               (if (match-beginning 4)
                                   (substring entry-main
                                              (match-beginning 4)
                                              (match-end 4))
                                 nil)))
             (endtimestring (icalendar--diarytime-to-isotime
                             (if (match-beginning 6)
                                 (substring entry-main
                                            (match-beginning 6)
                                            (match-end 6))
                               nil)
                             (if (match-beginning 7)
                                 (substring entry-main
                                            (match-beginning 7)
                                            (match-end 7))
                               nil)))
             (summary (icalendar--convert-string-for-export
                       (substring entry-main (match-beginning 8)
                                  (match-end 8)))))
        (icalendar--dmsg "diary-anniversary %s" entry-main)
        (when starttimestring
          (unless endtimestring
            (let ((time
                   (read (replace-regexp-in-string "^T0?" ""
                                          starttimestring))))
              (setq endtimestring (format "T%06d"
                                          (+ 10000 time))))))
        (cons (concat "\nDTSTART;"
                      (if starttimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      startisostring
                      (or starttimestring "")
                      "\nDTEND;"
                      (if endtimestring "VALUE=DATE-TIME:"
                        "VALUE=DATE:")
                      endisostring
                      (or endtimestring "")
                      "\nRRULE:FREQ=YEARLY;INTERVAL=1"
                      ;; the following is redundant,
                      ;; but korganizer seems to expect this... ;(
                      ;; and evolution doesn't understand it... :(
                      ;; so... who is wrong?!
                      ";BYMONTH="
                      (substring startisostring 4 6)
                      ";BYMONTHDAY="
                      (substring startisostring 6 8))
              summary))
    ;; no match
    nil))

;; ======================================================================
;; Import -- convert iCalendar to emacs-diary
;; ======================================================================

;;;###autoload
(defun icalendar-import-file (ical-filename diary-filename
                                            &optional non-marking)
  "Import an iCalendar file and append to a diary file.
Argument ICAL-FILENAME output iCalendar file.
Argument DIARY-FILENAME input `diary-file'.
Optional argument NON-MARKING determines whether events are created as
non-marking or not."
  (declare (obsolete diary-icalendar-import-file "31.1"))
  (interactive "fImport iCalendar data from file: \nFInto diary file: \nP")
  ;; clean up the diary file
  (save-current-buffer
    ;; now load and convert from the ical file
    (set-buffer (find-file ical-filename))
    (icalendar-import-buffer diary-filename t non-marking)))

;;;###autoload
(defun icalendar-import-buffer (&optional diary-filename do-not-ask
                                          non-marking)
  "Extract iCalendar events from current buffer.

This function searches the current buffer for the first iCalendar
object, reads it and adds all VEVENT elements to the diary
DIARY-FILENAME.

It will ask for each appointment whether to add it to the diary
unless DO-NOT-ASK is non-nil.  When called interactively,
DO-NOT-ASK is nil, so that you are asked for each event.

NON-MARKING determines whether diary events are created as
non-marking.

Return code t means that importing worked well, return code nil
means that an error has occurred.  Error messages will be in the
buffer `*icalendar-errors*'."
  (declare (obsolete diary-icalendar-import-buffer "31.1"))
  (interactive)
  (save-current-buffer
    ;; prepare ical
    (message "Preparing iCalendar...")
    (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
    (goto-char (point-min))
    (message "Preparing iCalendar...done")
    (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
        (let (ical-contents ical-errors)
          ;; read ical
          (message "Reading iCalendar...")
          (beginning-of-line)
          (setq ical-contents (icalendar--read-element nil nil))
          (message "Reading iCalendar...done")
          ;; convert ical
          (message "Converting iCalendar...")
          (setq ical-errors (icalendar--convert-ical-to-diary
                             ical-contents
                             diary-filename do-not-ask non-marking))
          (when diary-filename
            ;; save the diary file if it is visited already
            (let ((b (find-buffer-visiting diary-filename)))
              (when b
                (save-current-buffer
                  (set-buffer b)
                  (save-buffer)))))
          (message "Converting iCalendar...done")
          ;; return t if no error occurred
          (not ical-errors))
      (message
       "Current buffer does not contain iCalendar contents!")
      ;; return nil, i.e. import did not work
      nil)))

(defun icalendar--format-ical-event (event)
  "Create a string representation of an iCalendar EVENT."
  (declare (obsolete diary-icalendar-format-entry "31.1"))
  (if (functionp icalendar-import-format)
      (funcall icalendar-import-format event)
    (let ((string icalendar-import-format)
          (case-fold-search nil)
        (conversion-list
         '(("%c" CLASS       icalendar-import-format-class)
           ("%d" DESCRIPTION icalendar-import-format-description)
           ("%l" LOCATION    icalendar-import-format-location)
           ("%o" ORGANIZER   icalendar-import-format-organizer)
           ("%s" SUMMARY     icalendar-import-format-summary)
           ("%t" STATUS      icalendar-import-format-status)
           ("%u" URL         icalendar-import-format-url)
           ("%U" UID         icalendar-import-format-uid))))
    ;; convert the specifiers in the format string
    (mapc (lambda (i)
	    (let* ((spec (car i))
		   (prop (cadr i))
		   (format (car (cddr i)))
		   (contents (icalendar--get-event-property event prop))
		   (formatted-contents ""))
	      (when (and contents (> (length contents) 0))
		(setq formatted-contents
		      (replace-regexp-in-string "%s"
				       (icalendar--convert-string-for-import
					contents)
				       (symbol-value format)
				       t t)))
	      (setq string (replace-regexp-in-string spec
					    formatted-contents
					    string
					    t t))))
	  conversion-list)
    string)))

(defun icalendar--convert-ical-to-diary (ical-list diary-filename
                                                   &optional do-not-ask
                                                   non-marking)
  "Convert iCalendar data to an Emacs diary file.
Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
DIARY-FILENAME.  If DO-NOT-ASK is nil the user is asked for each event
whether to actually import it.  NON-MARKING determines whether diary
events are created as non-marking.
This function attempts to return t if something goes wrong.  In this
case an error string which describes all the errors and problems is
written into the buffer `*icalendar-errors*'."
  (declare (obsolete diary-icalendar-import-buffer "31.1"))
  (let* ((ev (icalendar--all-events ical-list))
         (error-string "")
         (event-ok t)
         (found-error nil)
         (zone-map (icalendar--convert-all-timezones ical-list))
         e diary-string)
    ;; step through all events/appointments
    (while ev
      (setq e (car ev))
      (setq ev (cdr ev))
      (setq event-ok nil)
      (condition-case error-val
          (let* ((dtstart (icalendar--get-event-property e 'DTSTART))
                 (dtstart-zone (icalendar--find-time-zone
 				(icalendar--get-event-property-attributes
 				 e 'DTSTART)
 				zone-map))
                 (dtstart-dec (icalendar--decode-isodatetime dtstart nil
                                                             dtstart-zone))
                 (start-d (icalendar--datetime-to-diary-date
                           dtstart-dec))
                 (start-t (and dtstart
                               (> (length dtstart) 8)
                               (icalendar--datetime-to-colontime dtstart-dec)))
                 (dtend (icalendar--get-event-property e 'DTEND))
                 (dtend-zone (icalendar--find-time-zone
 			      (icalendar--get-event-property-attributes
 			       e 'DTEND)
 			      zone-map))
                 (dtend-dec (icalendar--decode-isodatetime dtend
                                                           nil dtend-zone))
                 (dtend-1-dec (icalendar--decode-isodatetime dtend -1
                                                             dtend-zone))
                 end-d
                 end-1-d
                 end-t
                 (summary (icalendar--convert-string-for-import
                           (or (icalendar--get-event-property e 'SUMMARY)
                               "No summary")))
                 (rrule (icalendar--get-event-property e 'RRULE))
                 (rdate (icalendar--get-event-property e 'RDATE))
                 (duration (icalendar--get-event-property e 'DURATION)))
            (icalendar--dmsg "%s: `%s'" start-d summary)
            ;; check whether start-time is missing
            (if  (and dtstart
                      (string=
                       (cadr (icalendar--get-event-property-attributes
                              e 'DTSTART))
                       "DATE"))
                (setq start-t nil))
            (when duration
              (let ((dtend-dec-d (icalendar--add-decoded-times
                                  dtstart-dec
                                  (icalendar--decode-isoduration duration)))
                    (dtend-1-dec-d (icalendar--add-decoded-times
                                    dtstart-dec
                                    (icalendar--decode-isoduration duration
                                                                   t))))
                (if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
                    (message "Inconsistent endtime and duration for %s"
                             summary))
                (setq dtend-dec dtend-dec-d)
                (setq dtend-1-dec dtend-1-dec-d)))
            (setq end-d (if dtend-dec
                            (icalendar--datetime-to-diary-date dtend-dec)
                          start-d))
            (setq end-1-d (if dtend-1-dec
                              (icalendar--datetime-to-diary-date dtend-1-dec)
                            start-d))
            (setq end-t (if (and
                             dtend-dec
                             (not (string=
                                   (cadr
                                    (icalendar--get-event-property-attributes
                                     e 'DTEND))
                                   "DATE")))
                            (icalendar--datetime-to-colontime dtend-dec)))
            (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d)
            (cond
             ;; recurring event
             (rrule
              (setq diary-string
                    (icalendar--convert-recurring-to-diary e dtstart-dec start-t
                                                           end-t))
              (setq event-ok t))
             (rdate
              (icalendar--dmsg "rdate event")
              (setq diary-string "")
              (mapc (lambda (_datestring)
		      (setq diary-string
			    (concat diary-string "......")))
		    (icalendar--split-value rdate)))
             ;; non-recurring event
             ;; all-day event
             ((not (string= start-d end-d))
              (setq diary-string
                    (icalendar--convert-non-recurring-all-day-to-diary
                     start-d end-1-d))
              (setq event-ok t))
             ;; not all-day
             ((and start-t (or (not end-t)
                               (not (string= start-t end-t))))
              (setq diary-string
                    (icalendar--convert-non-recurring-not-all-day-to-diary
                     dtstart-dec start-t end-t))
              (setq event-ok t))
             ;; all-day event
             (t
              (icalendar--dmsg "all day event")
              (setq diary-string (icalendar--datetime-to-diary-date
                                  dtstart-dec "/"))
              (setq event-ok t)))
            ;; add all other elements unless the user doesn't want to have
            ;; them
            (if event-ok
                (progn
                  (setq diary-string
                        (concat diary-string " "
                                (icalendar--format-ical-event e)))
                  (if do-not-ask (setq summary nil))
                  ;; add entry to diary and store actual name of diary
                  ;; file (in case it was nil)
                  (setq diary-filename
                        (icalendar--add-diary-entry diary-string diary-filename
                                                    non-marking summary)))
              ;; event was not ok
              (setq found-error t)
              (setq error-string
                    (format "%s\nCannot handle this event:%s"
                            error-string e))))
        ;; FIXME: inform user about ignored event properties
        ;; handle errors
        (error
         (message "Ignoring event \"%s\"" e)
         (setq found-error t)
         (setq error-string (format "%s\n%s\nCannot handle this event: %s"
                                    error-val error-string e))
         (message "%s" error-string))))

    ;; insert final newline
    (if diary-filename
        (let ((b (find-buffer-visiting diary-filename)))
          (when b
            (save-current-buffer
              (set-buffer b)
              (goto-char (point-max))
              (insert "\n")))))
    (if found-error
        (save-current-buffer
          (set-buffer (get-buffer-create "*icalendar-errors*"))
          (erase-buffer)
          (insert error-string)))
    (message "Converting iCalendar...done")
    found-error))

;; subroutines for importing
(defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t)
  "Convert recurring iCalendar event E to diary format.

DTSTART-DEC is the DTSTART property of E.
START-T is the event's start time in diary format.
END-T is the event's end time in diary format."
  (declare (obsolete diary-icalendar-format-entry "31.1"))
  (icalendar--dmsg "recurring event")
  (let* ((rrule        (icalendar--get-event-property e 'RRULE))
         (rrule-props  (icalendar--split-value rrule))
         (frequency    (cadr (assoc 'FREQ rrule-props)))
         (until        (cadr (assoc 'UNTIL rrule-props)))
         (count        (cadr (assoc 'COUNT rrule-props)))
         (interval     (read (or (cadr (assoc 'INTERVAL rrule-props)) "1")))
         (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec))
         (until-conv   (icalendar--datetime-to-diary-date
                        (icalendar--decode-isodatetime until)))
         (until-1-conv (icalendar--datetime-to-diary-date
                        (icalendar--decode-isodatetime until -1)))
         (result  ""))

    ;; FIXME FIXME interval!!!!!!!!!!!!!

    (when count
      (if until
          (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
        (let ((until-1 0))
          (cond ((string-equal frequency "DAILY")
                 (setq until (icalendar--add-decoded-times
                              dtstart-dec
                              (list 0 0 0 (* (read count) interval) 0 0)))
                 (setq until-1 (icalendar--add-decoded-times
                                dtstart-dec
                                (list 0 0 0 (* (- (read count) 1) interval)
                                      0 0)))
                 )
                ((string-equal frequency "WEEKLY")
                 (setq until (icalendar--add-decoded-times
                              dtstart-dec
                              (list 0 0 0 (* (read count) 7 interval) 0 0)))
                 (setq until-1 (icalendar--add-decoded-times
                                dtstart-dec
                                (list 0 0 0 (* (- (read count) 1) 7
                                               interval) 0 0)))
                 )
                ((string-equal frequency "MONTHLY")
                 (setq until (icalendar--add-decoded-times
                              dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
                                                           interval) 0)))
                 (setq until-1 (icalendar--add-decoded-times
                                dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
                                                             interval) 0)))
                 )
                ((string-equal frequency "YEARLY")
                 (setq until (icalendar--add-decoded-times
                              dtstart-dec (list 0 0 0 0 0 (* (- (read count) 1)
                                                             interval))))
                 (setq until-1 (icalendar--add-decoded-times
                                dtstart-dec
                                (list 0 0 0 0 0 (* (- (read count) 1)
                                                   interval))))
                 )
                (t
                 (message "Cannot handle COUNT attribute for `%s' events."
                          frequency)))
          (setq until-conv (icalendar--datetime-to-diary-date until))
          (setq until-1-conv (icalendar--datetime-to-diary-date until-1))
          ))
      )
    (cond ((string-equal frequency "WEEKLY")
	   (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
		  (weekdays
		   (icalendar--get-weekday-numbers byday))
		  (weekday-clause
		   (when (> (length weekdays) 1)
		     (format "(memq (calendar-day-of-week date) '%s) "
			     weekdays))))
	     (if (not start-t)
		 (progn
		   ;; weekly and all-day
		   (icalendar--dmsg "weekly all-day")
		   (if until
		       (setq result
			     (format
			      (concat "%%%%(and "
				      "%s"
				      "(diary-block %s %s))")
			      (or weekday-clause
				  (format "(diary-cyclic %d %s) "
					  (* interval 7)
					  dtstart-conv))
			      dtstart-conv
			      (if count until-1-conv until-conv)
			      ))
		       (setq result
			     (format "%%%%(and %s(diary-cyclic %d %s))"
				     (or weekday-clause "")
				     (if weekday-clause 1 (* interval 7))
				     dtstart-conv))))
               ;; weekly and not all-day
               (icalendar--dmsg "weekly not-all-day")
               (if until
                   (setq result
                         (format
                          (concat "%%%%(and "
                                  "%s"
                                  "(diary-block %s %s)) "
                                  "%s%s%s")
			  (or weekday-clause
			      (format "(diary-cyclic %d %s) "
				      (* interval 7)
				      dtstart-conv))
                          dtstart-conv
                          until-conv
                          (or start-t "")
                          (if end-t "-" "") (or end-t "")))
                 ;; no limit
                 ;; FIXME!!!!
                 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
                 ;; DTEND;VALUE=DATE-TIME:20030919T113000
                 (setq result
                       (format
                        "%%%%(and %s(diary-cyclic %d %s)) %s%s%s"
			(or weekday-clause "")
			(if weekday-clause 1 (* interval 7))
			dtstart-conv
			(or start-t "")
                        (if end-t "-" "") (or end-t "")))))))
          ;; yearly
          ((string-equal frequency "YEARLY")
           (icalendar--dmsg "yearly")
           (if until
               (let ((day (nth 3 dtstart-dec))
                     (month (nth 4 dtstart-dec)))
                 (setq result (concat "%%(and (diary-date "
                                      (cond ((eq calendar-date-style 'iso)
                                             (format "t %d %d" month day))
                                            ((eq calendar-date-style 'european)
                                             (format "%d %d t" day month))
                                            ((eq calendar-date-style 'american)
                                             (format "%d %d t" month day)))
                                      ") (diary-block "
                                      dtstart-conv
                                      " "
                                      until-conv
                                      ")) "
                                      (or start-t "")
                                      (if end-t "-" "")
                                      (or end-t ""))))
             (setq result (format
                           "%%%%(diary-anniversary %s) %s%s%s"
                           (let* ((year (nth 5 dtstart-dec))
                                  (dtstart-1y-dec (copy-sequence dtstart-dec)))
                             (setf (nth 5 dtstart-1y-dec) (1- year))
                             (icalendar--datetime-to-diary-date dtstart-1y-dec))
                           (or start-t "")
                           (if end-t "-" "") (or end-t "")))))
          ;; monthly
          ((string-equal frequency "MONTHLY")
           (icalendar--dmsg "monthly")
	   (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
                  (count-weekday
                   (and byday
                        (save-match-data
                          (when (string-match "\\(-?[0-9]+\\)\\([A-Z][A-Z]\\)"
                                              byday)
                            (cons (substring byday
                                             (match-beginning 1)
                                             (match-end 1))
                                  (substring byday
                                             (match-beginning 2)
                                             (match-end 2)))))))
                  (rule-part
                   (if count-weekday
                       (let ((count (car count-weekday))
                             (weekdaynum (icalendar--get-weekday-number
                                          (cdr count-weekday))))
                         ;; FIXME: this is valid only for interval==1
                         (format "(diary-float t %s %s)" weekdaynum count))
                     (format "(diary-date %s)"
                             (let ((day (nth 3 dtstart-dec)))
                               (cond ((eq calendar-date-style 'iso)
                                      (format "t t %d" day))
                                     ((eq calendar-date-style 'european)
                                      (format "%d t t" day))
                                     ((eq calendar-date-style 'american)
                                      (format "t %d t" day))))))))
             (setq result
                   (format
                    "%%%%(and %s (diary-block %s %s)) %s%s%s"
                    rule-part
                    dtstart-conv
                    (if until
                        until-conv
                      (if (eq calendar-date-style 'iso) "9999 1 1" "1 1 9999")) ;; FIXME: should be unlimited
                    (or start-t "")
                    (if end-t "-" "") (or end-t "")))))
          ;; daily
          ((and (string-equal frequency "DAILY"))
           (if until
               (setq result
                     (format
                      (concat "%%%%(and (diary-cyclic %s %s) "
                              "(diary-block %s %s)) %s%s%s")
                      interval dtstart-conv dtstart-conv
                      (if count until-1-conv until-conv)
                      (or start-t "")
                      (if end-t "-" "") (or end-t "")))
             (setq result
                   (format
                    "%%%%(and (diary-cyclic %s %s)) %s%s%s"
                    interval
                    dtstart-conv
                    (or start-t "")
                    (if end-t "-" "") (or end-t ""))))))
    ;; Handle exceptions from recurrence rules
    (let ((ex-dates (icalendar--get-event-properties e 'EXDATE)))
      (while ex-dates
        (let* ((ex-start (icalendar--decode-isodatetime
                          (car ex-dates)))
               (ex-d (icalendar--datetime-to-diary-date
                      ex-start)))
          (setq result
                (replace-regexp-in-string "^%%(\\(and \\)?"
                                 (format
                                  "%%%%(and (not (diary-date %s)) "
                                  ex-d)
                                 result)))
        (setq ex-dates (cdr ex-dates))))
    ;; FIXME: exception rules are not recognized
    (if (icalendar--get-event-property e 'EXRULE)
        (setq result
              (concat result
                      "\n Exception rules: "
                      (icalendar--get-event-properties
                       e 'EXRULE))))
    result))

(defun icalendar--convert-non-recurring-all-day-to-diary (start-d end-d)
  "Convert non-recurring iCalendar EVENT to diary format.

DTSTART is the decoded DTSTART property of E.
Argument START-D gives the first day.
Argument END-D gives the last day."
  (declare (obsolete diary-icalendar-format-time-range "31.1"))
  (icalendar--dmsg "non-recurring all-day event")
  (format "%%%%(and (diary-block %s %s))" start-d end-d))

(defun icalendar--convert-non-recurring-not-all-day-to-diary (dtstart-dec
                                                              start-t
                                                              end-t)
  "Convert recurring icalendar EVENT to diary format.

DTSTART-DEC is the decoded DTSTART property of E.
START-T is the event's start time in diary format.
END-T is the event's end time in diary format."
  (declare (obsolete diary-icalendar-format-time-range "31.1"))
  (icalendar--dmsg "not all day event")
  (cond (end-t
         (format "%s %s-%s"
                 (icalendar--datetime-to-diary-date
                  dtstart-dec "/")
                 start-t end-t))
        (t
         (format "%s %s"
                 (icalendar--datetime-to-diary-date
                  dtstart-dec "/")
                 start-t))))

(defun icalendar--add-diary-entry (string diary-filename non-marking
                                          &optional summary)
  "Add STRING to the diary file DIARY-FILENAME.
STRING must be a properly formatted valid diary entry.  NON-MARKING
determines whether diary events are created as non-marking.  If
SUMMARY is not nil it must be a string that gives the summary of the
entry.  In this case the user will be asked whether he wants to insert
the entry."
  (declare (obsolete "see `diary-icalendar-post-entry-format-hook' and
`diary-icalendar--entry-import'" "31.1"))
  (when (or (not summary)
            (y-or-n-p (format-message "Add appointment for `%s' to diary? "
                                      summary)))
    (when summary
      (setq non-marking
            (y-or-n-p "Make appointment non-marking? ")))
    (unless diary-filename
      (setq diary-filename
            (read-file-name "Add appointment to this diary file: ")))
    (diary-make-entry string non-marking diary-filename t t))
  ;; return diary-filename in case it has been changed interactively
  diary-filename)

;; ======================================================================
;; Examples
;; ======================================================================
(defun icalendar-import-format-sample (event)
  "Example function for formatting an iCalendar EVENT."
  (declare (obsolete "see `diary-icalendar-vevent-skeleton'" "31.1"))
  (format (concat "SUMMARY='%s' DESCRIPTION='%s' LOCATION='%s' ORGANIZER='%s' "
                  "STATUS='%s' URL='%s' CLASS='%s'")
          (or (icalendar--get-event-property event 'SUMMARY) "")
          (or (icalendar--get-event-property event 'DESCRIPTION) "")
          (or (icalendar--get-event-property event 'LOCATION) "")
          (or (icalendar--get-event-property event 'ORGANIZER) "")
          (or (icalendar--get-event-property event 'STATUS) "")
          (or (icalendar--get-event-property event 'URL) "")
          (or (icalendar--get-event-property event 'CLASS) "")))

) ; Closes the top-level `with-suppressed-warnings' form above

;; Obsolete

(defconst icalendar-version "0.19" "Version number of icalendar.el.")
(make-obsolete-variable 'icalendar-version 'emacs-version "28.1")

(provide 'icalendar)

;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:
;;; icalendar.el ends here
