from typing import TYPE_CHECKING

from .common import (
    COLOUR_BLACK,
    COLOUR_WHITE,
    KINDLE_GREYSCALE_LEVELS,
    ChartBounds,
    FontType,
    RenderMetrics,
    build_vertical_band,
    measure_text,
)
from .forecast import build_hourly_series

if TYPE_CHECKING:
    from PIL import ImageDraw

    from kindle_dashboard.weather import Forecast

    from .chart_context import ChartRenderContext

PRECIPITATION_PLOT_HEIGHT_RATIO = 0.30
PRECIPITATION_MAX = 2.0
PRECIPITATION_RULE_INTERVAL_MM = 0.2
PRECIPITATION_RULE_MAX_MM = 1.0
PRECIPITATION_RULE_COLOUR = 223
KINDLE_DARKEST_NON_BLACK_LEVEL = 1
KINDLE_LIGHTEST_NON_WHITE_LEVEL = KINDLE_GREYSCALE_LEVELS - 2


def clamp_precipitation(value: float) -> float:
    return max(0.0, min(PRECIPITATION_MAX, value))


def clamp_percentage(value: float) -> float:
    return max(0.0, min(100.0, value))


def probability_to_kindle_greyscale(probability: float) -> int:
    clamped_probability = clamp_percentage(probability)
    max_level = KINDLE_GREYSCALE_LEVELS - 1
    usable_range = KINDLE_LIGHTEST_NON_WHITE_LEVEL - KINDLE_DARKEST_NON_BLACK_LEVEL
    grey_level = KINDLE_LIGHTEST_NON_WHITE_LEVEL - round(
        (clamped_probability / 100.0) * usable_range
    )
    return round((grey_level / max_level) * COLOUR_WHITE)


def showers_dominate(rain: float, showers: float) -> bool:
    return showers > rain


def format_precipitation_label(value: float) -> str:
    if value > PRECIPITATION_MAX:
        return f">{PRECIPITATION_MAX:.1f}mm"
    return f"{clamp_precipitation(value):.1f}mm"


def choose_checker_cell_size(
    bar_width: int,
    preferred_cell_size: int,
) -> int:
    if bar_width <= 0:
        msg = "Bar width must be positive"
        raise ValueError(msg)

    preferred = max(1, min(preferred_cell_size, bar_width))
    candidate_sizes = [
        cell_size
        for cell_size in range(1, bar_width + 1)
        if bar_width % cell_size == 0
    ]
    return min(
        candidate_sizes,
        key=lambda cell_size: abs(cell_size - preferred),
    )


def draw_checkered_overlay(
    draw: ImageDraw.ImageDraw,
    *,
    left: int,
    top: int,
    right: int,
    bottom: int,
    cell_size: int,
) -> None:
    for y in range(top, bottom + 1, cell_size):
        row = (y - top) // cell_size
        for x in range(left, right + 1, cell_size):
            column = (x - left) // cell_size
            if (row + column) % 2 != 0:
                continue
            square_right = x + cell_size - 1
            square_bottom = min(bottom, y + cell_size - 1)
            draw.rectangle(
                (x, y, square_right, square_bottom),
                fill=COLOUR_WHITE,
                outline=COLOUR_WHITE,
            )


