from __future__ import annotations

import datetime
import math
import re

_one_or_two_digits = "([ ]?\\d{1,2})"
_separator = "[.:'\\-\\/ ]"
DATE_OR_TIME_PATTERN = re.compile(
    f"""
        {_one_or_two_digits}  # day/hour
        {_separator}
        {_one_or_two_digits}  # month/minute
        {_separator}
        {_one_or_two_digits}  # year/second
        """,
    re.VERBOSE,
)


def encode_str(value: str, length: int) -> bytes:
    if len(value) > length:
        raise ValueError(
            f"{value!r} exceeds maximum field length: {len(value)} > {length}"
        )
    if not value.isprintable():
        raise ValueError(f"{value} contains non-printable characters")
    return value.encode("ascii").ljust(length)


def decode_str(field: bytes, encoding: str = "ascii") -> str:
    return field.decode(encoding=encoding, errors="replace").rstrip()


def encode_int(value: int, length: int) -> bytes:
    return encode_str(str(value), length)


def encode_float(value: float) -> bytes:
    if float(value).is_integer():
        value = int(value)
    return encode_str(str(value), 8)


def decode_float(field: bytes) -> float:
    value = float(decode_str(field))
    if math.isinf(value):
        raise ValueError(f"Field value is outside float range: {decode_str(field)}")
    return value


def decode_date(field: bytes) -> datetime.date:
    date = decode_str(field)
    match = DATE_OR_TIME_PATTERN.fullmatch(date)
    if match is None:
        raise ValueError(f"Invalid date for format DD.MM.YY: {date!r}")
    day, month, year = (int(g) for g in match.groups())
    if year >= 85:  # noqa: PLR2004
        year += 1900
    else:
        year += 2000
    return datetime.date(year, month, day)


def encode_date(value: datetime.date) -> bytes:
    if not 1985 <= value.year <= 2084:  # noqa: PLR2004
        raise ValueError("EDF only allows dates from 1985 to 2084")
    return encode_str(value.strftime("%d.%m.%y"), 8)


def decode_time(field: bytes) -> datetime.time:
    time = decode_str(field)
    match = DATE_OR_TIME_PATTERN.fullmatch(time)
    if match is None:
        raise ValueError(f"Invalid time for format hh.mm.ss: {time!r}")
    hours, minutes, seconds = (int(g) for g in match.groups())
    return datetime.time(hours, minutes, seconds)


def encode_time(value: datetime.time) -> bytes:
    return encode_str(value.isoformat().replace(":", "."), 8)
