/*
 * Copyright (c) 2014 - 2015 Ngewi Fet <ngewif@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gnucash.android.model;

import android.content.Context;
import android.support.annotation.NonNull;

import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.joda.time.LocalDateTime;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
* Represents a scheduled event which is stored in the database and run at regular mPeriod
*
* @author Ngewi Fet <ngewif@gmail.com>
*/
public class ScheduledAction extends BaseModel{

    private long mStartDate;
    private long mEndDate;
    private String mTag;

    /**
     * Recurrence of this scheduled action
     */
    private Recurrence mRecurrence;

    /**
     * Types of events which can be scheduled
     */
    public enum ActionType {TRANSACTION, BACKUP}

    /**
     * Next scheduled run of Event
     */
    private long mLastRun = 0;

    /**
     * Unique ID of the template from which the recurring event will be executed.
     * For example, transaction UID
     */
    private String mActionUID;

    /**
     * Flag indicating if this event is enabled or not
     */
    private boolean mIsEnabled;

    /**
     * Type of event being scheduled
     */
    private ActionType mActionType;

    /**
     * Number of times this event is planned to be executed
     */
    private int mTotalFrequency = 0;

    /**
     * How many times this action has already been executed
     */
    private int mExecutionCount = 0;

    /**
     * Flag for whether the scheduled transaction should be auto-created
     */
    private boolean mAutoCreate = true;
    private boolean mAutoNotify = false;
    private int mAdvanceCreateDays = 0;
    private int mAdvanceNotifyDays = 0;
    private String mTemplateAccountUID;

    public ScheduledAction(ActionType actionType){
        mActionType = actionType;
        mEndDate = 0;
        mIsEnabled = true; //all actions are enabled by default
    }

    /**
     * Returns the type of action to be performed by this scheduled action
     * @return ActionType of the scheduled action
     */
    public ActionType getActionType() {
        return mActionType;
    }

    /**
     * Sets the {@link ActionType}
     * @param actionType Type of action
     */
    public void setActionType(ActionType actionType) {
        this.mActionType = actionType;
    }

    /**
     * Returns the GUID of the action covered by this scheduled action
     * @return GUID of action
     */
    public String getActionUID() {
        return mActionUID;
    }

    /**
     * Sets the GUID of the action being scheduled
     * @param actionUID GUID of the action
     */
    public void setActionUID(String actionUID) {
        this.mActionUID = actionUID;
    }

    /**
     * Returns the timestamp of the last execution of this scheduled action
     * <p>This is not necessarily the time when the scheduled action was due, only when it was actually last executed.</p>
     * @return Timestamp in milliseconds since Epoch
     */
    public long getLastRunTime() {
        return mLastRun;
    }

    /**
     * Returns the time when the last schedule in the sequence of planned executions was executed.
     * This relies on the number of executions of the scheduled action
     * <p>This is different from {@link #getLastRunTime()} which returns the date when the system last
     * run the scheduled action.</p>
     * @return Time of last schedule, or -1 if the scheduled action has never been run
     */
    public long getTimeOfLastSchedule(){
        if (mExecutionCount == 0)
            return  -1;

        LocalDateTime startTime = LocalDateTime.fromDateFields(new Date(mStartDate));
        int multiplier = mRecurrence.getMultiplier();

        int factor = (mExecutionCount-1) * multiplier;
        switch (mRecurrence.getPeriodType()){
            case HOUR:
                startTime = startTime.plusHours(factor);
                break;
            case DAY:
                startTime = startTime.plusDays(factor);
                break;
            case WEEK:
                startTime = startTime.plusWeeks(factor);
                break;
            case MONTH:
                startTime = startTime.plusMonths(factor);
                break;
            case YEAR:
                startTime = startTime.plusYears(factor);
                break;
        }

        return startTime.toDate().getTime();
    }

    /**
     * Computes the next time that this scheduled action is supposed to be
     * executed based on the execution count.
     *
     * <p>This method does not consider the end time, or number of times it should be run.
     * It only considers when the next execution would theoretically be due.</p>
     *
     * @return Next run time in milliseconds
     */
    public long computeNextCountBasedScheduledExecutionTime(){
        return computeNextScheduledExecutionTimeStartingAt(getTimeOfLastSchedule());
    }

