/*
 * Copyright (c) 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.content.res.Resources;
import android.support.annotation.NonNull;

import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.ui.util.RecurrenceParser;
import org.joda.time.Days;
import org.joda.time.Hours;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.Months;
import org.joda.time.ReadablePeriod;
import org.joda.time.Weeks;
import org.joda.time.Years;

import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * Model for recurrences in the database
 * <p>Basically a wrapper around {@link PeriodType}</p>
 */
public class Recurrence extends BaseModel {

    private PeriodType mPeriodType;

    /**
     * Start time of the recurrence
     */
    private Timestamp mPeriodStart;

    /**
     * End time of this recurrence
     * <p>This value is not persisted to the database</p>
     */
    private Timestamp mPeriodEnd;

    /**
     * Days of week on which to run the recurrence
     */
    private List<Integer> mByDays = Collections.emptyList();

    private int mMultiplier = 1; //multiplier for the period type

    public Recurrence(@NonNull PeriodType periodType){
        setPeriodType(periodType);
        mPeriodStart = new Timestamp(System.currentTimeMillis());
    }

    /**
     * Return the PeriodType for this recurrence
     * @return PeriodType for the recurrence
     */
    public PeriodType getPeriodType() {
        return mPeriodType;
    }

    /**
     * Sets the period type for the recurrence
     * @param periodType PeriodType
     */
    public void setPeriodType(PeriodType periodType) {
        this.mPeriodType = periodType;
    }

    /**
     * Return the start time for this recurrence
     * @return Timestamp of start of recurrence
     */
    public Timestamp getPeriodStart() {
        return mPeriodStart;
    }

    /**
     * Set the start time of this recurrence
     * @param periodStart {@link Timestamp} of recurrence
     */
    public void setPeriodStart(Timestamp periodStart) {
        this.mPeriodStart = periodStart;
    }


    /**
     * Returns an approximate period for this recurrence
     * <p>The period is approximate because months do not all have the same number of days,
     * but that is assumed</p>
     * @return Milliseconds since Epoch representing the period
     * @deprecated Do not use in new code. Uses fixed period values for months and years (which have variable units of time)
     */
    public long getPeriod(){
        long baseMillis = 0;
        switch (mPeriodType){
            case HOUR:
                baseMillis = RecurrenceParser.HOUR_MILLIS;
                break;
            case DAY:
                baseMillis = RecurrenceParser.DAY_MILLIS;
                break;
            case WEEK:
                baseMillis = RecurrenceParser.WEEK_MILLIS;
                break;
            case MONTH:
                baseMillis = RecurrenceParser.MONTH_MILLIS;
                break;
            case YEAR:
                baseMillis = RecurrenceParser.YEAR_MILLIS;
                break;
        }
        return mMultiplier * baseMillis;
    }

    /**
     * Returns the event schedule (start, end and recurrence)
     * @return String description of repeat schedule
     */
    public String getRepeatString(){
        StringBuilder repeatBuilder = new StringBuilder(getFrequencyRepeatString());
        Context context = GnuCashApplication.getAppContext();

        String dayOfWeek = new SimpleDateFormat("EEEE", GnuCashApplication.getDefaultLocale())
                .format(new Date(mPeriodStart.getTime()));
        if (mPeriodType == PeriodType.WEEK) {
            repeatBuilder.append(" ").
                    append(context.getString(R.string.repeat_on_weekday, dayOfWeek));
        }

        if (mPeriodEnd != null){
            String endDateString = SimpleDateFormat.getDateInstance().format(new Date(mPeriodEnd.getTime()));
            repeatBuilder.append(", ").append(context.getString(R.string.repeat_until_date, endDateString));
        }
        return repeatBuilder.toString();
    }

    /**
     * Creates an RFC 2445 string which describes this recurring event.
     * <p>See http://recurrance.sourceforge.net/</p>
     * <p>The output of this method is not meant for human consumption</p>
     * @return String describing event
     */
    public String getRuleString(){
        String separator = ";";

        StringBuilder ruleBuilder = new StringBuilder();

//        =======================================================================
        //This section complies with the formal rules, but the betterpickers library doesn't like/need it

//        SimpleDateFormat startDateFormat = new SimpleDateFormat("'TZID'=zzzz':'yyyyMMdd'T'HHmmss", Locale.US);
//        ruleBuilder.append("DTSTART;");
//        ruleBuilder.append(startDateFormat.format(new Date(mStartDate)));
//            ruleBuilder.append("\n");
//        ruleBuilder.append("RRULE:");
//        ========================================================================


        ruleBuilder.append("FREQ=").append(mPeriodType.getFrequencyDescription()).append(separator);
        ruleBuilder.append("INTERVAL=").append(mMultiplier).append(separator);
        if (getCount() > 0)
            ruleBuilder.append("COUNT=").append(getCount()).append(separator);
        ruleBuilder.append(mPeriodType.getByParts(mPeriodStart.getTime())).append(separator);

        return ruleBuilder.toString();
    }

