import calendar
import json

from browser import document, window


# ---- Utilities ---- #

DAY_NAMES = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]

DECORATORS = {
    "basic": ("*", "|", "#", " ", " "),
    "geometric": ("□", "■", "●", " ", " "),
    "emojis": ("⬛", "📅", "🟦", "⬛", "⬜"),
}

COMMENT_CELL_DECORATOR = 0
DAY_CELL_DECORATOR = 1
DAY_NAME_DECORATOR = 2
EMPTY_CELL_DECORATOR = 3
PADDING_CELL_DECORATOR = 4

MONTH_SEPARATION = 10
LOCAL_STORAGE_STATE_KEY = "calendar-generator-state"

DEFAULT_STATE = {
    "include_day_names": True,
    "start_week_on_sunday": False,
    "leading_zeroes": False,
    "include_comments_line": False,
    "include_separation_line": False,
    "decorators": "none",
    "spaces_between": 5,
    "months_per_row": 3,
    "year": 2026,
}

def parse_int(value, default, minimum=None, maximum=None):
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        parsed = default

    if minimum is not None and parsed < minimum:
        parsed = minimum
    if maximum is not None and parsed > maximum:
        parsed = maximum
    return parsed

# ---- State management ---- #

state = dict(DEFAULT_STATE)


def save_state_to_local_storage():
    window.localStorage.setItem(LOCAL_STORAGE_STATE_KEY, json.dumps(state))


def load_state_from_local_storage():
    raw_state = window.localStorage.getItem(LOCAL_STORAGE_STATE_KEY)
    if not raw_state:
        return

    try:
        loaded_state = json.loads(str(raw_state))
    except Exception:
        return

    decorators = loaded_state.get("decorators")
    if decorators not in DECORATORS and decorators != "none":
        decorators = DEFAULT_STATE["decorators"]

    state["include_day_names"] = bool(loaded_state.get("include_day_names", DEFAULT_STATE["include_day_names"]))
    state["start_week_on_sunday"] = bool(loaded_state.get("start_week_on_sunday", DEFAULT_STATE["start_week_on_sunday"]))
    state["leading_zeroes"] = bool(loaded_state.get("leading_zeroes", DEFAULT_STATE["leading_zeroes"]))
    state["include_comments_line"] = bool(loaded_state.get("include_comments_line", DEFAULT_STATE["include_comments_line"]))
    state["include_separation_line"] = bool(loaded_state.get("include_separation_line", DEFAULT_STATE["include_separation_line"]))
    state["decorators"] = decorators if decorators is not None else DEFAULT_STATE["decorators"]
    state["spaces_between"] = parse_int(loaded_state.get("spaces_between"), DEFAULT_STATE["spaces_between"], minimum=0)
    state["months_per_row"] = parse_int(loaded_state.get("months_per_row"), DEFAULT_STATE["months_per_row"], minimum=1, maximum=12)
    state["year"] = parse_int(loaded_state.get("year"), DEFAULT_STATE["year"], minimum=1900, maximum=2100)


def sync_inputs_from_state():
    document["includeDayNames"].checked = state["include_day_names"]
    document["startWeekOnSunday"].checked = state["start_week_on_sunday"]
    document["leadingZeroes"].checked = state["leading_zeroes"]
    document["includeCommentsLine"].checked = state["include_comments_line"]
    document["includeSeparationLine"].checked = state["include_separation_line"]
    document["decoratorsNone"].checked = state["decorators"] == "none"
    document["decoratorsBasic"].checked = state["decorators"] == "basic"
    document["decoratorsGeometric"].checked = state["decorators"] == "geometric"
    document["decoratorsEmojis"].checked = state["decorators"] == "emojis"
    document["spacesBetween"].value = str(state["spaces_between"])
    document["monthsPerRow"].value = str(state["months_per_row"])
    document["year"].value = str(state["year"])

def sync_state_from_inputs():
    state["include_day_names"] = document["includeDayNames"].checked
    state["start_week_on_sunday"] = document["startWeekOnSunday"].checked
    state["leading_zeroes"] = document["leadingZeroes"].checked
    state["include_comments_line"] = document["includeCommentsLine"].checked
    state["include_separation_line"] = document["includeSeparationLine"].checked

    decorators = "none"
    if document["decoratorsBasic"].checked:
        decorators = "basic"
    elif document["decoratorsGeometric"].checked:
        decorators = "geometric"
    elif document["decoratorsEmojis"].checked:
        decorators = "emojis"
    state["decorators"] = decorators

    spaces_between = parse_int(document["spacesBetween"].value, 5, minimum=0)
    months_per_row = parse_int(document["monthsPerRow"].value, 3, minimum=1, maximum=12)
    year = parse_int(document["year"].value, 2026, minimum=1900, maximum=2100)

    document["spacesBetween"].value = str(spaces_between)
    document["monthsPerRow"].value = str(months_per_row)
    document["year"].value = str(year)

    state["spaces_between"] = spaces_between
    state["months_per_row"] = months_per_row
    state["year"] = year


