/*
 * MoasdaWiki App
 * Copyright (C) 2008 - 2022 Herbert Reiter (herbert@moasdawiki.net)
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation (GPL-3.0-only).
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
 */

package net.moasdawiki.app;

import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.CalendarContract;
import androidx.core.app.ActivityCompat;
import androidx.preference.PreferenceManager;

import android.util.Log;
import android.widget.Toast;

import net.moasdawiki.service.transform.TerminTransformer;
import net.moasdawiki.util.PathUtils;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;

import static android.content.Context.ACCOUNT_SERVICE;

/**
 * Creates an Android calendar with all events found on Wiki pages and keeps them up-to-date.
 */
public class CalendarSyncAdapter extends AbstractThreadedSyncAdapter {

    private static final String TAG = "CalendarSyncAdapter";

    static final String ACCOUNT_NAME = "MoasdaWiki";
    static final String ACCOUNT_TYPE = "net.moasdawiki";
    static final String PROVIDER_NAME = "net.moasdawiki.app.provider";

    private static final String CALENDAR_NAME = "MoasdaWiki Events";
    private static final Uri CALENDAR_URI = CalendarContract.Calendars.CONTENT_URI;
    private static final Uri EVENT_URI    = CalendarContract.Events.   CONTENT_URI;
    private static final Uri REMINDER_URI = CalendarContract.Reminders.CONTENT_URI;

    private static final int REMINDER_BEFORE_EVENT_MINUTES = 4 * 60;

    private final ContentResolver contentResolver;