    /**
     * Computes the next time that this scheduled action is supposed to be
     * executed based on the time of the last run.
     *
     * <p>This method does not consider the end time, or number of times it should be run.
     * It only considers when the next execution would theoretically be due.</p>
     *
     * @return Next run time in milliseconds
     */
    public long computeNextTimeBasedScheduledExecutionTime() {
        return computeNextScheduledExecutionTimeStartingAt(getLastRunTime());
    }

    /**
     * Computes the next time that this scheduled action is supposed to be
     * executed starting at startTime.
     *
     * <p>This method does not consider the end time, or number of times it should be run.
     * It only considers when the next execution would theoretically be due.</p>
     *
     * @param startTime time in milliseconds to use as start to compute the next schedule.
     *
     * @return Next run time in milliseconds
     */
    private long computeNextScheduledExecutionTimeStartingAt(long startTime) {
        if (startTime <= 0){ // has never been run
            return mStartDate;
        }

        int multiplier = mRecurrence.getMultiplier();
        LocalDateTime nextScheduledExecution = LocalDateTime.fromDateFields(new Date(startTime));
        switch (mRecurrence.getPeriodType()) {
            case HOUR:
                nextScheduledExecution = nextScheduledExecution.plusHours(multiplier);
                break;
            case DAY:
                nextScheduledExecution = nextScheduledExecution.plusDays(multiplier);
                break;
            case WEEK:
                nextScheduledExecution = computeNextWeeklyExecutionStartingAt(nextScheduledExecution);
                break;
            case MONTH:
                nextScheduledExecution = nextScheduledExecution.plusMonths(multiplier);
                break;
            case YEAR:
                nextScheduledExecution = nextScheduledExecution.plusYears(multiplier);
                break;
        }
        return nextScheduledExecution.toDate().getTime();
    }

    /**
     * Computes the next time that this weekly scheduled action is supposed to be
     * executed starting at startTime.
     *
     * If no days of the week have been set (GnuCash desktop allows it), it will return a
     * date in the future to ensure ScheduledActionService doesn't execute it.
     *
     * @param startTime LocalDateTime to use as start to compute the next schedule.
     *
     * @return Next run time as a LocalDateTime. A date in the future, if no days of the week
     *      were set in the Recurrence.
     */
    @NonNull
    private LocalDateTime computeNextWeeklyExecutionStartingAt(LocalDateTime startTime) {
        if (mRecurrence.getByDays().isEmpty())
            return LocalDateTime.now().plusDays(1); // Just a date in the future

        // Look into the week of startTime for another scheduled day of the week
        for (int dayOfWeek : mRecurrence.getByDays() ) {
            int jodaDayOfWeek = convertCalendarDayOfWeekToJoda(dayOfWeek);
            LocalDateTime candidateNextDueTime = startTime.withDayOfWeek(jodaDayOfWeek);
            if (candidateNextDueTime.isAfter(startTime))
                return candidateNextDueTime;
        }

        // Return the first scheduled day of the week from the next due week
        int firstScheduledDayOfWeek = convertCalendarDayOfWeekToJoda(mRecurrence.getByDays().get(0));
        return startTime.plusWeeks(mRecurrence.getMultiplier())
                        .withDayOfWeek(firstScheduledDayOfWeek);
    }