    /**
     * Return the number of days left in this period
     * @return Number of days left in period
     */
    public int getDaysLeftInCurrentPeriod(){
        LocalDateTime startDate = new LocalDateTime(System.currentTimeMillis());
        int interval = mMultiplier - 1;
        LocalDateTime endDate = null;
        switch (mPeriodType){
            case HOUR:
                endDate = new LocalDateTime(System.currentTimeMillis()).plusHours(interval);
                break;
            case DAY:
                endDate = new LocalDateTime(System.currentTimeMillis()).plusDays(interval);
                break;
            case WEEK:
                endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(interval);
                break;
            case MONTH:
                endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(interval);
                break;
            case YEAR:
                endDate = startDate.dayOfYear().withMaximumValue().plusYears(interval);
                break;
        }

        return Days.daysBetween(startDate, endDate).getDays();
    }

    /**
     * Returns the number of periods from the start date of this recurrence until the end of the
     * interval multiplier specified in the {@link PeriodType}
     * //fixme: Improve the documentation
     * @return Number of periods in this recurrence
     */
    public int getNumberOfPeriods(int numberOfPeriods) {
        LocalDateTime startDate = new LocalDateTime(mPeriodStart.getTime());
        LocalDateTime endDate;
        int interval = mMultiplier;
        //// TODO: 15.08.2016 Why do we add the number of periods. maybe rename method or param
        switch (mPeriodType){
            case HOUR: //this is not the droid you are looking for
                endDate = startDate.plusHours(numberOfPeriods);
                return Hours.hoursBetween(startDate, endDate).getHours();
            case DAY:
                endDate = startDate.plusDays(numberOfPeriods);
                return Days.daysBetween(startDate, endDate).getDays();
            case WEEK:
                endDate = startDate.dayOfWeek().withMaximumValue().plusWeeks(numberOfPeriods);
                return Weeks.weeksBetween(startDate, endDate).getWeeks() / interval;
            case MONTH:
                endDate = startDate.dayOfMonth().withMaximumValue().plusMonths(numberOfPeriods);
                return Months.monthsBetween(startDate, endDate).getMonths() / interval;
            case YEAR:
                endDate = startDate.dayOfYear().withMaximumValue().plusYears(numberOfPeriods);
                return Years.yearsBetween(startDate, endDate).getYears() / interval;
        }

        return 0;
    }

    /**
     * Return the name of the current period
     * @return String of current period
     */
    public String getTextOfCurrentPeriod(int periodNum){
        LocalDate startDate = new LocalDate(mPeriodStart.getTime());
        switch (mPeriodType){
            case HOUR:
                //nothing to see here. Just use default period designation
                break;
            case DAY:
                return startDate.dayOfWeek().getAsText();
            case WEEK:
                return startDate.weekOfWeekyear().getAsText();
            case MONTH:
                return startDate.monthOfYear().getAsText();
            case YEAR:
                return startDate.year().getAsText();
        }
        return "Period " + periodNum;
    }

    /**
     * Return the days of week on which to run the recurrence.
     *
     * <p>Days are expressed as defined in {@link java.util.Calendar}.
     * For example, Calendar.MONDAY</p>
     *
     * @return list of days of week on which to run the recurrence.
     */
    public @NonNull List<Integer> getByDays(){
        return Collections.unmodifiableList(mByDays);
    }

    /**
     * Sets the days on which to run the recurrence.
     *
     * <p>Days must be expressed as defined in {@link java.util.Calendar}.
     * For example, Calendar.MONDAY</p>
     *
     * @param byDays list of days of week on which to run the recurrence.
     */
    public void setByDays(@NonNull List<Integer> byDays){
        mByDays = new ArrayList<>(byDays);
    }

    /**
     * Computes the number of occurrences of this recurrences between start and end date
     * <p>If there is no end date or the PeriodType is unknown, it returns -1</p>
     * @return Number of occurrences, or -1 if there is no end date
     */
    public int getCount(){
        if (mPeriodEnd == null)
            return -1;

        int multiple = mMultiplier;
        ReadablePeriod jodaPeriod;
        switch (mPeriodType){
            case HOUR:
                jodaPeriod = Hours.hours(multiple);
                break;
            case DAY:
                jodaPeriod = Days.days(multiple);
                break;
            case WEEK:
                jodaPeriod = Weeks.weeks(multiple);
                break;
            case MONTH:
                jodaPeriod = Months.months(multiple);
                break;
            case YEAR:
                jodaPeriod = Years.years(multiple);
                break;
            default:
                jodaPeriod = Months.months(multiple);
        }
        int count = 0;
        LocalDateTime startTime = new LocalDateTime(mPeriodStart.getTime());
        while (startTime.toDateTime().getMillis() < mPeriodEnd.getTime()){
            ++count;
            startTime = startTime.plus(jodaPeriod);
        }
        return count;

/*
        //this solution does not use looping, but is not very accurate

        int multiplier = mMultiplier;
        LocalDateTime startDate = new LocalDateTime(mPeriodStart.getTime());
        LocalDateTime endDate = new LocalDateTime(mPeriodEnd.getTime());
        switch (mPeriodType){
            case DAY:
                return Days.daysBetween(startDate, endDate).dividedBy(multiplier).getDays();
            case WEEK:
                return Weeks.weeksBetween(startDate, endDate).dividedBy(multiplier).getWeeks();
            case MONTH:
                return Months.monthsBetween(startDate, endDate).dividedBy(multiplier).getMonths();
            case YEAR:
                return Years.yearsBetween(startDate, endDate).dividedBy(multiplier).getYears();
            default:
                return -1;
        }
*/
    }

