/*
 * Created on 18.10.2004
 */
package net.sourceforge.ganttproject.calendar;

import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import net.sourceforge.ganttproject.GanttCalendar;
import net.sourceforge.ganttproject.GanttProject;
import net.sourceforge.ganttproject.language.GanttLanguage;
import net.sourceforge.ganttproject.parser.HolidayTagHandler;
import net.sourceforge.ganttproject.time.TimeUnit;
import net.sourceforge.ganttproject.time.TimeUnitStack;
import net.sourceforge.ganttproject.time.TimeUnitStack.Listener;

/**
 * @author bard
 */
public class WeekendCalendarImpl extends GPCalendarBase implements GPCalendar {

    private final Calendar myCalendar = GanttLanguage.getInstance().newCalendar();

    private DayType[] myTypes = new DayType[7];

    private int myWeekendDaysCount;

    private Set<Date> publicHolidaysArray = new LinkedHashSet<Date>();

    private final Set<Date> myStableHolidays = new LinkedHashSet<Date>();
    
    private AlwaysWorkingTimeCalendarImpl myRestlessCalendar = new AlwaysWorkingTimeCalendarImpl();
    
    private Date myBusinessDayStart;
    
    private Date myBusinessDayEnd;
    
    private IntervalManager myIntervalManager;

    private TimeUnit myDefaultUnit;

    public WeekendCalendarImpl() {
        for (int i = 0; i < myTypes.length; i++) {
            myTypes[i] = GPCalendar.DayType.WORKING;
        }
        setWeekDayType(GregorianCalendar.SATURDAY, GPCalendar.DayType.WEEKEND);
        setWeekDayType(GregorianCalendar.SUNDAY, GPCalendar.DayType.WEEKEND);
    }

    public List<GPCalendarActivity> getActivities(Date startDate, Date endDate, TimeUnit framer) {
        if (myWeekendDaysCount == 0) {
            return myRestlessCalendar.getActivities(startDate, endDate);
        }
        List<GPCalendarActivity> result = new ArrayList<GPCalendarActivity>();
        Date curDayStart = framer.adjustLeft(startDate);
        boolean isWeekendState = isNonWorkingDay(curDayStart);
        // System.err.println("getActivities(): start="+startDate+"
        // end="+endDate);
        while (curDayStart.before(endDate)) {
            // System.err.println("curDayStart="+curDayStart);
            Date changeStateDayStart = getStateChangeDate(curDayStart,
                    !isWeekendState);
            // System.err.println("changeStateDayStart="+changeStateDayStart);
            if (changeStateDayStart.before(endDate)) {
                result.add(new CalendarActivityImpl(curDayStart,
                        changeStateDayStart, !isWeekendState));
                curDayStart = changeStateDayStart;
                isWeekendState = !isWeekendState;
                continue;
            } else {
                result.add(new CalendarActivityImpl(curDayStart, endDate,
                        !isWeekendState));
                break;
            }
        }
        return result;                
    }
//    public List<GPCalendarActivity> getActivities(Date startDate, Date endDate, TimeUnit timeUnit) {
//        return getActivities(startDate, endDate, (DateFrameable)timeUnit);
//    }
    
    public boolean isWeekend(Date curDayStart) {
        myCalendar.setTime(curDayStart);
        int dayOfWeek = myCalendar.get(Calendar.DAY_OF_WEEK);
        return myTypes[dayOfWeek - 1] == GPCalendar.DayType.WEEKEND;
    }

    private Date getStateChangeDate(Date startDate, boolean changeToWeekend) {
        return getStateChangeDate(startDate, myDefaultUnit, changeToWeekend, true);
    }

    private Date getStateChangeDate(Date startDate, TimeUnit timeUnit,
            boolean changeToWeekend, boolean moveRightNotLeft) {
        Date result = startDate;
        do {
            result = moveRightNotLeft ? timeUnit.adjustRight(result)
                    : timeUnit.jumpLeft(result);
            if ("Tue Feb 14 01:00:00 GMT 2006".equals(result.toString())) {
                throw new IllegalStateException("time unit="+timeUnit);
            }
        } while (changeToWeekend ^ isNonWorkingDay(result)); 
        return result;
    }

    protected List<GPCalendarActivity> getActivitiesForward(Date startDate, TimeUnit timeUnit,
            long unitCount) {
        List<GPCalendarActivity> result = new ArrayList<GPCalendarActivity>();
        Date unitStart = timeUnit.adjustLeft(startDate);
        while (unitCount > 0) {
            boolean isWeekendState = isNonWorkingDay(unitStart);
            if (isWeekendState) {
                Date workingUnitStart = getStateChangeDate(unitStart, timeUnit,
                        false, true);
                result.add(new CalendarActivityImpl(unitStart,
                        workingUnitStart, false));
                unitStart = workingUnitStart;
                continue;
            }
            Date nextUnitStart = timeUnit.adjustRight(unitStart);
            result.add(new CalendarActivityImpl(unitStart, nextUnitStart,
                            true));
            unitStart = nextUnitStart;
            unitCount--;
        }
        return result;
    }