    /**
     * Converts a java.util.Calendar day of the week constant to the
     * org.joda.time.DateTimeConstants equivalent.
     *
     * @param calendarDayOfWeek day of the week constant from java.util.Calendar
     * @return day of the week constant equivalent from org.joda.time.DateTimeConstants
     */
    private int convertCalendarDayOfWeekToJoda(int calendarDayOfWeek) {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.DAY_OF_WEEK, calendarDayOfWeek);
        return LocalDateTime.fromCalendarFields(cal).getDayOfWeek();
    }

    /**
     * Set time of last execution of the scheduled action
     * @param nextRun Timestamp in milliseconds since Epoch
     */
    public void setLastRun(long nextRun) {
        this.mLastRun = nextRun;
    }

    /**
     * Returns the period of this scheduled action in milliseconds.
     * @return Period in milliseconds since Epoch
     * @deprecated Uses fixed values for time of months and years (which actually vary depending on number of days in month or leap year)
     */
    public long getPeriod() {
        return mRecurrence.getPeriod();
    }

    /**
     * Returns the time of first execution of the scheduled action
     * @return Start time of scheduled action in milliseconds since Epoch
     */
    public long getStartTime() {
        return mStartDate;
    }

    /**
     * Sets the time of first execution of the scheduled action
     * @param startDate Timestamp in milliseconds since Epoch
     */
    public void setStartTime(long startDate) {
        this.mStartDate = startDate;
        if (mRecurrence != null) {
            mRecurrence.setPeriodStart(new Timestamp(startDate));
        }
    }

    /**
     * Returns the time of last execution of the scheduled action
     * @return Timestamp in milliseconds since Epoch
     */
    public long getEndTime() {
        return mEndDate;
    }

    /**
     * Sets the end time of the scheduled action
     * @param endDate Timestamp in milliseconds since Epoch
     */
    public void setEndTime(long endDate) {
        this.mEndDate = endDate;
        if (mRecurrence != null){
            mRecurrence.setPeriodEnd(new Timestamp(mEndDate));
        }
    }

    /**
     * Returns the tag of this scheduled action
     * <p>The tag saves additional information about the scheduled action,
     * e.g. such as export parameters for scheduled backups</p>
     * @return Tag of scheduled action
     */
    public String getTag() {
        return mTag;
    }

    /**
     * Sets the tag of the schedules action.
     * <p>The tag saves additional information about the scheduled action,
     * e.g. such as export parameters for scheduled backups</p>
     * @param tag Tag of scheduled action
     */
    public void setTag(String tag) {
        this.mTag = tag;
    }

    /**
     * Returns {@code true} if the scheduled action is enabled, {@code false} otherwise
     * @return {@code true} if the scheduled action is enabled, {@code false} otherwise
     */
    public boolean isEnabled(){
        return mIsEnabled;
    }

    /**
     * Toggles the enabled state of the scheduled action
     * Disabled scheduled actions will not be executed
     * @param enabled Flag if the scheduled action is enabled or not
     */
    public void setEnabled(boolean enabled){
        this.mIsEnabled = enabled;
    }

    /**
     * Returns the total number of planned occurrences of this scheduled action.
     * @return Total number of planned occurrences of this action
     */
    public int getTotalPlannedExecutionCount(){
        return mTotalFrequency;
    }

    /**
     * Sets the number of occurences of this action
     * @param plannedExecutions Number of occurences
     */
    public void setTotalPlannedExecutionCount(int plannedExecutions){
        this.mTotalFrequency = plannedExecutions;
    }

    /**
     * Returns how many times this scheduled action has already been executed
     * @return Number of times this action has been executed
     */
    public int getExecutionCount(){
        return mExecutionCount;
    }

    /**
     * Sets the number of times this scheduled action has been executed
     * @param executionCount Number of executions
     */
    public void setExecutionCount(int executionCount){
        mExecutionCount = executionCount;
    }

    /**
     * Returns flag if transactions should be automatically created or not
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @return {@code true} if the transaction should be auto-created, {@code false} otherwise
     */
    public boolean shouldAutoCreate() {
        return mAutoCreate;
    }

    /**
     * Set flag for automatically creating transaction based on this scheduled action
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @param autoCreate Flag for auto creating transactions
     */
    public void setAutoCreate(boolean autoCreate) {
        this.mAutoCreate = autoCreate;
    }

    /**
     * Check if user will be notified of creation of scheduled transactions
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @return {@code true} if user will be notified, {@code false} otherwise
     */
    public boolean shouldAutoNotify() {
        return mAutoNotify;
    }

    /**
     * Sets whether to notify the user that scheduled transactions have been created
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @param autoNotify Boolean flag
     */
    public void setAutoNotify(boolean autoNotify) {
        this.mAutoNotify = autoNotify;
    }

    /**
     * Returns number of days in advance to create the transaction
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @return Number of days in advance to create transaction
     */
    public int getAdvanceCreateDays() {
        return mAdvanceCreateDays;
    }

    /**
     * Set number of days in advance to create the transaction
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @param advanceCreateDays Number of days
     */
    public void setAdvanceCreateDays(int advanceCreateDays) {
        this.mAdvanceCreateDays = advanceCreateDays;
    }

    /**
     * Returns the number of days in advance to notify of scheduled transactions
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @return {@code true} if user will be notified, {@code false} otherwise
     */
    public int getAdvanceNotifyDays() {
        return mAdvanceNotifyDays;
    }

    /**
     * Set number of days in advance to notify of scheduled transactions
     * <p>This flag is currently unused in the app. It is only included here for compatibility with GnuCash desktop XML</p>
     * @param advanceNotifyDays Number of days
     */
    public void setAdvanceNotifyDays(int advanceNotifyDays) {
        this.mAdvanceNotifyDays = advanceNotifyDays;
    }

    /**
     * Return the template account GUID for this scheduled action
     * <p>This method generates one if none was set</p>
     * @return String GUID of template account
     */
    public String getTemplateAccountUID() {
        if (mTemplateAccountUID == null)
            return mTemplateAccountUID = generateUID();
        else
            return mTemplateAccountUID;
    }

    /**
     * Set the template account GUID
     * @param templateAccountUID String GUID of template account
     */
    public void setTemplateAccountUID(String templateAccountUID) {
        this.mTemplateAccountUID = templateAccountUID;
    }

    /**
     * Returns the event schedule (start, end and recurrence)
     * @return String description of repeat schedule
     */
    public String getRepeatString(){
        StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRepeatString());
        Context context = GnuCashApplication.getAppContext();
        if (mEndDate <= 0 && mTotalFrequency > 0){
            ruleBuilder.append(", ").append(context.getString(R.string.repeat_x_times, mTotalFrequency));
        }

        return ruleBuilder.toString();
    }

    /**
     * Creates an RFC 2445 string which describes this recurring event
     * <p>See http://recurrance.sourceforge.net/</p>
     * @return String describing event
     */
    public String getRuleString(){
        String separator = ";";

        StringBuilder ruleBuilder = new StringBuilder(mRecurrence.getRuleString());

        if (mEndDate > 0){
            SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
            df.setTimeZone(TimeZone.getTimeZone("UTC"));
            ruleBuilder.append("UNTIL=").append(df.format(new Date(mEndDate))).append(separator);
        } else if (mTotalFrequency > 0){
            ruleBuilder.append("COUNT=").append(mTotalFrequency).append(separator);
        }

        return ruleBuilder.toString();
    }

    /**
     * Return GUID of recurrence pattern for this scheduled action
     * @return {@link Recurrence} object
     */
    public Recurrence getRecurrence() {
        return mRecurrence;
    }

    /**
     * Overloaded method for setting the recurrence of the scheduled action.
     * <p>This method allows you to specify the periodicity and the ordinal of it. For example,
     * a recurrence every fortnight would give parameters: {@link PeriodType#WEEK}, ordinal:2</p>
     * @param periodType Periodicity of the scheduled action
     * @param ordinal Ordinal of the periodicity. If unsure, specify 1
     * @see #setRecurrence(Recurrence)
     */
    public void setRecurrence(PeriodType periodType, int ordinal){
        Recurrence recurrence = new Recurrence(periodType);
        recurrence.setMultiplier(ordinal);
        setRecurrence(recurrence);
    }

    /**
     * Sets the recurrence pattern of this scheduled action
     * <p>This also sets the start period of the recurrence object, if there is one</p>
     * @param recurrence {@link Recurrence} object
     */
    public void setRecurrence(@NonNull Recurrence recurrence) {
        this.mRecurrence = recurrence;
        //if we were parsing XML and parsed the start and end date from the scheduled action first,
        //then use those over the values which might be gotten from the recurrence
        if (mStartDate > 0){
            mRecurrence.setPeriodStart(new Timestamp(mStartDate));
        } else {
            mStartDate = mRecurrence.getPeriodStart().getTime();
        }

        if (mEndDate > 0){
            mRecurrence.setPeriodEnd(new Timestamp(mEndDate));
        } else if (mRecurrence.getPeriodEnd() != null){
            mEndDate = mRecurrence.getPeriodEnd().getTime();
        }
    }

    /**
     * Creates a ScheduledAction from a Transaction and a period
     * @param transaction Transaction to be scheduled
     * @param period Period in milliseconds since Epoch
     * @return Scheduled Action
     * @deprecated Used for parsing legacy backup files. Use {@link Recurrence} instead
     */
    @Deprecated
    public static ScheduledAction parseScheduledAction(Transaction transaction, long period){
        ScheduledAction scheduledAction = new ScheduledAction(ActionType.TRANSACTION);
        scheduledAction.mActionUID = transaction.getUID();
        Recurrence recurrence = Recurrence.fromLegacyPeriod(period);
        scheduledAction.setRecurrence(recurrence);
        return scheduledAction;
    }

    @Override
    public String toString() {
        return mActionType.name() + " - " + getRepeatString();
    }
}
