/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.ofbiz.service.calendar;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.StringUtil;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;

import com.ibm.icu.util.Calendar;

/**
 * Recurrence Info Object
 */
public class RecurrenceInfo {

    public static final String module = RecurrenceInfo.class.getName();

    protected GenericValue info;
    protected Date startDate;
    protected List<RecurrenceRule> rRulesList;
    protected List<RecurrenceRule> eRulesList;
    protected List<Date> rDateList;
    protected List<Date> eDateList;

    /** Creates new RecurrenceInfo */
    public RecurrenceInfo(GenericValue info) throws RecurrenceInfoException {
        this.info = info;
        if (!"RecurrenceInfo".equals(info.getEntityName())) {
            throw new RecurrenceInfoException("Invalid RecurrenceInfo Value object.");
        }
        init();
    }

    /** Initializes the rules for this RecurrenceInfo object. */
    public void init() throws RecurrenceInfoException {

        if (info.get("startDateTime") == null) {
            throw new RecurrenceInfoException("Recurrence startDateTime cannot be null.");
        }

        // Get start date
        long startTime = info.getTimestamp("startDateTime").getTime();

        if (startTime > 0) {
            int nanos = info.getTimestamp("startDateTime").getNanos();

            startTime += (nanos / 1000000);
        } else {
            throw new RecurrenceInfoException("Recurrence startDateTime must have a value.");
        }
        startDate = new Date(startTime);

        // Get the recurrence rules objects
        try {
            rRulesList = new ArrayList<>();
            for (GenericValue value: info.getRelated("RecurrenceRule", null, null, false)) {
                rRulesList.add(new RecurrenceRule(value));
            }
        } catch (GenericEntityException gee) {
            rRulesList = null;
        } catch (RecurrenceRuleException rre) {
            throw new RecurrenceInfoException("Illegal rule format.", rre);
        }

        // Get the exception rules objects
        try {
            eRulesList = new ArrayList<>();
            for (GenericValue value: info.getRelated("ExceptionRecurrenceRule", null, null, false)) {
                eRulesList.add(new RecurrenceRule(value));
            }
        } catch (GenericEntityException gee) {
            eRulesList = null;
        } catch (RecurrenceRuleException rre) {
            throw new RecurrenceInfoException("Illegal rule format", rre);
        }

        // Get the recurrence date list
        rDateList = RecurrenceUtil.parseDateList(StringUtil.split(info.getString("recurrenceDateTimes"), ","));
        // Get the exception date list
        eDateList = RecurrenceUtil.parseDateList(StringUtil.split(info.getString("exceptionDateTimes"), ","));

        // Sort the lists.
        Collections.sort(rDateList);
        Collections.sort(eDateList);
    }

    /** Returns the primary key for this value object */
    public String getID() {
        return info.getString("recurrenceInfoId");
    }

    /** Returns the startDate Date object. */
    public Date getStartDate() {
        return (Date) this.startDate.clone();
    }

    /** Returns the long value of the startDate. */
    public long getStartTime() {
        return this.startDate.getTime();
    }

    /** Returns a recurrence rule iterator */
    public Iterator<RecurrenceRule> getRecurrenceRuleIterator() {
        return rRulesList.iterator();
    }

    /** Returns a sorted recurrence date iterator */
    public Iterator<Date> getRecurrenceDateIterator() {
        return rDateList.iterator();
    }

    /** Returns a exception recurrence iterator */
    public Iterator<RecurrenceRule> getExceptionRuleIterator() {
        return eRulesList.iterator();
    }

    /** Returns a sorted exception date iterator */
    public Iterator<Date> getExceptionDateIterator() {
        return eDateList.iterator();
    }

    /** Returns the current count of this recurrence. */
    public long getCurrentCount() {
        if (info.get("recurrenceCount") != null) {
            return info.getLong("recurrenceCount").longValue();
        }
        return 0;
    }