    protected List<GPCalendarActivity> getActivitiesBackward(Date startDate, TimeUnit timeUnit,
            long unitCount) {
        List<GPCalendarActivity> result = new LinkedList<GPCalendarActivity>();
        Date unitStart = timeUnit.adjustLeft(startDate);
        while (unitCount > 0) {
            Date prevUnitStart = timeUnit.jumpLeft(unitStart);
            boolean isWeekendState = isNonWorkingDay(prevUnitStart);
            if (isWeekendState) {
                Date lastWorkingUnitStart = getStateChangeDate(prevUnitStart,
                        timeUnit, false, false);
                Date firstWeekendUnitStart = timeUnit
                        .adjustRight(lastWorkingUnitStart);
                Date lastWeekendUnitEnd = unitStart;
                result.add(0, new CalendarActivityImpl(firstWeekendUnitStart,
                        lastWeekendUnitEnd, false));
                unitStart = firstWeekendUnitStart;
            } else {
                result.add(0, new CalendarActivityImpl(prevUnitStart,
                        unitStart, true));
                unitCount--;
                unitStart = prevUnitStart;
            }
        }
        return result;
    }

    public void setWeekDayType(int day, DayType type) {
        if (type != myTypes[day - 1]) {
            myWeekendDaysCount += (type == DayType.WEEKEND ? 1 : -1);
        }
        myTypes[day - 1] = type;
    }

    public DayType getWeekDayType(int day) {
        return myTypes[day - 1];
    }

    public Date findClosestWorkingTime(Date time) {
        if (myWeekendDaysCount == 0) {
            return time;
        }
        if (!isNonWorkingDay(time)) {
            return time;
        }
        return getStateChangeDate(time, false);
    }

    public void setPublicHoliDayType(int month, int date) {
        setPublicHoliDayType(new GanttCalendar(1, month - 1, date).getTime());
        myStableHolidays.add(new GanttCalendar(1, month - 1, date).getTime());
    }

    public void setPublicHoliDayType(Date curDayStart) {
        publicHolidaysArray.add(curDayStart);
    }

    public boolean isPublicHoliDay(Date curDayStart) {
        boolean result = publicHolidaysArray.contains(curDayStart);
        if (!result) {
            result = myStableHolidays.contains(new GanttCalendar(1, curDayStart.getMonth(), curDayStart.getDate()).getTime()); 
        }
        return result;
    }
    
    public DayType getDayTypeDate(Date curDayStart, TimeUnit timeUnit) {
        if(isWeekend(curDayStart)){            
            return GPCalendar.DayType.WEEKEND;
        }
        if (isPublicHoliDay(curDayStart)) {            
            return GPCalendar.DayType.HOLIDAY;
        }
        if ("hour".equals(timeUnit.getName()) && myBusinessDayStart!=null) {
            return  curDayStart.getHours()>=myBusinessDayStart.getHours() &&
                    curDayStart.getHours()<myBusinessDayEnd.getHours() ? GPCalendar.DayType.WORKING : GPCalendar.DayType.WEEKEND;
        }
        return GPCalendar.DayType.WORKING;
    }

    public boolean isNonWorkingDay(Date time) {
        return 
            isWeekend(time) ||
            isPublicHoliDay(time) ||
            isOutOfBusinessHours(time);
    }

    public boolean isOutOfBusinessHours(Date time) {
        if (myBusinessDayEnd==null || myBusinessDayStart==null) {
            return false;
        }
        return time.getHours() >= myBusinessDayEnd.getHours() || time.getHours() < myBusinessDayStart.getHours(); 
    }

    public void setPublicHolidays(URL calendar, GanttProject gp) {
        publicHolidaysArray.clear();
        if (calendar != null) {
            XMLCalendarOpen opener = new XMLCalendarOpen();

            HolidayTagHandler dependencyHandler = new HolidayTagHandler(gp);

            opener.addTagHandler(dependencyHandler);
            opener.addParsingListener(dependencyHandler);
            try {
                opener.load(calendar.openStream());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    }

    public Collection<Date> getPublicHolidays() {
        return publicHolidaysArray;
    }

    public void setBusinessHours(Date businessDayStart, Date businessDayEnd) {
        myBusinessDayStart = businessDayStart;
        myBusinessDayEnd = businessDayEnd;
    }

    public Date getBusinessDayEnd() {
        return myBusinessDayEnd;
    }

    public Date getBusinessDayStart() {
        return myBusinessDayStart;
    }
    
    public IntervalManager getIntervalManager(){
        return myIntervalManager;
    }
    
    public void setIntervalManager(IntervalManager intervalManager){
        myIntervalManager = intervalManager;
    }

    public void setDefaultUnit(TimeUnit defaultTimeUnit) {
        myDefaultUnit = defaultTimeUnit;
    }
}
