"""Label rendering utilities — generates PIL Images for warehouse labels.

Label specs (203 DPI / 8 dots-per-mm):
  small  55 × 32 mm   →  440 ×  256 dots
  large 100 × 150 mm  →  800 × 1200 dots
"""

import os
from io import BytesIO

from PIL import Image, ImageDraw, ImageFont, ImageOps

LABEL_DPI: int = 203  # dots per inch (8 dpmm)

# Small label — 55×32 mm
LABEL_W: int = 440
LABEL_H: int = 256

# Large label — 100×150 mm
LARGE_LABEL_W: int = 800
LARGE_LABEL_H: int = 1200

# Font search order — first match wins
_REGULAR_PATHS = [
    # macOS
    '/System/Library/Fonts/Supplemental/Arial.ttf',
    '/Library/Fonts/Arial.ttf',
    # Linux (Debian/Ubuntu/Pi)
    '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
    '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
    '/usr/share/fonts/truetype/freefont/FreeSans.ttf',
    '/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf',
]
_BOLD_PATHS = [
    '/System/Library/Fonts/Supplemental/Arial Bold.ttf',
    '/Library/Fonts/Arial Bold.ttf',
    '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
    '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
    '/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf',
]


def _find(paths: list[str]) -> str | None:
    for p in paths:
        if os.path.exists(p):
            return p
    return None


_regular_path = _find(_REGULAR_PATHS)
_bold_path = _find(_BOLD_PATHS) or _regular_path


def load_font(size: int, bold: bool = False) -> ImageFont.ImageFont:
    """Return a TrueType font at *size* pixels, falling back to PIL default."""
    path = _bold_path if bold else _regular_path
    if path:
        try:
            return ImageFont.truetype(path, size)
        except Exception:
            pass
    return ImageFont.load_default()


def new_label(w: int = LABEL_W, h: int = LABEL_H) -> tuple[Image.Image, ImageDraw.ImageDraw]:
    """Return a blank white label canvas and its draw object."""
    img = Image.new('L', (w, h), 255)
    return img, ImageDraw.Draw(img)