    @SuppressWarnings("SameParameterValue")
    CalendarSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
        super(context, autoInitialize, allowParallelSyncs);
        contentResolver = context.getContentResolver();
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
        Log.d(TAG, "Begin of onPerformSync()");
        List<TerminTransformer.Event> events = getWikiEvents();
        String calendarId = createCalendar();
        if (calendarId != null) {
            deleteAllEvents(calendarId);
            addEvents(calendarId, events);
        }

        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(() -> {
            Toast toast = Toast.makeText(getContext(), R.string.calendar_sync_finished, Toast.LENGTH_SHORT);
            toast.show();
        });
        Log.d(TAG, "End of onPerformSync()");
    }

    /**
     * Imports all events on Wiki pages to the Android calendar.
     */
    @NotNull
    private List<TerminTransformer.Event> getWikiEvents() {
        Log.d(TAG, "Reading Wiki events");
        WikiEngineApplication app = (WikiEngineApplication) getContext();
        TerminTransformer terminTransformer = app.getTerminTransformer();
        if (terminTransformer == null) {
            Log.e(TAG, "TerminTransformer not initialized yet, cannot retrieve event list");
            return Collections.emptyList();
        }
        List<TerminTransformer.Event> events = terminTransformer.getEvents();
        Log.d(TAG, "Wiki events found: " + events.size());
        return events;
    }

    @NotNull
    private Uri buildUri(@NotNull Uri uri) {
        return uri.buildUpon()
                .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, ACCOUNT_NAME)
                .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, ACCOUNT_TYPE).build();
    }

    /**
     * Returns the ID of the event calendar. If the calendar doesn't exist, it is created.
     *
     * @return Calendar ID
     */
    @Nullable
    private String createCalendar() {
        // Calendar already existing?
        Log.d(TAG, "Check if calendar already exists");
        Uri calendarUri = buildUri(CALENDAR_URI);
        try (Cursor cursor = contentResolver.query(calendarUri,
                new String[] { CalendarContract.Calendars._ID },
                "(" + CalendarContract.Calendars.ACCOUNT_TYPE + " = ? AND " + CalendarContract.Calendars.NAME + " = ?)",
                new String[] { ACCOUNT_TYPE, CALENDAR_NAME },
                null)) {
            if (cursor != null && cursor.moveToFirst()) {
                // Calendar is already existing
                String calendarId = cursor.getString(0);
                Log.d(TAG, "Calendar already exists, id=" + calendarId);
                return calendarId;
            }
        }

        // Create a new calendar
        Log.d(TAG, "Calendar does not exist, create new one");
        ContentValues cv = new ContentValues();
        cv.put(CalendarContract.Calendars.ACCOUNT_NAME,          ACCOUNT_NAME);
        cv.put(CalendarContract.Calendars.ACCOUNT_TYPE,          ACCOUNT_TYPE);
        cv.put(CalendarContract.Calendars.NAME,                  CALENDAR_NAME);
        String displayName = getContext().getString(R.string.calendar_display_name);
        cv.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName);
        //cv.put(CalendarContract.Calendars.CALENDAR_COLOR,        0xEA8561);
        cv.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_READ);
        cv.put(CalendarContract.Calendars.OWNER_ACCOUNT,         ACCOUNT_NAME);
        cv.put(CalendarContract.Calendars.VISIBLE,               1);
        cv.put(CalendarContract.Calendars.SYNC_EVENTS,           1);
        Uri newCalendar = contentResolver.insert(calendarUri, cv);
        Log.i(TAG, "Created calendar: " + newCalendar);
        if (newCalendar != null) {
            return newCalendar.getLastPathSegment();
        } else {
            return null;
        }
    }

    /**
     * Clears the calendar. This is necessary if e.g. a birthday has been changed or removed.
     */
    private void deleteAllEvents(@NotNull String calendarId) {
        Log.d(TAG, "Delete all events from calendar");
        Uri eventUri = buildUri(EVENT_URI);
        try (Cursor cursor = contentResolver.query(eventUri,
                new String[] { CalendarContract.Events._ID },
                "(" + CalendarContract.Events.CALENDAR_ID + " = ?)",
                new String[] { calendarId },
                null)) {
            int count = 0;
            while (cursor != null && cursor.moveToNext()) {
                Uri deleteUri = ContentUris.withAppendedId(eventUri, cursor.getInt(0));
                contentResolver.delete(deleteUri, null, null);
                count++;
            }
            Log.d(TAG, "Deleted " + count + " events from calendar");
        }
    }

    /**
     * Adds all events from the event list. The first occurrence is in the current year, the events
     * are repeated every year.
     */
    private void addEvents(@NotNull String calendarId, @NotNull List<TerminTransformer.Event> events) {
        Log.d(TAG, "Create calendar events");
        for (TerminTransformer.Event event : events) {
            String title = event.description;
            if (title == null) {
                title = PathUtils.extractWebName(event.pagePath);
            }
            String description = getContext().getString(R.string.calendar_date) + ": " + TerminTransformer.formatGermanDate(event.dateFields);
            String eventId = addEvent(calendarId, event.dateFields.day, event.dateFields.month, event.dateFields.year, title, description);
            if (eventId != null) {
                addReminder(eventId);
            }
        }
        Log.i(TAG, "Created events: " + events.size());
    }

    /**
     * Adds a single event to the calendar.
     */
    @Nullable
    private String addEvent(@NotNull String calendarId, @Nullable Integer day, @Nullable Integer month, @Nullable Integer year, @NotNull String title, @NotNull String description) {
        Log.d(TAG, "Create calendar event: day=" + day + ", month=" + month + ", year=" + year
                + ", title=" + title + ", description=" + description);
        ContentValues cv = new ContentValues();
        cv.put(CalendarContract.Events.CALENDAR_ID, calendarId);
        cv.put(CalendarContract.Events.TITLE, title);
        cv.put(CalendarContract.Events.DESCRIPTION, description);

        TimeZone utc = TimeZone.getTimeZone("UTC");
        Calendar beginTime = Calendar.getInstance(utc);
        if (day == null) {
            day = 1;
        }
        if (month == null) {
            month = 1;
        }
        if (year == null) {
            year = beginTime.get(Calendar.YEAR);
        }
        beginTime.clear();
        beginTime.set(year, month - 1, day);

        cv.put(CalendarContract.Events.DTSTART, beginTime.getTimeInMillis());
        cv.put(CalendarContract.Events.DURATION, "PT1D");
        cv.put(CalendarContract.Events.ALL_DAY, 1);
        cv.put(CalendarContract.Events.RRULE, "FREQ=YEARLY");
        cv.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE);

        Uri eventUri = buildUri(EVENT_URI);
        Uri newEvent = contentResolver.insert(eventUri, cv);
        Log.i(TAG, "Created event: " + newEvent);
        if (newEvent != null) {
            return newEvent.getLastPathSegment();
        } else {
            return null;
        }
    }

    /**
     * Add a reminder to the given event.
     */
    private void addReminder(@NotNull String eventId) {
        Log.d(TAG, "Add reminder to eventId=" + eventId);
        ContentValues cv = new ContentValues();
        cv.put(CalendarContract.Reminders.EVENT_ID, eventId);
        cv.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);
        cv.put(CalendarContract.Reminders.MINUTES, REMINDER_BEFORE_EVENT_MINUTES);
        Uri reminderUri = buildUri(REMINDER_URI);
        Uri newReminder = contentResolver.insert(reminderUri, cv);
        Log.d(TAG, "Created reminder: " + newReminder);
    }

    /**
     * Initiates the calendar sync.
     */
    public static void requestCalendarSync(@NotNull Activity activity) {
        Log.d(TAG, "Requesting calendar synchronization");
        Context context = activity.getApplicationContext();

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        boolean calendarEnabled = preferences.getBoolean(Constants.PREFERENCES_CALENDAR_ENABLED, false);
        if (!calendarEnabled) {
            Log.d(TAG, "Calendar integration disabled, cancel operation");
            return;
        }

        try {
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) {
                    Log.d(TAG, "User has permanently denied permission WRITE_CALENDAR, informing him");
                    String hint = context.getString(R.string.calendar_permission_request, "WRITE_CALENDAR");
                    activity.runOnUiThread(() -> Toast.makeText(context, hint, Toast.LENGTH_SHORT).show());
                }
                Log.d(TAG, "Ask for permission WRITE_CALENDAR");
                ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_CALENDAR}, 0);
                return;
            }
            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.READ_CALENDAR)) {
                    Log.d(TAG, "User has permanently denied permission READ_CALENDAR, informing him");
                    String hint = context.getString(R.string.calendar_permission_request, "READ_CALENDAR");
                    activity.runOnUiThread(() -> Toast.makeText(context, hint, Toast.LENGTH_SHORT).show());
                }
                Log.d(TAG, "Ask for permission READ_CALENDAR");
                ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CALENDAR}, 0);
                return;
            }

            Account syncAccount = new Account(CalendarSyncAdapter.ACCOUNT_NAME, CalendarSyncAdapter.ACCOUNT_TYPE);
            AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE);
            if (accountManager != null && !accountManager.addAccountExplicitly(syncAccount, null, null)) {
                Log.e(TAG, "Error creating sync account, maybe account already exists");
            }

            Bundle settingsBundle = new Bundle();
            settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
            settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
            ContentResolver.requestSync(syncAccount, CalendarSyncAdapter.PROVIDER_NAME, settingsBundle);
            Log.d(TAG, "syncCalendar() finished");
        }
        catch (Throwable t) {
            Log.e(TAG, "Error in syncCalendar()", t);
        }
    }
}