def draw_precipitation_chart(
    draw: ImageDraw.ImageDraw,
    font: FontType,
    chart_bounds: ChartBounds,
    chart_context: ChartRenderContext,
    forecast: Forecast,
    metrics: RenderMetrics,
) -> None:
    hour_slots = chart_context.hour_slots
    x_axis_y = chart_context.x_axis_y
    x_positions = chart_context.x_positions

    # Open-Meteo's hourly precipitation value at HH:00 covers the preceding
    # interval [HH-1:00, HH:00), so bars are placed on the starting hour.
    slot_values = build_hourly_series(
        forecast,
        forecast.precipitation,
        hour_slots,
        hour_shift=-1,
    )
    slot_probabilities = build_hourly_series(
        forecast,
        forecast.precipitation_probability,
        hour_slots,
        hour_shift=-1,
    )
    slot_rain = build_hourly_series(
        forecast,
        forecast.rain,
        hour_slots,
        hour_shift=-1,
    )
    slot_showers = build_hourly_series(
        forecast,
        forecast.showers,
        hour_slots,
        hour_shift=-1,
    )
    precipitation_top, precipitation_bottom = build_vertical_band(
        chart_bounds.top,
        chart_bounds.bottom,
        top_ratio=1.0 - PRECIPITATION_PLOT_HEIGHT_RATIO,
        bottom_ratio=1.0,
    )
    plot_left = chart_bounds.left
    plot_top = precipitation_top + metrics.plot_padding
    plot_right = chart_bounds.right
    plot_bottom = min(
        precipitation_bottom - metrics.plot_padding,
        x_axis_y,
    )
    plot_height = plot_bottom - plot_top
    interval_values = slot_values[:-1]
    interval_probabilities = slot_probabilities[:-1]
    interval_rain = slot_rain[:-1]
    interval_showers = slot_showers[:-1]
    intervals = zip(
        interval_values,
        interval_probabilities,
        interval_rain,
        interval_showers,
        strict=False,
    )
    max_visible_precipitation = max(
        (clamp_precipitation(float(value)) for value in interval_values),
        default=0.0,
    )
    max_rule_value = min(max_visible_precipitation, PRECIPITATION_RULE_MAX_MM)
    if max_rule_value >= PRECIPITATION_RULE_INTERVAL_MM:
        rule_value = PRECIPITATION_RULE_INTERVAL_MM
        while rule_value <= max_rule_value + 1e-9:
            if abs(rule_value - PRECIPITATION_RULE_MAX_MM) < 1e-9:
                rule_value = round(rule_value + PRECIPITATION_RULE_INTERVAL_MM, 1)
                continue
            rule_y = plot_bottom - round(
                (rule_value / PRECIPITATION_MAX) * plot_height
            )
            if plot_top <= rule_y <= plot_bottom:
                draw.line(
                    (plot_left, rule_y, plot_right, rule_y),
                    fill=PRECIPITATION_RULE_COLOUR,
                    width=max(1, metrics.thin_shape_stroke_width),
                )
            rule_value = round(rule_value + PRECIPITATION_RULE_INTERVAL_MM, 1)

    largest_bar_label: tuple[str, int, int, int] | None = None
    largest_clamped_value = -1.0

    for index, (precipitation_value, probability_value, rain_value, showers_value) in (
        enumerate(intervals)
    ):
        value = float(precipitation_value)
        clamped_value = clamp_precipitation(value)
        bar_left = x_positions[index]
        bar_right = x_positions[index + 1]

        bar_height = round((clamped_value / PRECIPITATION_MAX) * plot_height)
        if clamped_value > 0 and bar_height == 0:
            bar_height = 1
        if bar_height <= 0:
            continue

        bar_top = plot_bottom - bar_height + 1
        bar_fill = probability_to_kindle_greyscale(float(probability_value))
        draw.rectangle(
            (bar_left, bar_top, bar_right, plot_bottom),
            fill=bar_fill,
            outline=bar_fill,
        )

        if clamped_value > largest_clamped_value:
            largest_clamped_value = clamped_value
            largest_bar_label = (
                format_precipitation_label(value),
                bar_left,
                bar_right,
                bar_top,
            )

        if showers_dominate(rain_value, showers_value):
            base_cell_size = max(2, metrics.thin_shape_stroke_width * 2)
            bar_width = bar_right - bar_left + 1
            checker_cell_size = choose_checker_cell_size(
                bar_width,
                max(2, base_cell_size + 1),
            )
            draw_checkered_overlay(
                draw,
                left=bar_left,
                top=bar_top,
                right=bar_right,
                bottom=plot_bottom,
                cell_size=checker_cell_size,
            )

    if max_rule_value >= PRECIPITATION_RULE_MAX_MM:
        major_rule_y = plot_bottom - round(
            (PRECIPITATION_RULE_MAX_MM / PRECIPITATION_MAX) * plot_height
        )
        if plot_top <= major_rule_y <= plot_bottom:
            draw.line(
                (plot_left, major_rule_y, plot_right, major_rule_y),
                fill=PRECIPITATION_RULE_COLOUR,
                width=max(1, metrics.thin_shape_stroke_width),
            )

    if largest_bar_label is None:
        return

    label_text, bar_left, bar_right, bar_top = largest_bar_label
    label_width, label_height, _ = measure_text(draw, font, label_text)
    label_x = bar_left + ((bar_right - bar_left + 1 - label_width) // 2)
    label_x = max(chart_bounds.left, min(label_x, chart_bounds.right - label_width))
    label_y = max(
        chart_bounds.top,
        bar_top - label_height - metrics.temperature_label_offset,
    )
    draw.text((label_x, label_y), label_text, fill=COLOUR_BLACK, font=font)
