import RDates: RDates, CalendarManager, RDate, Calendar, apply, NullCalendarManager
import Dates: Dates, Date

abstract type CalendarRule end
includehols(::CalendarRule, from::Date, to::Date) = repeat([false], (to-from).value + 1)
excludehols(::CalendarRule, from::Date, to::Date) = repeat([false], (to-from).value + 1)

"""
    WindowCalendarRule(rule; from::Union{Nothing,Date}=nothing, to::Union{Nothing,Date}=nothing)

Restrict another calendar rule to within a window of [from, to].
"""
struct WindowCalendarRule <: CalendarRule
    rule::CalendarRule
    from::Union{Nothing,Date}
    to::Union{Nothing,Date}

    WindowCalendarRule(rule; from=nothing, to=nothing) = new(rule, from, to)
end

function includehols(rule::WindowCalendarRule, from::Date, to::Date)
    rule.from != nothing && rule.from > to && return repeat([false], (to-from).value + 1)
    rule.to != nothing && rule.to < from && return repeat([false], (to-from).value + 1)
    hols = includehols(rule.rule, from, to)
    r1 = rule.from != nothing ? (1:(rule.from-from).value) : []
    r2 = rule.to != nothing ? ((rule.to-from).value+2:(to-from).vaue+1) : []
    for i in Iterators.flatten([r1,r2])
        @inbounds hols[i] = false
    end
    hols
end

function excludehols(rule::WindowCalendarRule, from::Date, to::Date)
    rule.from != nothing && rule.from > to && return repeat([false], (to-from).value + 1)
    rule.to != nothing && rule.to < from && return repeat([false], (to-from).value + 1)
    hols = excludehols(rule.rule, from, to)
    r1 = rule.from != nothing ? (1:(rule.from-from).value) : []
    r2 = rule.to != nothing ? ((rule.to-from).value+2:(to-from).vaue+1) : []
    for i in Iterators.flatten([r1,r2])
        @inbounds hols[i] = false
    end
    hols
end

"""
    PeriodicCalendarRule(period::RDate, increment::RDate; include::Bool=true, cal_mgr::CalendarManager=NullCalendarManager())

A calendar rule which is generated by iterating on a `period` and then applying an `increment`. To mark
the calendar rule is an exclusion then can set the `include` parameter to false.

```julia-repl
julia> PeriodicCalendarRule(rd"1y", rd"1MAY+Last MON");
```
"""
struct PeriodicCalendarRule <: CalendarRule
    period::RDate
    increment::RDate
    include::Bool
    cal_mgr::CalendarManager

    PeriodicCalendarRule(period::RDate, increment::RDate; include=true, cal_mgr=NullCalendarManager()) = new(period, increment, include, cal_mgr)
end

function holidays(rule::PeriodicCalendarRule, from::Date, to::Date)
    hols = repeat([false], (to-from).value + 1)
    for d in RDates.range(from, to, rule.period; cal_mgr=rule.cal_mgr)
        d1 = apply(rule.increment, d, rule.cal_mgr)
        if from <= d1 <= to
            @inbounds hols[(d1-from).value+1] = true
        end
    end
    hols
end
includehols(rule::PeriodicCalendarRule, from::Date, to::Date) = rule.include ? holidays(rule, from, to) : repeat([false], (to-from).value + 1)
excludehols(rule::PeriodicCalendarRule, from::Date, to::Date) = rule.include ? repeat([false], (to-from).value + 1) : holidays(rule, from, to)

"""
    ExplicitDateRule(;inclusions::Vector{Date}=[], exclusions::Vector{Date}=[])

A calendar rule to mark an explicit set of dates as holiday or not holidays.
"""
struct ExplicitDateRule <: CalendarRule
    inclusions::Vector{Date}
    exclusions::Vector{Date}
    ExplicitDateRule(;inclusions=Vector{Date}(), exclusions=Vector{Date}()) = new(inclusions, exclusions)
end

function includehols(rule::ExplicitDateRule, from::Date, to::Date)
    hols = repeat([false], (to-from).value + 1)
    for d in rule.inclusions
        if from <= d <= to
            @inbounds hols[(d-from).value+1] = true
        end
    end
    hols
end

function excludehols(rule::ExplicitDateRule, from::Date, to::Date)
    hols = repeat([false], (to-from).value + 1)
    for d in rule.exclusions
        if from <= d <= to
            @inbounds hols[(d-from).value+1] = true
        end
    end
    hols
end


"""
    RuleBasedCalendar(rules::Vector{CalendarRule})

A calendar system which is calculated by applying a set of of rules in order.
"""
struct RuleBasedCalendar <: Calendar
    rules::Vector{CalendarRule}
end

function RDates.holidays(cal::RuleBasedCalendar, from::Date, to::Date)
    hols = repeat([false], (to-from).value + 1)
    for rule in cal.rules
        hols = hols .| includehols(rule, from, to)
        hols = hols .& (.! excludehols(rule, from, to))
    end
    hols
end

function RDates.is_holiday(cal::RuleBasedCalendar, date::Date)
    year = Dates.year(date); from = Date(year,1,1); to = Date(year,12,31)
    holidays(cal, from, to)[(date-from).value + 1]
end
