#!/usr/pkg/bin/python3.13
# coding=utf-8
#
# Copyright (C) 2008 Aurelio A. Heckert <aurium(a)gmail.com>
#    Week number option added by Olav Vitters and Nicolas Dufour (2012)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
"""
A calendar generator plugin for Inkscape, but also can be used as a standalone
command line application.
#
More on ISO week number calculation on:
http://en.wikipedia.org/wiki/ISO_week_date
(The first week of a year is the week that contains the first Thursdayof the year.)
"""

__version__ = "0.4"

import datetime
import calendar
import re
import sys

import inkex
from inkex import TextElement
from inkex.localization import inkex_gettext as _

if sys.version_info[0] > 2:

    def unicode(stringlike, encoding):
        """Compatibility for python 2 strings"""
        if isinstance(stringlike, bytes):
            return stringlike.decode(encoding)
        return stringlike


class Calendar(inkex.EffectExtension):
    """Generate Calendar in SVG"""

    def add_arguments(self, pars):
        pars.add_argument("--tab", type=str, dest="tab")
        pars.add_argument(
            "--month",
            type=int,
            default=0,
            help="Month to be generated. If 0, then the entry year will be generated.",
        )
        pars.add_argument(
            "--year",
            type=int,
            default=0,
            help="Year to be generated. If 0, then the current year will be generated.",
        )
        pars.add_argument(
            "--fill-empty-day-boxes",
            type=inkex.Boolean,
            dest="fill_edb",
            default=True,
            help="Fill empty day boxes with next month days.",
        )
        pars.add_argument(
            "--show-week-number",
            type=inkex.Boolean,
            dest="show_weeknr",
            default=False,
            help="Include a week number column.",
        )
        pars.add_argument(
            "--start-day", default="sun", help='Week start day. ("sun" or "mon")'
        )
        pars.add_argument(
            "--weekend",
            default="sat+sun",
            help='Define the weekend days. ("sat+sun" or "sat" or "sun")',
        )
        pars.add_argument(
            "--auto-organize",
            type=inkex.Boolean,
            dest="auto_organize",
            default=True,
            help="Automatically set the size and positions.",
        )
        pars.add_argument(
            "--months-per-line",
            type=int,
            dest="months_per_line",
            default=3,
            help="Number of months side by side.",
        )
        pars.add_argument(
            "--month-width",
            type=str,
            dest="month_width",
            default="6cm",
            help="The width of the month days box.",
        )
        pars.add_argument(
            "--month-margin",
            type=str,
            dest="month_margin",
            default="1cm",
            help="The space between the month boxes.",
        )
        pars.add_argument(
            "--color-year",
            type=inkex.Color,
            dest="color_year",
            default=inkex.Color(0x808080FF),
            help="Color for the year header.",
        )
        pars.add_argument(
            "--color-month",
            type=inkex.Color,
            dest="color_month",
            default=inkex.Color(0x686868FF),
            help="Color for the month name header.",
        )
        pars.add_argument(
            "--color-day-name",
            type=inkex.Color,
            dest="color_day_name",
            default=inkex.Color(0x909090FF),
            help="Color for the week day names header.",
        )
        pars.add_argument(
            "--color-day",
            type=inkex.Color,
            dest="color_day",
            default=inkex.Color(0x000000FF),
            help="Color for the common day box.",
        )
        pars.add_argument(
            "--color-weekend",
            type=inkex.Color,
            dest="color_weekend",
            default=inkex.Color(0x787878FF),
            help="Color for the weekend days.",
        )
        pars.add_argument(
            "--color-nmd",
            type=inkex.Color,
            dest="color_nmd",
            default=inkex.Color(0xB0B0B0FF),
            help="Color for the next month day, in empty day boxes.",
        )
        pars.add_argument(
            "--color-weeknr",
            type=inkex.Color,
            dest="color_weeknr",
            default=inkex.Color(0x787878FF),
            help="Color for the week numbers.",
        )
        pars.add_argument(
            "--font-year",
            type=str,
            dest="font_year",
            default="arial",
            help="Font for the year string.",
        )
        pars.add_argument(
            "--font-month",
            type=str,
            dest="font_month",
            default="arial",
            help="Font for the month strings.",
        )
        pars.add_argument(
            "--font-day-name",
            type=str,
            dest="font_day_name",
            default="arial",
            help="Font for the days of the week strings.",
        )
        pars.add_argument(
            "--font-day",
            type=str,
            dest="font_day",
            default="arial",
            help="Font for the day strings.",
        )
        pars.add_argument(
            "--month-names",
            type=str,
            dest="month_names",
            default="January February March "
            "April May June July "
            "August September October "
            "November December",
            help="The month names for localization.",
        )
        pars.add_argument(
            "--day-names",
            type=str,
            dest="day_names",
            default="Sun Mon Tue Wed Thu Fri Sat",
            help="The week day names for localization.",
        )
        pars.add_argument(
            "--weeknr-name",
            type=str,
            dest="weeknr_name",
            default="Wk",
            help="The week number column name for localization.",
        )
        pars.add_argument(
            "--encoding",
            type=str,
            dest="input_encode",
            default="arabic",
            help="The input encoding of the names.",
        )

    def validate_options(self):
        """Validity check for various text inputs"""
        # inkex.errormsg( self.options.input_encode )
        # Convert string names lists in real lists
        month_name = re.match(r"\s*(.*[^\s])\s*", self.options.month_names)
        self.options.month_names = re.split(r"\s+", month_name.group(1))
        month_name = re.match(r"\s*(.*[^\s])\s*", self.options.day_names)
        self.options.day_names = re.split(r"\s+", month_name.group(1))

        # Validate names lists
        if len(self.options.month_names) != 12:
            inkex.errormsg(
                'The month name list "'
                + str(self.options.month_names)
                + '" is invalid. Using default.'
            )
            self.options.month_names = [
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December",
            ]
        if len(self.options.day_names) != 7:
            inkex.errormsg(
                'The day name list "'
                + str(self.options.day_names)
                + '" is invalid. Using default.'
            )
            self.options.day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
        # Convert year 0 to current year
        if self.options.year == 0:
            self.options.year = datetime.datetime.today().year
        # Year 1 starts it's week at monday, obligatorily
        if self.options.year == 1:
            self.options.start_day = "mon"
        # Set the calendar start day
        if self.options.start_day == "sun":
            calendar.setfirstweekday(6)
        else:
            calendar.setfirstweekday(0)
        # Convert string numbers with unit to user space float numbers
        self.options.month_width = self.svg.unittouu(self.options.month_width)
        self.options.month_margin = self.svg.unittouu(self.options.month_margin)

    # initial values
    month_x_pos = 0
    month_y_pos = 0
    weeknr = 0

    def calculate_size_and_positions(self):
        """month_margin month_width months_per_line auto_organize"""
        self.doc_w = self.svg.unittouu(self.document.getroot().get("width"))
        self.doc_h = self.svg.unittouu(self.document.getroot().get("height"))
        if self.options.show_weeknr:
            self.cols_before = 1
        else:
            self.cols_before = 0
        if self.options.auto_organize:
            if self.doc_h > self.doc_w:
                self.months_per_line = 3
            else:
                self.months_per_line = 4
        else:
            self.months_per_line = self.options.months_per_line
        # self.month_w = self.doc_w / self.months_per_line
        if self.options.auto_organize:
            self.month_w = (self.doc_w * 0.8) / self.months_per_line
            self.month_margin = self.month_w / 10
        else:
            self.month_w = self.options.month_width
            self.month_margin = self.options.month_margin
        self.day_w = self.month_w / (7 + self.cols_before)
        self.day_h = self.month_w / 9
        self.month_h = self.day_w * 7
        if self.options.month == 0:
            self.year_margin = (
                self.doc_w
                + self.day_w
                - (self.month_w * self.months_per_line)
                - (self.month_margin * (self.months_per_line - 1))
            ) / 2  # - self.month_margin
        else:
            self.year_margin = (self.doc_w - self.month_w) / 2
        self.style_day = {
            "font-size": str(self.day_w / 2),
            "font-family": self.options.font_day,
            "text-anchor": "middle",
            "text-align": "center",
            "fill": self.options.color_day,
        }
        self.style_weekend = self.style_day.copy()
        self.style_weekend["fill"] = self.options.color_weekend
        self.style_nmd = self.style_day.copy()
        self.style_nmd["fill"] = self.options.color_nmd
        self.style_month = self.style_day.copy()
        self.style_month["fill"] = self.options.color_month
        self.style_month["font-family"] = self.options.font_month
        self.style_month["font-size"] = str(self.day_w / 1.5)
        self.style_month["font-weight"] = "bold"
        self.style_day_name = self.style_day.copy()
        self.style_day_name["fill"] = self.options.color_day_name
        self.style_day_name["font-family"] = self.options.font_day_name
        self.style_day_name["font-size"] = str(self.day_w / 3)
        self.style_year = self.style_day.copy()
        self.style_year["fill"] = self.options.color_year
        self.style_year["font-family"] = self.options.font_year
        self.style_year["font-size"] = str(self.day_w * 2)
        self.style_year["font-weight"] = "bold"
        self.style_weeknr = self.style_day.copy()
        self.style_weeknr["fill"] = self.options.color_weeknr
        self.style_weeknr["font-size"] = str(self.day_w / 3)

    def is_weekend(self, pos):
        """Detect weekend days; weekend values: "sat+sun" or "sat" or "sun" """
        if self.options.start_day == "sun":
            if self.options.weekend == "sat+sun" and pos == 0:
                return True
            if self.options.weekend == "sat+sun" and pos == 6:
                return True
            if self.options.weekend == "sat" and pos == 6:
                return True
            if self.options.weekend == "sun" and pos == 0:
                return True
        else:
            if self.options.weekend == "sat+sun" and pos == 5:
                return True
            if self.options.weekend == "sat+sun" and pos == 6:
                return True
            if self.options.weekend == "sat" and pos == 5:
                return True
            if self.options.weekend == "sun" and pos == 6:
                return True
        return False

    def in_line_month(self, cal):
        """Lay out days of week in each week"""
        cal2 = []
        for week in cal:
            for day in week:
                if day != 0:
                    cal2.append(day)
        return cal2

    def write_month_header(self, g, m):
        """Write month header"""
        txt_atts = {
            "style": str(inkex.Style(self.style_month)),
            "x": str((self.month_w - self.day_w) / 2),
            "y": str(self.day_h / 5),
        }
        try:
            g.add(TextElement(**txt_atts)).text = unicode(
                self.options.month_names[m - 1], self.options.input_encode
            )
        except:
            raise ValueError(_("You must select a correct system encoding."))

        week_group = g.add(inkex.Group())
        week_x = 0
        if self.options.start_day == "sun":
            day_names = self.options.day_names[:]
        else:
            day_names = self.options.day_names[1:]
            day_names.append(self.options.day_names[0])

        if self.options.show_weeknr:
            day_names.insert(0, self.options.weeknr_name)

        for wday in day_names:
            txt_atts = {
                "style": str(inkex.Style(self.style_day_name)),
                "x": str(self.day_w * week_x),
                "y": str(self.day_h),
            }
            try:
                week_group.add(TextElement(**txt_atts)).text = unicode(
                    wday, self.options.input_encode
                )
            except:
                raise ValueError(_("You must select a correct system encoding."))

            week_x += 1

    def create_month(self, m):
        """Lay out one month in place on the calendar"""
        txt_atts = {
            "transform": "translate("
            + str(
                self.year_margin + (self.month_w + self.month_margin) * self.month_x_pos
            )
            + ","
            + str((self.day_h * 4) + (self.month_h * self.month_y_pos))
            + ")",
            "id": "month_" + str(m) + "_" + str(self.options.year),
        }
        g = self.year_g.add(inkex.Group(**txt_atts))
        self.write_month_header(g, m)
        gdays = g.add(inkex.Group())
        cal = calendar.monthcalendar(self.options.year, m)
        if m == 1:
            if self.options.year > 1:
                before_month = self.in_line_month(
                    calendar.monthcalendar(self.options.year - 1, 12)
                )
        else:
            before_month = self.in_line_month(
                calendar.monthcalendar(self.options.year, m - 1)
            )
        if m == 12:
            next_month = self.in_line_month(
                calendar.monthcalendar(self.options.year + 1, 1)
            )
        else:
            next_month = self.in_line_month(
                calendar.monthcalendar(self.options.year, m + 1)
            )
        if len(cal) < 6:
            # add a line after the last week
            cal.append([0, 0, 0, 0, 0, 0, 0])
        if len(cal) < 6:
            # add a line before the first week (Feb 2009)
            cal.reverse()
            cal.append([0, 0, 0, 0, 0, 0, 0])
            cal.reverse()
        # How mutch before month days will be showed:
        bmd = cal[0].count(0) + cal[1].count(0)
        before = True
        week_y = 0
        for week in cal:
            if (
                self.weeknr != 0
                and (
                    (self.options.start_day == "mon" and week[0] != 0)
                    or (self.options.start_day == "sun" and week[1] != 0)
                )
            ) or (
                self.weeknr == 0
                and (
                    (self.options.start_day == "mon" and week[3] > 0)
                    or (self.options.start_day == "sun" and week[4] > 0)
                )
            ):
                self.weeknr += 1
            week_x = 0
            if self.options.show_weeknr:
                # Remove leap week (starting previous year) and empty weeks
                if self.weeknr != 0 and not (week[0] == 0 and week[6] == 0):
                    style = self.style_weeknr
                    txt_atts = {
                        "style": str(inkex.Style(style)),
                        "x": str(self.day_w * week_x),
                        "y": str(self.day_h * (week_y + 2)),
                    }
                    gdays.add(TextElement(**txt_atts)).text = str(self.weeknr)
                    week_x += 1
                else:
                    week_x += 1
            for day in week:
                style = self.style_day
                if self.is_weekend(week_x - self.cols_before):
                    style = self.style_weekend
                if day == 0:
                    style = self.style_nmd
                txt_atts = {
                    "style": str(inkex.Style(style)),
                    "x": str(self.day_w * week_x),
                    "y": str(self.day_h * (week_y + 2)),
                }
                text = None
                if day == 0 and not self.options.fill_edb:
                    pass  # draw nothing
                elif day == 0:
                    if before:
                        text = str(before_month[-bmd])
                        bmd -= 1
                    else:
                        text = str(next_month[bmd])
                        bmd += 1
                else:
                    text = str(day)
                    before = False
                if text:
                    gdays.add(TextElement(**txt_atts)).text = text
                week_x += 1
            week_y += 1
        self.month_x_pos += 1
        if self.month_x_pos >= self.months_per_line:
            self.month_x_pos = 0
            self.month_y_pos += 1

    def effect(self):
        self.validate_options()
        self.calculate_size_and_positions()
        parent = self.document.getroot()
        txt_atts = {"id": "year_" + str(self.options.year)}
        self.year_g = parent.add(inkex.Group(**txt_atts))
        txt_atts = {
            "style": str(inkex.Style(self.style_year)),
            "x": str(self.doc_w / 2),
            "y": str(self.day_w * 1.5),
        }
        self.year_g.add(TextElement(**txt_atts)).text = str(self.options.year)
        try:
            if self.options.month == 0:
                for m in range(1, 13):
                    self.create_month(m)
            else:
                self.create_month(self.options.month)
        except ValueError as err:
            return inkex.errormsg(str(err))
        return None


if __name__ == "__main__":
    Calendar().run()
