---------------------------------------------------------------------------
-- Display a monthly or yearly calendar.
--
-- This module defines two widgets: a month calendar and a year calendar
--
-- The two widgets have a `date` property, in the form of
-- a table `{day=[number|nil], month=[number|nil], year=[number]}`.
--
-- The `year` widget displays the whole specified year, e.g. `{year=2006}`.
--
-- The `month` widget displays the calendar for the specified month, e.g. `{month=12, year=2006}`,
-- highlighting the specified day if the day is provided in the date, e.g. `{day=22, month=12, year=2006}`.
--
-- Cell and container styles can be overridden using the `fn_embed` callback function
-- which is called before adding the widgets to the layouts. The `fn_embed` function
-- takes three arguments, the original widget, the flag (`string` used to identified the widget)
-- and the date (`table`).
-- It returns another widget, embedding (and modifying) the original widget.
--
--@DOC_wibox_widget_defaults_calendar_EXAMPLE@
--
-- @author getzze
-- @copyright 2017 getzze
-- @widgetmod wibox.widget.calendar
---------------------------------------------------------------------------


local setmetatable = setmetatable
local string = string
local gtable = require("gears.table")
local vertical = require("wibox.layout.fixed").vertical
local grid = require("wibox.layout.grid")
local textbox = require("wibox.widget.textbox")
local bgcontainer = require("wibox.container.background")
local base = require("wibox.widget.base")
local beautiful = require("beautiful")

local calendar = { mt = {} }

local properties = { "date"        , "font"         , "spacing" , "week_numbers",
                     "start_sunday", "long_weekdays", "fn_embed", "flex_height",
                     "fill_month"  ,
                   }

--- The calendar font.
-- @beautiful beautiful.calendar_font
-- @tparam string font Font of the calendar

--- The calendar spacing.
-- @beautiful beautiful.calendar_spacing
-- @tparam number spacing Spacing of the grid (twice this value for inter-month spacing)

--- Display the calendar week numbers.
-- @beautiful beautiful.calendar_week_numbers
-- @param boolean Display week numbers

--- Start the week on Sunday.
-- @beautiful beautiful.calendar_start_sunday
-- @param boolean Start the week on Sunday

--- Format the weekdays with three characters instead of two
-- @beautiful beautiful.calendar_long_weekdays
-- @param boolean Use three characters for the weekdays instead of two

--- Allow cells to have flexible height.
-- Flexible height allow cells to adapt their height to fill the empty space at the bottom of the widget.
-- @beautiful beautiful.flex_height
-- @param boolean Cells can skretch to fill the empty space.

--- The calendar date.
--
-- E.g.. `{day=21, month=2, year=2005}`, `{month=2, year=2005}, {year=2005}`
-- @tparam[opt=nil] table|nil date
-- @tparam number date.year Date year
-- @tparam number|nil date.month Date month
-- @tparam number|nil date.day Date day
-- @propertytype nil The current date.
-- @property date

--- The calendar font.
--
-- Choose a monospace font for a better rendering.
--
--@DOC_wibox_widget_calendar_font_EXAMPLE@
--
-- @tparam[opt="Monospace 10"] font font Font of the calendar
-- @property font
-- @usebeautiful beautiful.calendar_font

--- The calendar spacing.
--
-- The spacing between cells in the month.
-- The spacing between months in a year calendar is twice this value.
-- @tparam[opt=5] number spacing Spacing of the grid
-- @property spacing
-- @negativeallowed false
-- @propertyunit pixel
-- @usebeautiful beautiful.calendar_spacing

--- Display the calendar week numbers.
--
--@DOC_wibox_widget_calendar_week_numbers_EXAMPLE@
--
-- @tparam[opt=false] boolean week_numbers Display week numbers
-- @property week_numbers
-- @usebeautiful beautiful.calendar_week_numbers

