# 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Copyright 2026 SerialLab Corp.  All rights reserved.

"""
Data Matrix barcode generation for GS1 pharmaceutical barcodes.

This module generates GS1 Data Matrix barcodes from either raw GS1 element
strings or EPC URN values.  It supports SVG and PNG output formats.

Dependencies:
    - ppf.datamatrix (pure Python Data Matrix encoder, no native libs)
    - Pillow (optional, required only for PNG output)

Usage::

    from gs123.datamatrix import GS1DataMatrix

    # From a GS1 element string
    dm = GS1DataMatrix("(01)20614141073461(21)0000010020(17)261231(10)LOTA1")
    dm.save_svg("barcode.svg")
    dm.save_png("barcode.png")

    # From an EPC URN (requires company_prefix_length)
    dm = GS1DataMatrix.from_urn(
        "urn:epc:id:sgtin:0614141.207346.10020",
        company_prefix_length=7,
        lot="LOTA1",
        expiration="261231",
    )
    dm.save_png("barcode.png", scale=12)

    # Get raw bytes
    svg_bytes = dm.svg()
    png_bytes = dm.png(scale=10)
"""

from __future__ import annotations

import io
from typing import Optional

from ppf.datamatrix import DataMatrix

from gs123.conversion import URNConverter, BarcodeConverter

# GS1 FNC1 is encoded as GS (ASCII 29) between variable-length fields
# when the barcode string uses no parentheses.  With parentheses the
# boundaries are implicit.
_GS = "\x1d"