def update_symbols_warning_visibility():
    show_warning = state["decorators"] == "geometric" or state["decorators"] == "emojis"
    document["decoratorsWarningField"].style.display = "inline-block" if show_warning else "none"


def render_calendar():
    sync_state_from_inputs()
    save_state_to_local_storage()
    update_symbols_warning_visibility()
    document["calendarOutput"].text = build_calendar_text()


# ---- Calendar generation ---- #

def month_block(year, month, include_day_names, start_week_on_sunday, leading_zeroes, include_comments_line, include_separation_line, decorators):
    use_decorators = decorators != "none"
    day_names = DAY_NAMES[-1:] + DAY_NAMES[:-1] if start_week_on_sunday else DAY_NAMES
    first_weekday = 6 if start_week_on_sunday else 0

    month_width = 2 * 7 + state["spaces_between"] * 6
    if use_decorators:
        month_width = month_width + 7 * 2
    
    month_name = calendar.month_name[month]
    month_name_line = month_name.center(month_width)
    lines = [month_name_line]

    if include_day_names:
        if use_decorators:
            day_names = [DECORATORS[decorators][DAY_NAME_DECORATOR] + " " + day_name for day_name in day_names]
        lines.append((" " * state["spaces_between"]).join(day_names))

    empty_cell = "  " if not use_decorators else DECORATORS[decorators][EMPTY_CELL_DECORATOR] + "   "
    day_format = "{:02d}" if leading_zeroes else "{:2d}"

    weeks = calendar.Calendar(firstweekday=first_weekday).monthdayscalendar(year, month)
    for week in weeks:
        cells = []
        for day in week:
            if day == 0:
                cells.append(empty_cell)
            elif use_decorators:
                cells.append(DECORATORS[decorators][DAY_CELL_DECORATOR] + " " + day_format.format(day))
            else:
                cells.append(day_format.format(day))
        lines.append((" " * state["spaces_between"]).join(cells))

        if include_comments_line:
            if use_decorators:
                comment_cells = []
                for day in week:
                    decorator_index = COMMENT_CELL_DECORATOR if day != 0 else EMPTY_CELL_DECORATOR
                    comment_cells.append(DECORATORS[decorators][decorator_index] + "   ")
                lines.append((" " * state["spaces_between"]).join(comment_cells))
            else:
                lines.append(" " * month_width)

        if include_separation_line:
            lines.append(" " * month_width)

    return lines, month_width


def build_calendar_text():
    use_decorators = state["decorators"] != "none"

    blocks = []
    widths = []

    for month in range(1, 13):
        block, width = month_block(
            state["year"],
            month,
            state["include_day_names"],
            state["start_week_on_sunday"],
            state["leading_zeroes"],
            state["include_comments_line"],
            state["include_separation_line"],
            state["decorators"],
        )
        blocks.append(block)
        widths.append(width)

    row_gap = " " * MONTH_SEPARATION

    calendar_width = sum(widths[: state["months_per_row"]]) + MONTH_SEPARATION * (state["months_per_row"] - 1)
    year_line = str(state["year"]).center(calendar_width)
    output_lines = [year_line]

    for row_start in range(0, 12, state["months_per_row"]):
        row_blocks = blocks[row_start : row_start + state["months_per_row"]]
        row_widths = widths[row_start : row_start + state["months_per_row"]]
        max_height = max(len(block) for block in row_blocks)

        padded_blocks = []
        for block, width in zip(row_blocks, row_widths):
            if use_decorators:
                padding_line = (DECORATORS[state["decorators"]][PADDING_CELL_DECORATOR] + "   " + (" ") * state["spaces_between"]) * 6 + DECORATORS[state["decorators"]][PADDING_CELL_DECORATOR] + "   "
            else:
                padding_line = " " * width
            padded = block + [padding_line] * (max_height - len(block))
            padded_blocks.append(padded)

        for line_index in range(max_height):
            output_lines.append(row_gap.join(block[line_index] for block in padded_blocks))

        if row_start + state["months_per_row"] < 12:
            output_lines.append("")

    return "\n".join(output_lines)


# ---- Event handlers ---- #

def on_option_change(_event):
    render_calendar()

document["includeDayNames"].bind("change", on_option_change)
document["startWeekOnSunday"].bind("change", on_option_change)
document["leadingZeroes"].bind("change", on_option_change)
document["includeCommentsLine"].bind("change", on_option_change)
document["includeSeparationLine"].bind("change", on_option_change)
document["decoratorsNone"].bind("change", on_option_change)
document["decoratorsBasic"].bind("change", on_option_change)
document["decoratorsGeometric"].bind("change", on_option_change)
document["decoratorsEmojis"].bind("change", on_option_change)
document["spacesBetween"].bind("input", on_option_change)
document["monthsPerRow"].bind("input", on_option_change)
document["year"].bind("input", on_option_change)


# ---- Initialization ---- #

load_state_from_local_storage()
sync_inputs_from_state()
render_calendar()