--- Start the week on Sunday.
--
--@DOC_wibox_widget_calendar_start_sunday_EXAMPLE@
--
-- @tparam[opt=false] boolean start_sunday Start the week on Sunday
-- @property start_sunday
-- @usebeautiful beautiful.calendar_start_sunday

--- Format the weekdays with three characters instead of two
--
--@DOC_wibox_widget_calendar_long_weekdays_EXAMPLE@
--
-- @tparam[opt=false] boolean long_weekdays Use three characters for the weekdays instead of two
-- @property long_weekdays
-- @usebeautiful beautiful.calendar_long_weekdays

--- The widget encapsulating function.
--
-- Function that takes a widget, flag (`string`) and date (`table`) as argument
-- and returns a widget encapsulating the input widget.
--
-- Default value: function (widget, flag, date) return widget end
--
-- It is used to add a container to the grid layout and to the cells:
--
--@DOC_wibox_widget_calendar_fn_embed_cell_EXAMPLE@
-- @tparam[opt=nil] function|nil fn_embed Function to embed the widget depending on its flag.
-- @functionparam widget widget
-- @functionparam string flag The type of widget. It is one of `"header"`, `"monthheader"`,
--  `"weeknumber"` `"weekday"`, `"focus"`, `"month"` or `"normal"`.
-- @functionparam table date A table with `day`, `month` and `year` keys.
-- @functionreturn widget A new widget to insert into the calendar.
-- @propertytype nil Use an uncustomized `wibox.widget.textbox`.
-- @property fn_embed

--- Allow cells to have flexible height
--
--@DOC_wibox_widget_calendar_flex_height_EXAMPLE@
--
-- @tparam[opt=false] boolean flex_height Allow flex height.
-- @property flex_height
-- @usebeautiful beautiful.flex_height

--- Make a textbox
-- @tparam string text Text of the textbox
-- @tparam string font Font of the text
-- @tparam boolean center Center the text horizontally
-- @treturn wibox.widget.textbox
local function make_cell(text, font, center)
    local w = textbox()
    w:set_markup(text)
    w:set_halign(center and "center" or "right")
    w:set_valign("center")
    w:set_font(font)
    return w
end