def draw_barcode(
    canvas: Image.Image,
    data: str,
    x: int,
    y: int,
    width: int = 420,
    height: int = 48,
    symbology: str = 'code128',
    hrt_text: str = None,
) -> None:
    """Render a barcode horizontally onto *canvas*.

    Args:
        canvas:    The label image to draw onto.
        data:      Barcode payload string.
        x, y:      Top-left position in dots.
        width:     Target width in dots.
        height:    Target height in dots for the bar area (HRT drawn below).
        symbology: 'code128' (default) or 'ean' for EAN-13.
    """
    import barcode as bc
    from barcode.writer import ImageWriter

    is_ean = symbology in ('ean', 'ean13')
    buf = BytesIO()

    if is_ean:
        digits = ''.join(c for c in data if c.isdigit())
        try:
            EAN13Cls = bc.get_barcode_class('ean13')
            # EAN-13 expects 12 data digits; library appends the check digit.
            # If we have 13 digits the 13th is already the check digit — drop it.
            d12 = digits[:12] if len(digits) >= 13 else digits.ljust(12, '0')[:12]
            bc_obj = EAN13Cls(d12, writer=ImageWriter())
        except Exception:
            bc_obj = bc.Code128(data, writer=ImageWriter())
            is_ean = False
    else:
        bc_obj = bc.Code128(data, writer=ImageWriter())

    # Render bars only; we'll draw HRT ourselves so we control the font size
    bc_obj.write(buf, options={
        'module_width': 0.8,
        'module_height': 15.0,
        'quiet_zone': 2.0,
        'write_text': False,
        'dpi': LABEL_DPI,
    })
    buf.seek(0)

    barcode_img = Image.open(buf).convert('L')
    barcode_img = barcode_img.resize((width, height), Image.LANCZOS)
    barcode_img = barcode_img.point(lambda v: 0 if v < 128 else 255)
    canvas.paste(barcode_img, (x, y))

    # Draw HRT below bars
    draw = ImageDraw.Draw(canvas)
    text_size = max(10, height // 4)
    font = load_font(text_size, bold=False)

    if is_ean:
        # EAN-13 layout: first digit left of bars | 6 digits | 6 digits | right of bars
        # We approximate by splitting the barcode width into the right proportions.
        # EAN-13: 1 quiet(9m) + start(3m) + 6×7m + center(5m) + 6×7m + end(3m) + quiet(9m) = 113m total
        # quiet=9m on each side, first digit sits below left quiet zone
        q_frac   = 9 / 113          # quiet zone fraction of total width
        lg_frac  = (9 + 3) / 113    # left quiet + start guard → start of L digits
        cg_frac  = (9 + 3 + 42) / 113  # start of center guard
        rg_frac  = (9 + 3 + 42 + 5) / 113  # end of center guard → start of R digits
        end_frac = (9 + 3 + 42 + 5 + 42 + 3) / 113  # end of last bar

        full13 = d12 + str(  # rebuild the check digit from what the library computed
            (10 - sum((3 if i % 2 else 1) * int(c) for i, c in enumerate(d12)) % 10) % 10
        )
        first_digit   = full13[0]
        left_group    = full13[1:7]
        right_group   = full13[7:]

        ty = y + height + 2

        # First digit — below left quiet zone
        bbox = draw.textbbox((0, 0), first_digit, font=font)
        d_x = x + int(width * q_frac / 2) - (bbox[2] - bbox[0]) // 2
        draw.text((d_x, ty), first_digit, fill=0, font=font)

        # Left 6 digits — centered between start guard and center guard
        bbox = draw.textbbox((0, 0), left_group, font=font)
        d_x = x + int(width * (lg_frac + cg_frac) / 2) - (bbox[2] - bbox[0]) // 2
        draw.text((d_x, ty), left_group, fill=0, font=font)

        # Right 6 digits — centered between center guard and end guard
        bbox = draw.textbbox((0, 0), right_group, font=font)
        d_x = x + int(width * (rg_frac + end_frac) / 2) - (bbox[2] - bbox[0]) // 2
        draw.text((d_x, ty), right_group, fill=0, font=font)
    else:
        hrt = hrt_text if hrt_text is not None else data
        bbox = draw.textbbox((0, 0), hrt, font=font)
        tw = bbox[2] - bbox[0]
        tx = x + (width - tw) // 2
        ty = y + height + 2
        draw.text((tx, ty), hrt, fill=0, font=font)


def draw_barcode_vertical(
    canvas: Image.Image,
    data: str,
    x: int,
    y: int,
    bar_length: int = 220,
    bar_thickness: int = 44,
    symbology: str = 'code128',
) -> None:
    """Render a barcode rotated 90° CCW onto *canvas*.

    Args:
        canvas:        The label image to draw onto.
        data:          Barcode payload string.
        x, y:          Top-left of the rotated barcode on the canvas.
        bar_length:    Length of the bars — becomes the HEIGHT after rotation.
        bar_thickness: Thickness of the bar strip — becomes the WIDTH after rotation.
        symbology:     'code128' (default) or 'ean' / 'upc' for EAN-13.
    """
    import barcode as bc
    from barcode.writer import ImageWriter

    is_ean = False
    buf = BytesIO()

    if symbology in ('ean', 'upc'):
        digits = ''.join(c for c in data if c.isdigit())
        try:
            EAN13Cls = bc.get_barcode_class('ean13')
            # EAN-13 library expects 12 data digits and appends the check digit.
            # If we have 13 digits the last one is the existing check digit — drop it.
            if len(digits) >= 13:
                d12 = digits[:12]
            elif len(digits) >= 7:
                d12 = digits.ljust(12, '0')[:12]
            else:
                raise ValueError(f'Too few digits for EAN-13: {len(digits)}')
            bc_obj = EAN13Cls(d12, writer=ImageWriter())
            is_ean = True
        except Exception:
            bc_obj = bc.Code128(data, writer=ImageWriter())
    else:
        bc_obj = bc.Code128(data, writer=ImageWriter())

    bc_obj.write(buf, options={
        'module_width': 0.8,
        'module_height': 15.0,
        'quiet_zone': 2.0,
        'write_text': is_ean,   # EAN: let library draw proper digit layout
        'text_distance': 1.0,
        'font_size': 8,
        'dpi': LABEL_DPI,
    })
    buf.seek(0)

    barcode_img = Image.open(buf).convert('L')
    # EAN image is taller than bars-only (includes HRT digit row at bottom).
    # Give ~38% extra thickness so digits aren't squished when we resize.
    target_t = int(bar_thickness * 1.38) if is_ean else bar_thickness
    barcode_img = barcode_img.resize((bar_length, target_t), Image.LANCZOS)
    barcode_img = barcode_img.point(lambda v: 0 if v < 128 else 255)
    # Rotate 90° CCW — bar_length becomes height, target_t becomes width
    barcode_img = barcode_img.rotate(90, expand=True)
    canvas.paste(barcode_img, (x, y))

    if not is_ean:
        # Code128: draw HRT as rotated text alongside the bars
        draw = ImageDraw.Draw(canvas)
        text_size = max(8, bar_thickness // 5)
        font = load_font(text_size, bold=False)
        bbox = draw.textbbox((0, 0), data, font=font)
        tw = bbox[2] - bbox[0]
        th = bbox[3] - bbox[1]
        txt_img = Image.new('L', (tw + 4, th + 4), 255)
        txt_draw = ImageDraw.Draw(txt_img)
        txt_draw.text((2, 2), data, fill=0, font=font)
        txt_img = txt_img.rotate(90, expand=True)
        tx = x + bar_thickness + 3
        ty = y + bar_length // 2 - txt_img.height // 2
        canvas.paste(txt_img, (tx, ty))


def image_to_zpl(img: Image.Image) -> str:
    """Convert a PIL Image to a ZPL string using the ^GF (Graphic Field) command.

    Sends one full-label bitmap to the printer — no host fonts needed.
    """
    w, h = img.size
    bytes_per_row = (w + 7) // 8
    total_bytes = bytes_per_row * h

    # Invert: our canvas is black-on-white; ZPL ^GF uses 1=print (dark)
    bw = ImageOps.invert(img.convert('L')).convert('1', dither=Image.Dither.NONE)
    raw = bw.tobytes()

    hex_data = raw.hex().upper()

    return (
        f"^XA^PW{w}^LL{h}^MNT^LH0,0"
        f"^FO0,0^GFA,{total_bytes},{total_bytes},{bytes_per_row},{hex_data}^FS"
        f"^XZ"
    )


def image_to_png_b64(img: Image.Image) -> str:
    """Encode a PIL Image as a base64-encoded PNG string."""
    import base64
    buf = BytesIO()
    img.save(buf, format='PNG')
    return base64.b64encode(buf.getvalue()).decode('utf-8')