    /** Increments the current count of this recurrence and updates the record. */
    public void incrementCurrentCount() throws GenericEntityException {
        incrementCurrentCount(true);
    }

    /** Increments the current count of this recurrence. */
    public void incrementCurrentCount(boolean store) throws GenericEntityException {
        if (store) {
            info.set("recurrenceCount", getCurrentCount() + 1);
            info.store();
        }
    }

    /** Removes the recurrence from persistant store. */
    public void remove() throws RecurrenceInfoException {
        List<RecurrenceRule> rulesList = new ArrayList<>();

        rulesList.addAll(rRulesList);
        rulesList.addAll(eRulesList);

        try {
            for (RecurrenceRule rule: rulesList) {
                rule.remove();
            }
            info.remove();
        } catch (RecurrenceRuleException | GenericEntityException e) {
            throw new RecurrenceInfoException(e.getMessage(), e);
        }
    }

    /** Returns the first recurrence. */
    public long first() {
        return startDate.getTime();
        // First recurrence is always the start time
    }

    /** Returns the estimated last recurrence. */
    public long last() {
        // TODO: find the last recurrence.
        return 0;
    }

    /** Returns the next recurrence from now. */
    public long next() {
        return next(RecurrenceUtil.now());
    }

    /** Returns the next recurrence from the specified time. */
    public long next(long fromTime) {
        // Check for the first recurrence (StartTime is always the first recurrence)
        if (getCurrentCount() == 0 || fromTime == 0 || fromTime == startDate.getTime()) {
            return first();
        }

        if (Debug.verboseOn()) {
            Debug.logVerbose("Date List Size: " + (rDateList == null ? 0 : rDateList.size()), module);
            Debug.logVerbose("Rule List Size: " + (rRulesList == null ? 0 : rRulesList.size()), module);
        }

        // Check the rules and date list
        if (rDateList == null && rRulesList == null) {
            return 0;
        }

        long nextRuleTime = fromTime;
        boolean hasNext = true;

        // Get the next recurrence from the rule(s).
        Iterator<RecurrenceRule> rulesIterator = getRecurrenceRuleIterator();
        while (rulesIterator.hasNext()) {
            RecurrenceRule rule = rulesIterator.next();
            while (hasNext) {
                // Gets the next recurrence time from the rule.
                nextRuleTime = getNextTime(rule, nextRuleTime);
                // Tests the next recurrence against the rules.
                if (nextRuleTime == 0 || isValid(nextRuleTime)) {
                    hasNext = false;
                }
            }
        }
        return nextRuleTime;
    }

    /** Checks the current recurrence validity at the moment. */
    public boolean isValidCurrent() {
        return isValidCurrent(RecurrenceUtil.now());
    }

    /** Checks the current recurrence validity for checkTime. */
    public boolean isValidCurrent(long checkTime) {
        if (checkTime == 0 || (rDateList == null && rRulesList == null)) {
            return false;
        }

        boolean found = false;
        Iterator<RecurrenceRule> rulesIterator = getRecurrenceRuleIterator();
        while (rulesIterator.hasNext()) {
            RecurrenceRule rule = rulesIterator.next();
            long currentTime = rule.validCurrent(getStartTime(), checkTime, getCurrentCount());
            currentTime = checkDateList(rDateList, currentTime, checkTime);
            if ((currentTime > 0) && isValid(checkTime)) {
                found = true;
            } else {
                return false;
            }
        }

        return found;
    }

    private long getNextTime(RecurrenceRule rule, long fromTime) {
        long nextTime = rule.next(getStartTime(), fromTime, getCurrentCount());
        if (Debug.verboseOn()) {
            Debug.logVerbose("Next Time Before Date Check: " + nextTime, module);
        }
        return checkDateList(rDateList, nextTime, fromTime);
    }