class GS1DataMatrix:
    """
    Generates a GS1 Data Matrix barcode from a GS1 element string or EPC URN.

    The encoded payload follows GS1 General Specifications: the symbology
    identifier ``]d2`` indicates a GS1 Data Matrix (FNC1 in first position).
    ``ppf.datamatrix`` encodes plain text; the resulting barcode is
    scannable by any GS1-aware reader.

    Attributes:
        data (str): The GS1 element string that is encoded.
        matrix (list[list[int]]): The 2-D module matrix (1 = dark, 0 = light).
    """

    def __init__(self, gs1_element_string: str):
        """
        Create a Data Matrix from a GS1 element string.

        The string may use parenthesised AI notation —
        ``(01)04012345678901(21)SN123(17)261231(10)LOT1`` — or bare AI
        notation with ``\\x1d`` (GS) separators.

        :param gs1_element_string: The GS1 element string to encode.
        """
        if not gs1_element_string:
            raise ValueError("gs1_element_string must not be empty.")
        self.data: str = gs1_element_string
        self._dm = DataMatrix(self.data)

    # ------------------------------------------------------------------
    # Alternate constructors
    # ------------------------------------------------------------------

    @classmethod
    def from_urn(
        cls,
        urn: str,
        company_prefix_length: int = 7,
        lot: Optional[str] = None,
        expiration: Optional[str] = None,
        serial_number_length: int = 12,
        padding_character: str = "0",
        parentheses: bool = True,
    ) -> "GS1DataMatrix":
        """
        Build a Data Matrix from an EPC URN value.

        Supports both SGTIN and SSCC URNs.  For SGTIN barcodes the *lot*
        and *expiration* parameters are appended when supplied.

        :param urn: An ``urn:epc:id:sgtin:`` or ``urn:epc:id:sscc:`` value.
        :param company_prefix_length: Length of the GS1 company prefix.
        :param lot: Lot / batch number (AI 10).
        :param expiration: Expiration date in YYMMDD format (AI 17).
        :param serial_number_length: Pad serial numbers to this length.
        :param padding_character: Character used for serial number padding.
        :param parentheses: If *True*, wrap AIs in parentheses.
        :return: A new :class:`GS1DataMatrix` instance.
        """
        converter = URNConverter(urn)
        barcode_val = converter.get_barcode_value(
            lot=lot,
            expiration=expiration,
            parenthesis=parentheses,
            serial_number_padding=True,
            serial_number_length=serial_number_length,
            padding_character=padding_character,
        )
        return cls(barcode_val)

    @classmethod
    def from_barcode(
        cls,
        barcode_val: str,
        company_prefix_length: int = 7,
        lot: Optional[str] = None,
        expiration: Optional[str] = None,
        parentheses: bool = True,
    ) -> "GS1DataMatrix":
        """
        Build a Data Matrix by first parsing a GS1 barcode string with
        :class:`~gs123.conversion.BarcodeConverter`, then re-encoding it
        with optional lot / expiry overrides.

        This is useful when you have a scanned barcode value and want to
        regenerate a clean Data Matrix from it.

        :param barcode_val: A scanned or constructed GS1 barcode string.
        :param company_prefix_length: Length of the GS1 company prefix.
        :param lot: Override lot value (or use the one parsed from barcode).
        :param expiration: Override expiry (or use parsed value).
        :param parentheses: Wrap AIs in parentheses.
        :return: A new :class:`GS1DataMatrix` instance.
        """
        bc = BarcodeConverter(barcode_val, company_prefix_length)
        urn = bc.epc_urn
        use_lot = lot or bc.lot
        use_exp = expiration or bc.expiration_date
        return cls.from_urn(
            urn,
            company_prefix_length=company_prefix_length,
            lot=use_lot,
            expiration=use_exp,
            parentheses=parentheses,
        )

    # ------------------------------------------------------------------
    # Matrix access
    # ------------------------------------------------------------------

    @property
    def matrix(self) -> list:
        """Return the 2-D module matrix (list of lists, 1=dark 0=light)."""
        return self._dm.matrix

    @property
    def symbol_size(self) -> tuple:
        """Return ``(rows, cols)`` of the Data Matrix symbol."""
        m = self._dm.matrix
        return (len(m), len(m[0]) if m else 0)

    # ------------------------------------------------------------------
    # SVG output
    # ------------------------------------------------------------------

    def svg(self, scale: int = 10, quiet_zone: int = 1, color: str = "#000") -> str:
        """
        Return the Data Matrix as an SVG string.

        :param scale: Pixels per module.
        :param quiet_zone: Number of quiet-zone modules around the symbol.
        :param color: Module (dark) colour.
        :return: SVG markup as a string.
        """
        m = self._dm.matrix
        rows = len(m)
        cols = len(m[0]) if m else 0
        total_w = (cols + 2 * quiet_zone) * scale
        total_h = (rows + 2 * quiet_zone) * scale

        parts = [
            '<?xml version="1.0" encoding="utf-8" ?>',
            f'<svg xmlns="http://www.w3.org/2000/svg" '
            f'width="{total_w}" height="{total_h}" '
            f'viewBox="0 0 {total_w} {total_h}">',
            f'<rect width="100%" height="100%" fill="#FFF"/>',
        ]
        for y, row in enumerate(m):
            for x, val in enumerate(row):
                if val:
                    px = (x + quiet_zone) * scale
                    py = (y + quiet_zone) * scale
                    parts.append(
                        f'<rect x="{px}" y="{py}" '
                        f'width="{scale}" height="{scale}" fill="{color}"/>'
                    )
        parts.append("</svg>")
        return "\n".join(parts)

    def save_svg(
        self,
        path: str,
        scale: int = 10,
        quiet_zone: int = 1,
        color: str = "#000",
    ) -> None:
        """
        Write the Data Matrix to an SVG file.

        :param path: Output file path.
        :param scale: Pixels per module.
        :param quiet_zone: Quiet-zone modules.
        :param color: Module colour.
        """
        with open(path, "w", encoding="utf-8") as f:
            f.write(self.svg(scale=scale, quiet_zone=quiet_zone, color=color))

    # ------------------------------------------------------------------
    # PNG output (requires Pillow)
    # ------------------------------------------------------------------

    def png(self, scale: int = 10, quiet_zone: int = 1) -> bytes:
        """
        Return the Data Matrix as PNG bytes.

        :param scale: Pixels per module.
        :param quiet_zone: Quiet-zone modules around the symbol.
        :return: PNG image bytes.
        :raises ImportError: If Pillow is not installed.
        """
        try:
            from PIL import Image
        except ImportError:
            raise ImportError(
                "Pillow is required for PNG output.  "
                "Install it with: pip install Pillow"
            )

        m = self._dm.matrix
        rows = len(m)
        cols = len(m[0]) if m else 0
        total_w = (cols + 2 * quiet_zone) * scale
        total_h = (rows + 2 * quiet_zone) * scale

        img = Image.new("1", (total_w, total_h), 1)  # 1-bit white
        pixels = img.load()
        for y, row in enumerate(m):
            for x, val in enumerate(row):
                if val:
                    x0 = (x + quiet_zone) * scale
                    y0 = (y + quiet_zone) * scale
                    for dy in range(scale):
                        for dx in range(scale):
                            pixels[x0 + dx, y0 + dy] = 0

        buf = io.BytesIO()
        img.save(buf, format="PNG")
        return buf.getvalue()

    def save_png(self, path: str, scale: int = 10, quiet_zone: int = 1) -> None:
        """
        Write the Data Matrix to a PNG file.

        :param path: Output file path.
        :param scale: Pixels per module.
        :param quiet_zone: Quiet-zone modules.
        """
        with open(path, "wb") as f:
            f.write(self.png(scale=scale, quiet_zone=quiet_zone))

    # ------------------------------------------------------------------
    # Dunder methods
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        rows, cols = self.symbol_size
        return f"GS1DataMatrix(data={self.data!r}, size={rows}x{cols})"

    def __str__(self) -> str:
        return self.data
