/*
 * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
 */

package at.bitfire.synctools.mapping.calendar

import android.content.Entity
import android.provider.CalendarContract.Events
import android.provider.CalendarContract.ExtendedProperties
import at.bitfire.synctools.icalendar.AssociatedEvents
import at.bitfire.synctools.icalendar.DatePropertyTzMapper.normalizedDate
import at.bitfire.synctools.icalendar.plusAssign
import at.bitfire.synctools.icalendar.recurrenceId
import at.bitfire.synctools.mapping.calendar.handler.AccessLevelHandler
import at.bitfire.synctools.mapping.calendar.handler.AndroidEventEntityHandler
import at.bitfire.synctools.mapping.calendar.handler.AttendeesHandler
import at.bitfire.synctools.mapping.calendar.handler.AvailabilityHandler
import at.bitfire.synctools.mapping.calendar.handler.CategoriesHandler
import at.bitfire.synctools.mapping.calendar.handler.ColorHandler
import at.bitfire.synctools.mapping.calendar.handler.DescriptionHandler
import at.bitfire.synctools.mapping.calendar.handler.DurationHandler
import at.bitfire.synctools.mapping.calendar.handler.EndTimeHandler
import at.bitfire.synctools.mapping.calendar.handler.LocationHandler
import at.bitfire.synctools.mapping.calendar.handler.OrganizerHandler
import at.bitfire.synctools.mapping.calendar.handler.OriginalInstanceTimeHandler
import at.bitfire.synctools.mapping.calendar.handler.RecurrenceFieldsHandler
import at.bitfire.synctools.mapping.calendar.handler.RemindersHandler
import at.bitfire.synctools.mapping.calendar.handler.SequenceHandler
import at.bitfire.synctools.mapping.calendar.handler.StartTimeHandler
import at.bitfire.synctools.mapping.calendar.handler.StatusHandler
import at.bitfire.synctools.mapping.calendar.handler.TitleHandler
import at.bitfire.synctools.mapping.calendar.handler.UidHandler
import at.bitfire.synctools.mapping.calendar.handler.UnknownPropertiesHandler
import at.bitfire.synctools.mapping.calendar.handler.UrlHandler
import at.bitfire.synctools.storage.calendar.EventAndExceptions
import at.bitfire.synctools.storage.calendar.EventsContract
import at.bitfire.synctools.util.AndroidTimeUtils.toZonedDateTime
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.ParameterList
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VEvent
import net.fortuna.ical4j.model.parameter.TzId
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.ExDate
import net.fortuna.ical4j.model.property.ProdId
import net.fortuna.ical4j.model.property.RDate
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.RecurrenceId
import java.time.Instant
import java.time.LocalDate
import java.util.LinkedList
import java.util.UUID

/**
 * Mapper from Android event main + data rows to [VEvent].
 *
 * @param accountName       account name (used to generate reminder email address)
 * @param prodIdGenerator   generates the `PRODID` to use
 */