    /**
     * Sets the end time of this recurrence by specifying the number of occurences
     * @param numberOfOccurences Number of occurences from the start time
     */
    public void setPeriodEnd(int numberOfOccurences){
        LocalDateTime localDate = new LocalDateTime(mPeriodStart.getTime());
        LocalDateTime endDate;
        int occurrenceDuration = numberOfOccurences * mMultiplier;
        switch (mPeriodType){
            case HOUR:
                endDate = localDate.plusHours(occurrenceDuration);
                break;
            case DAY:
                endDate = localDate.plusDays(occurrenceDuration);
                break;
            case WEEK:
                endDate = localDate.plusWeeks(occurrenceDuration);
                break;
            default:
            case MONTH:
                endDate = localDate.plusMonths(occurrenceDuration);
                break;
            case YEAR:
                endDate = localDate.plusYears(occurrenceDuration);
                break;
        }
        mPeriodEnd = new Timestamp(endDate.toDateTime().getMillis());
    }

    /**
     * Return the end date of the period in milliseconds
     * @return End date of the recurrence period
     */
    public Timestamp getPeriodEnd(){
        return mPeriodEnd;
    }

    /**
     * Set period end date
     * @param endTimestamp End time in milliseconds
     */
    public void setPeriodEnd(Timestamp endTimestamp){
        mPeriodEnd = endTimestamp;
    }

    /**
     * Returns the multiplier for the period type. The default multiplier is 1.
     * e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2.
     *
     * @return  Multiplier for the period type
     */
    public int getMultiplier(){
        return mMultiplier;
    }

    /**
     * Sets the multiplier for the period type.
     * e.g. bi-weekly actions have period type {@link PeriodType#WEEK} and multiplier 2.
     *
     * @param multiplier Multiplier for the period type
     */
    public void setMultiplier(int multiplier){
        mMultiplier = multiplier;
    }

    /**
     * Returns a localized string describing the period type's frequency.
     *
     * @return String describing the period type
     */
    private String getFrequencyRepeatString(){
        Resources res = GnuCashApplication.getAppContext().getResources();
        switch (mPeriodType) {
            case HOUR:
                return res.getQuantityString(R.plurals.label_every_x_hours, mMultiplier, mMultiplier);
            case DAY:
                return res.getQuantityString(R.plurals.label_every_x_days, mMultiplier, mMultiplier);
            case WEEK:
                return res.getQuantityString(R.plurals.label_every_x_weeks, mMultiplier, mMultiplier);
            case MONTH:
                return res.getQuantityString(R.plurals.label_every_x_months, mMultiplier, mMultiplier);
            case YEAR:
                return res.getQuantityString(R.plurals.label_every_x_years, mMultiplier, mMultiplier);
            default:
                return "";
        }
    }

    /**
     * Returns a new {@link Recurrence} with the {@link PeriodType} specified in the old format.
     *
     * @param period Period in milliseconds since Epoch (old format to define a period)
     * @return Recurrence with the specified period.
     */
    public static Recurrence fromLegacyPeriod(long period) {
        int result = (int) (period/RecurrenceParser.YEAR_MILLIS);
        if (result > 0) {
            Recurrence recurrence = new Recurrence(PeriodType.YEAR);
            recurrence.setMultiplier(result);
            return recurrence;
        }

        result = (int) (period/RecurrenceParser.MONTH_MILLIS);
        if (result > 0) {
            Recurrence recurrence = new Recurrence(PeriodType.MONTH);
            recurrence.setMultiplier(result);
            return recurrence;
        }

        result = (int) (period/RecurrenceParser.WEEK_MILLIS);
        if (result > 0) {
            Recurrence recurrence = new Recurrence(PeriodType.WEEK);
            recurrence.setMultiplier(result);
            return recurrence;
        }

        result = (int) (period/RecurrenceParser.DAY_MILLIS);
        if (result > 0) {
            Recurrence recurrence = new Recurrence(PeriodType.DAY);
            recurrence.setMultiplier(result);
            return recurrence;
        }

        result = (int) (period/RecurrenceParser.HOUR_MILLIS);
        if (result > 0) {
            Recurrence recurrence = new Recurrence(PeriodType.HOUR);
            recurrence.setMultiplier(result);
            return recurrence;
        }


        return new Recurrence(PeriodType.DAY);
    }
}
