/**
 * ***************************************************************************
 * Copyright (c) 2010 Qcadoo Limited
 * Project: Qcadoo MES
 * Version: 1.4
 *
 * This file is part of Qcadoo.
 *
 * Qcadoo is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation; either version 3 of the License,
 * or (at your option) any later version.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 * ***************************************************************************
 */
package com.qcadoo.mes.productionPerShift.dates;

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

import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.springframework.stereotype.Service;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.qcadoo.commons.dateTime.TimeRange;
import com.qcadoo.commons.functional.FluentOptional;
import com.qcadoo.commons.functional.LazyStream;
import com.qcadoo.mes.basic.shift.Shift;

@Service
public class OrderRealizationDaysResolver {

    public LazyStream<OrderRealizationDay> asStreamFrom(final DateTime orderStartDateTime, final List<Shift> shifts) {
        OrderRealizationDay firstDay = find(orderStartDateTime, 1, true, shifts);
        return LazyStream.create(firstDay,
                prevElement -> find(orderStartDateTime, prevElement.getRealizationDayNumber() + 1, false, shifts));
    }

    public OrderRealizationDay find(final DateTime orderStartDateTime, final int startingFrom, final boolean isFirstDay,
            final List<Shift> shifts) {
        OrderRealizationDay firstWorkingDay = findFirstWorkingDayFrom(orderStartDateTime.toLocalDate(), startingFrom, shifts);
        if (isFirstDay) {
            return tryResolveFirstDay(firstWorkingDay, orderStartDateTime).or(firstWorkingDay);
        }
        return firstWorkingDay;
    }

    private Optional<OrderRealizationDay> tryResolveFirstDay(final OrderRealizationDay firstWorkingDay,
            final DateTime orderStartDateTime) {
        LocalDate orderStartDate = orderStartDateTime.toLocalDate();
        LocalTime orderStartTime = orderStartDateTime.toLocalTime();

        Optional<Shift> shiftStartingOrder = firstWorkingDay.findShiftWorkingAt(orderStartTime);
        Optional<TimeRange> workTimeRange = FluentOptional.wrap(shiftStartingOrder).flatMap(shift -> {
            int prevDayOfWeek = firstWorkingDay.getDate().minusDays(1).getDayOfWeek();
            return shift.findWorkTimeAt(prevDayOfWeek, orderStartTime);
        }).toOpt();

        if (shiftWorkTimeMatchPredicate(shiftStartingOrder, workTimeRange, startsDayBefore(orderStartTime))) {
            return Optional.of(new OrderRealizationDay(orderStartDate, firstWorkingDay.getRealizationDayNumber() - 1,
                    Lists.newArrayList(shiftStartingOrder.asSet())));
        }
        if (shiftWorkTimeMatchPredicate(shiftStartingOrder, workTimeRange, endsNextDay(orderStartTime))) {
            return Optional.of(new OrderRealizationDay(firstWorkingDay.getDate(), firstWorkingDay.getRealizationDayNumber(),
                    Lists.newArrayList(shiftStartingOrder.asSet())));
        }
        return Optional.absent();
    }

    private boolean shiftWorkTimeMatchPredicate(final Optional<Shift> maybeShift, final Optional<TimeRange> workTime,
            final Function<TimeRange, Boolean> workTimePredicate) {
        return maybeShift.transform(shift -> workTime.transform(workTimePredicate).or(false)).or(false);
    }

    private Function<TimeRange, Boolean> endsNextDay(final LocalTime orderStartTime) {
        return timeRange -> timeRange.startsDayBefore() && !orderStartTime.isBefore(timeRange.getFrom());
    }

    private Function<TimeRange, Boolean> startsDayBefore(final LocalTime orderStartTime) {
        return timeRange -> timeRange.startsDayBefore() && !orderStartTime.isAfter(timeRange.getTo());
    }


    private OrderRealizationDay findFirstWorkingDayFrom(final LocalDate orderStartDate, final int startFrom,
            final List<Shift> shifts) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());

        int daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR);
        for (int offset = startFrom; offset < startFrom + daysInYear; offset++) {
            List<Shift> workingShifts = getShiftsWorkingAt(orderStartDate.plusDays(offset - 1).getDayOfWeek(), shifts);
            if (!workingShifts.isEmpty()) {
                return new OrderRealizationDay(orderStartDate, offset, workingShifts);
            }
        }
        // assuming that no working shifts in period of year length means that client does not use shifts calendar and there won't
        // be any offsets caused by work free time boundaries.
        return new OrderRealizationDay(orderStartDate, startFrom, Collections.<Shift> emptyList());
    }

    private List<Shift> getShiftsWorkingAt(final int dayOfWeek, final List<Shift> shifts) {
        return FluentIterable.from(shifts).filter(shift -> shift.worksAt(dayOfWeek)).toList();
    }

}