--- Create a grid layout with the month calendar
-- @tparam table props Table of calendar properties
-- @tparam table date Date table
-- @tparam number date.year Date year
-- @tparam number date.month Date month
-- @tparam number|nil date.day Date day
-- @treturn widget Grid layout
local function create_month(props, date, focus_date)
    focus_date = focus_date or date

    local fill_month      = props.fill_month
    local start_row       = 3
    local week_start      = props.start_sunday and 1 or 2
    local last_day        = os.date("*t", os.time{year=date.year, month=date.month+1, day=0})
    local month_days      = last_day.day
    local column_fday     = (last_day.wday - month_days + 1 - week_start ) % 7
    local prev_month_days = fill_month and column_fday > 0 and os.date("*t", os.time{year=date.year, month=date.month, day=0}).day

    local num_columns  = props.week_numbers and 8 or 7
    local start_column = num_columns - 6

    local num_rows
    if fill_month then
        num_rows = 6
    else
        -- Compute number of rows
        -- There are at least 4 weeks in a month
        num_rows     = 4
        -- On every month but february on non bisextile years
        if last_day.day > 28 then
            -- The number of days span over at least 5 weeks
            num_rows = num_rows + 1

            -- On month with 30+ days add 1 week if:
            -- - if 30 days and the first day is the last day of the week
            -- - if 31 days and the first days is at least the second to last day
            if column_fday >= 5 then
                if last_day.day == 30 and column_fday == 6 or last_day.day == 31 then
                    num_rows = num_rows + 1
                end
            end
        -- If the first day of february is anything but the first day of the week
        elseif column_fday > 1 then
            -- Span over 5 weeks
            num_rows = num_rows + 1
        end
    end

    -- Create grid layout
    local layout = grid()
    layout:set_expand(false)
    layout:set_homogeneous(false)
    layout:set_spacing(props.spacing)
    layout:set_forced_num_rows(num_rows)
    layout:set_forced_num_cols(num_columns)

    --local flags = {"header", "weekdays", "weeknumber", "normal", "focus"}
    local cell_date, t, i, j, w, flag, flag_ext, text

    -- Header
    flag = "header"
    t = os.time{year=date.year, month=date.month, day=1}
    if props.subtype=="monthheader" then
        flag = "monthheader"
        text = os.date("%B", t)
    else
        text = os.date("%B %Y", t)
    end
    w = props.fn_embed(make_cell(text, props.font, true), flag, date)
    layout:add_widget_at(w, 1, 1, 1, num_columns)

    -- Days
    i = start_row
    j = start_column + (fill_month and 0 or column_fday)
    local current_week = nil
    local drawn_weekdays = 0
    local from = fill_month and (1 - column_fday) or 1
    local to = fill_month and (7 * 6 - column_fday) or month_days
    for d=from, to do
        if d < 1 then
            cell_date = {year=date.year, month=date.month-1, day=d+prev_month_days}
        elseif d > month_days then
            cell_date = {year=date.year, month=date.month+1, day=d-month_days}
        else
            cell_date = {year=date.year, month=date.month, day=d}
        end
        t = os.time(cell_date)
        -- Week number
        if props.week_numbers then
            text = os.date("%V", t)
            if tonumber(text) ~= current_week then
                flag = "weeknumber"
                w = props.fn_embed(make_cell(text, props.font), flag, cell_date)
                layout:add_widget_at(w, i, 1, 1, 1)
                current_week = tonumber(text)
            end
        end
        -- Week days
        if drawn_weekdays < 7 then
            flag = "weekday"
            text = os.date("%a", t)
            if not props.long_weekdays then
                text = string.sub(text, 1, 2)
            end
            w = props.fn_embed(make_cell(text, props.font), flag, cell_date)
            layout:add_widget_at(w, 2, j, 1, 1)
            drawn_weekdays = drawn_weekdays +1
        end
        -- Normal day
        flag = "normal"
        text = string.format("%2d", cell_date.day)
        -- Focus day
        if focus_date.day == cell_date.day and focus_date.month == cell_date.month and focus_date.year == cell_date.year then
            flag = "focus"
            text = "<b>"..text.."</b>"
        end
        flag_ext = cell_date.month == date.month and "" or "_other"
        w = props.fn_embed(make_cell(text, props.font), flag .. flag_ext, cell_date)
        layout:add_widget_at(w, i, j, 1, 1)

        -- find next cell
        i,j = layout:get_next_empty(i,j)
        if j < start_column then j = start_column end
    end
    return props.fn_embed(layout, "month", date)
end


--- Create a grid layout for the year calendar
-- @tparam table props Table of year calendar properties
-- @tparam number|string date Year to display.
-- @treturn widget Grid layout
local function create_year(props, date)
    -- Create a grid widget with the 12 months
    local in_layout = grid()
    in_layout:set_expand(true)
    in_layout:set_homogeneous(true)
    in_layout:set_spacing(2*props.spacing)
    in_layout:set_forced_num_cols(4)
    in_layout:set_forced_num_rows(3)

    local month_date
    local current_date = os.date("*t")

    for month=1,12 do
        if date.year == current_date.year and month == current_date.month then
            month_date = {day=current_date.day, month=current_date.month, year=current_date.year}
        else
            month_date = {month=month, year=date.year}
        end
        in_layout:add(create_month(props, month_date))
    end

    -- Create a vertical layout
    local flag, text = "yearheader", string.format("%s", date.year)
    local year_header = props.fn_embed(make_cell(text, props.font, true), flag, date)
    local out_layout = vertical()
    out_layout:set_spacing(2*props.spacing) -- separate header from calendar grid
    out_layout:add(year_header)
    out_layout:add(in_layout)
    return props.fn_embed(out_layout, "year", date)