    private long checkDateList(List<Date> dateList, long time, long fromTime) {
        long nextTime = time;

        if (UtilValidate.isNotEmpty(dateList)) {
            for (Date thisDate: dateList) {
                if (nextTime > 0 && thisDate.getTime() < nextTime && thisDate.getTime() > fromTime) {
                    nextTime = thisDate.getTime();
                } else if (nextTime == 0 && thisDate.getTime() > fromTime) {
                    nextTime = thisDate.getTime();
                }
            }
        }
        return nextTime;
    }

    private boolean isValid(long time) {
        Iterator<RecurrenceRule> exceptRulesIterator = getExceptionRuleIterator();

        while (exceptRulesIterator.hasNext()) {
            RecurrenceRule except = exceptRulesIterator.next();

            if (except.isValid(getStartTime(), time) || eDateList.contains(new Date(time))) {
                return false;
            }
        }
        return true;
    }

    public String primaryKey() {
        return info.getString("recurrenceInfoId");
    }

    public static RecurrenceInfo makeInfo(Delegator delegator, long startTime, int frequency,
            int interval, int count) throws RecurrenceInfoException {
        return makeInfo(delegator, startTime, frequency, interval, count, 0);
    }

    public static RecurrenceInfo makeInfo(Delegator delegator, long startTime, int frequency,
            int interval, long endTime) throws RecurrenceInfoException {
        return makeInfo(delegator, startTime, frequency, interval, -1, endTime);
    }

    public static RecurrenceInfo makeInfo(Delegator delegator, long startTime, int frequency,
            int interval, int count, long endTime) throws RecurrenceInfoException {
        try {
            RecurrenceRule r = RecurrenceRule.makeRule(delegator, frequency, interval, count, endTime);
            String ruleId = r.primaryKey();
            GenericValue value = delegator.makeValue("RecurrenceInfo");

            value.set("recurrenceRuleId", ruleId);
            value.set("startDateTime", new java.sql.Timestamp(startTime));
            delegator.createSetNextSeqId(value);
            RecurrenceInfo newInfo = new RecurrenceInfo(value);

            return newInfo;
        } catch (RecurrenceRuleException | GenericEntityException e) {
            throw new RecurrenceInfoException(e.getMessage(), e);
        } catch (RecurrenceInfoException rie) {
            throw rie;
        }
    }

    /** Convert a RecurrenceInfo object to a TemporalExpression object.
     * @param info A RecurrenceInfo instance
     * @return A TemporalExpression instance
     */
    public static TemporalExpression toTemporalExpression(RecurrenceInfo info) {
        if (info == null) {
            throw new IllegalArgumentException("info argument cannot be null");
        }
        return new RecurrenceWrapper(info);
    }

    /** Wraps a RecurrenceInfo object with a TemporalExpression object. This
     * class is intended to help with the transition from RecurrenceInfo/RecurrenceRule
     * to TemporalExpression.
     */
    @SuppressWarnings("serial")
    protected static class RecurrenceWrapper extends TemporalExpression {
        protected RecurrenceInfo info;
        protected RecurrenceWrapper() {}
        public RecurrenceWrapper(RecurrenceInfo info) {
            this.info = info;
        }
        @Override
        public Calendar first(Calendar cal) {
            long result = this.info.first();
            if (result == 0) {
                return null;
            }
            Calendar first = (Calendar) cal.clone();
            first.setTimeInMillis(result);
            return first;
        }
        @Override
        public boolean includesDate(Calendar cal) {
            return this.info.isValidCurrent(cal.getTimeInMillis());
        }
        @Override
        public Calendar next(Calendar cal, ExpressionContext context) {
            long result = this.info.next(cal.getTimeInMillis());
            if (result == 0) {
                return null;
            }
            Calendar next = (Calendar) cal.clone();
            next.setTimeInMillis(result);
            return next;
        }
        @Override
        public void accept(TemporalExpressionVisitor visitor) {}
        @Override
        public boolean isSubstitutionCandidate(Calendar cal, TemporalExpression expressionToTest) {
            return false;
        }
    }
}