class AndroidEventHandler(
    accountName: String,
    private val prodIdGenerator: ProdIdGenerator
) {

    private val entityHandlers: Array<AndroidEventEntityHandler> = arrayOf(
        // event row fields
        UidHandler(),
        OriginalInstanceTimeHandler(),
        TitleHandler(),
        LocationHandler(),
        StartTimeHandler(),
        EndTimeHandler(),
        DurationHandler(),
        RecurrenceFieldsHandler(),
        DescriptionHandler(),
        ColorHandler(),
        AccessLevelHandler(),
        AvailabilityHandler(),
        StatusHandler(),
        // scheduling
        SequenceHandler(),
        OrganizerHandler(),
        AttendeesHandler(),
        // extended properties
        CategoriesHandler(),
        UnknownPropertiesHandler(),
        UrlHandler(),
        // sub-components
        RemindersHandler(accountName)
    )


    /**
     * Result of the [mapToVEvents] operation.
     *
     * @param associatedEvents  mapped events
     * @param uid               UID of the mapped events
     * @param generatedUid      whether [uid] was generated by [mapToVEvents] (*false*: `UID` was already present before mapping)
     */
    class MappingResult(
        val associatedEvents: AssociatedEvents,
        val uid: String,
        val generatedUid: Boolean
    )

    /**
     * Maps an Android event with its exceptions to VEVENTs.
     *
     * VEVENTs must have a valid `UID`, so this method (or better to say, the [UidHandler] that it calls)
     * generates an UID, if necessary. If an `UID` was generated, it is noted in the result.
     */
    fun mapToVEvents(eventAndExceptions: EventAndExceptions): MappingResult {
        // make sure that main event has a UID
        var generatedUid = false
        val uid = provideUid(eventAndExceptions.main) {
            generatedUid = true
            UUID.randomUUID().toString()
        }

        // map main event
        val main = mapEvent(
            entity = eventAndExceptions.main,
            main = eventAndExceptions.main
        )

        // add exceptions of recurring main event
        val rRules = main.getProperties<RRule<*>>(Property.RRULE)
        val rDates = main.getProperties<RDate<*>>(Property.RDATE)
        val exceptions = LinkedList<VEvent>()
        if (rRules.isNotEmpty() || rDates.isNotEmpty()) {
            for (exception in eventAndExceptions.exceptions) {
                // convert exception to Event
                val exceptionEvent = mapEvent(
                    entity = exception,
                    main = eventAndExceptions.main
                )

                // make sure that exception has a RECURRENCE-ID
                val recurrenceId = exceptionEvent.recurrenceId ?: continue

                // generate EXDATE instead of VEVENT with RECURRENCE-ID for cancelled instances
                if (exception.entityValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED)
                    main += asExDate(exception, recurrenceId)
                else
                    exceptions += exceptionEvent
            }
        }

        val mappedEvents = AssociatedEvents(
            main = main,
            exceptions = exceptions,
            prodId = generateProdId(eventAndExceptions.main)
        )
        return MappingResult(
            associatedEvents = mappedEvents,
            uid = uid,
            generatedUid = generatedUid
        )
    }

    private fun asExDate(entity: Entity, recurrenceId: RecurrenceId<*>): ExDate<*> {
        val originalAllDay = (entity.entityValues.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0
        val date = recurrenceId.normalizedDate()

        // Return ExDate
        return if (originalAllDay) {
            // .. as date, without time
            ExDate(ParameterList(listOf(Value.DATE)), DateList(LocalDate.from(date)))
        } else if (date is Instant) {
            ExDate(DateList(date))
        } else {
            // .. as ZonedDateTime, with time and TZ param
            ExDate(
                ParameterList(listOf(TzId(date.toZonedDateTime().zone.id))),
                DateList(date.toZonedDateTime())
            )
        }
    }

    private fun generateProdId(main: Entity): ProdId {
        val mutators: String? = main.entityValues.getAsString(Events.MUTATORS)
        val packages: List<String> = mutators?.split(MUTATORS_SEPARATOR)?.toList() ?: emptyList()
        return prodIdGenerator.generateProdId(packages)
    }

    /**
     * Reads data of an event from the calendar provider, i.e. converts the [entity] values into a [VEvent].
     *
     * @param entity            event row as returned by the calendar provider
     * @param main              main event row as returned by the calendar provider
     *
     * @return generated data object
     */
    private fun mapEvent(entity: Entity, main: Entity): VEvent {
        // initialization adds DTSTAMP
        val vEvent = VEvent(/* initialise = */ true)

        for (handler in entityHandlers)
            handler.process(from = entity, main = main, to = vEvent)
        return vEvent
    }

    /**
     * Makes sure that the event has a UID ([Events.UID_2445] in the main event row).
     *
     * If the event doesn't have a UID, a new one is generated using [generateUid] and
     * put into [main].
     *
     * @param main  event to be checked (**will be modified** if it doesn't already have a UID)
     */
    private fun provideUid(
        main: Entity,
        generateUid: () -> String
    ): String {
        val mainValues = main.entityValues
        val existingUid = mainValues.getAsString(Events.UID_2445)
        if (existingUid != null) {
            // UID already present, nothing to do
            return existingUid
        }

        // have a look at extended properties (Google Calendar)
        val googleCalendarUid = main.subValues.firstOrNull {
            it.uri == ExtendedProperties.CONTENT_URI &&
            it.values.getAsString(ExtendedProperties.NAME) == EventsContract.EXTNAME_GOOGLE_CALENDAR_UID
        }?.values?.getAsString(ExtendedProperties.VALUE)
        if (googleCalendarUid != null) {
            // copy to UID_2445 so that it will be processed by UidHandler and return
            mainValues.put(Events.UID_2445, googleCalendarUid)
            return googleCalendarUid
        }

        // still no UID, generate one
        val newUid = generateUid()
        mainValues.put(Events.UID_2445, newUid)
        return newUid
    }


    companion object {

        /**
         * The [Events.MUTATORS] field contains a list of unique package names that have modified the event,
         * separated by this separator.
         */
        const val MUTATORS_SEPARATOR = ','

    }
}