end


--- Set the container to the current date
-- @param self Widget to update
local function fill_container(self)
    local date = self._private.date
    local focus_date = self._private.focus_date
    if date then
        -- Create calendar grid
        if self._private.type == "month" then
            self._private.container:set_widget(create_month(self._private, date, focus_date))
        elseif self._private.type == "year" then
            self._private.container:set_widget(create_year(self._private, date))
        end
    else
        self._private.container:set_widget(nil)
    end
    self:emit_signal("widget::layout_changed")
end


-- Set the calendar date
function calendar:set_date(date, focus_date)
    if date ~= self._private.date or focus_date ~= self._private.focus_date then
        self._private.date = date
        self._private.focus_date = focus_date
        -- (Re)create calendar grid
        fill_container(self)
    end
end


-- Build properties function
for _, prop in ipairs(properties) do
    -- setter
    if not calendar["set_" .. prop] then
        calendar["set_" .. prop] = function(self, value)
            if (string.sub(prop,1,3)=="fn_" and type(value) == "function") or self._private[prop] ~= value then
                self._private[prop] = value
                -- (Re)create calendar grid
                fill_container(self)
            end
        end
    end
    -- getter
    if not calendar["get_" .. prop] then
        calendar["get_" .. prop] = function(self)
            return self._private[prop]
        end
    end
end


--- Return a new calendar widget by type.
--
-- @tparam string type Type of the calendar, `year` or `month`
-- @tparam table date Date of the calendar
-- @tparam number date.year Date year
-- @tparam number|nil date.month Date month
-- @tparam number|nil date.day Date day
-- @tparam[opt="Monospace 10"] string font Font of the calendar
-- @treturn widget The calendar widget
local function get_calendar(type, date, font)
    local ct = bgcontainer()
    local ret = base.make_widget(ct, "calendar", {enable_properties = true})
    gtable.crush(ret, calendar, true)

    ret._private.type = type
    ret._private.container = ct

    -- default values
    ret._private.date = date
    ret._private.font = font or beautiful.calendar_font or "Monospace 10"

    ret._private.spacing       = beautiful.calendar_spacing or 5
    ret._private.week_numbers  = beautiful.calendar_week_numbers or false
    ret._private.start_sunday  = beautiful.calendar_start_sunday or false
    ret._private.long_weekdays = beautiful.calendar_long_weekdays or false
    ret._private.flex_height   = beautiful.calendar_flex_height or false
    ret._private.fill_month    = beautiful.calendar_fill_month or false
    ret._private.fn_embed      = function (w, _) return w end

    -- header specific
    ret._private.subtype = type=="year" and "monthheader" or "fullheader"

    fill_container(ret)
    return ret
end

--- A month calendar widget.
--
-- A calendar widget is a grid containing the calendar for one month.
-- If the day is specified in the date, its cell is highlighted.
--
--@DOC_wibox_widget_calendar_month_EXAMPLE@
-- @tparam table date Date of the calendar
-- @tparam number date.year Date year
-- @tparam number date.month Date month
-- @tparam number|nil date.day Date day
-- @tparam[opt="Monospace 10"] string font Font of the calendar
-- @treturn widget The month calendar widget
-- @constructorfct wibox.widget.calendar.month
function calendar.month(date, font)
    return get_calendar("month", date, font)
end

--- A year calendar widget.
--
-- A calendar widget is a grid containing the calendar for one year.
--
--@DOC_wibox_widget_calendar_year_EXAMPLE@
-- @tparam table date Date of the calendar
-- @tparam number date.year Date year
-- @tparam number|nil date.month Date month
-- @tparam number|nil date.day Date day
-- @tparam[opt="Monospace 10"] string font Font of the calendar
-- @treturn widget The year calendar widget
-- @constructorfct wibox.widget.calendar.year
function calendar.year(date, font)
    return get_calendar("year", date, font)
end


return setmetatable(calendar, calendar.mt)

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
