r"""RotatoCAM post-processor for the HolzProfi CNC6090 on an HC-204A control.

Drop this file into:      %USERPROFILE%\.rotatocam\posts\
then restart RotatoCAM.   It appears in the Controller dropdown as "HolzProfi CNC6090 / HC-204A
                          (custom)".

WHY YOU MIGHT WANT THIS FILE
    From RotatoCAM 0.69.2.223.14 this post is built in and you do not need it. On 0.69.2.223.13 it
    is not, and this drop-in adds it. Once you are on .14 or later you can delete this file; if you
    keep it, it simply takes precedence over the identical built-in.

ALSO SET, IN MACHINE SETUP
    Rotary axis letter:  A
    Rotary runs along:   Y      <- the CNC6090's chuck points down Y, not X. Needs 0.69.2.223.13
                                   or later. Without it the program comes out a quarter turn wrong.

WHAT THIS POST DOES AND WHY
    Built from the HC-204A manual (Chengdu Xingduowei, 28 pp), whose accepted command set is small:

        G00 G01 G02 G03 G04 G17 G18 G19 G28 G43.4 G49 G50.1 G51.1 G53 G54~G59 G90 G91
        M03 M04 M05 M08 M09 M12 M13 M30 M34 M35 M60 M61

    Missing from those lists, and therefore never emitted here: G20 G21 G93 G94 M00 M06.

    * No G93/G94 -> no feed-mode word is sent, and rotary feeds are CONVERTED into ordinary
      per-minute feeds rather than relabelled. Printing an inverse-time number where the control
      reads mm/min is wrong by orders of magnitude, not slightly wrong.
    * No G20/G21 -> metric only. An inch job is refused rather than sent without a units word.
    * No M06/M00 -> there is no documented way to pause a running program, so multi-operation
      programs are refused. Export each operation on its own and change tools at the pendant.

    Simultaneous four-axis IS supported: the manual's parameter table has a "Speed ratio of rotating
    shaft ... in 4-axis linkage" setting (HC-204A default 2) and the model list calls it a
    "single-head 4-axis handheld control system". Note that this parameter scales rotary speed by a
    factor no program can see.

NOT VERIFIED BY CUTTING. Air-cut the first job. Three things the manual does not answer:
    1. Does the control accept absolute A past 360 and keep turning the same way, or does it wrap?
       A continuous finish spiral emits A450, A720, A1080 and must not be wrapped. If yours wraps,
       say so and the post can be changed.
    2. When A moves with XYZ, does the control time the block by counting degrees as millimetres,
       the way grbl does? The feed conversion assumes it does, because nothing better is documented.
    3. Is G04's P word seconds or milliseconds? RotatoCAM emits the spindle warm-up dwell as
       G4 P<seconds>, so on a millisecond control a 2 s warm-up becomes 2 ms. That fails safe, but
       set the dwell to 0 in Post settings if you would rather it were not there.

Report anything you learn to therealrevjmoney@gmail.com and it can go into the built-in post.
"""

from rotatocam.post._emit import render, render_program
from rotatocam.post.base import Post

_DIALECT = "HolzProfi CNC6090 / HC-204A (4-axis linkage, A about Y)"
_ROTARY_LETTER = "A"          # pinned: the machine's rotary word is A
_PREAMBLE = ("G90", "G17")    # no units word and no feed-mode word exist on this control

_INCH_MSG = (
    "The HC-204A post is metric-only. The controller's command table lists neither G20 nor G21, so "
    "there is no way to tell it the program changed units. Set G-code output to mm."
)
_TOOLCHANGE_MSG = (
    "The HC-204A has no documented tool-change or program-pause code: its manual lists M03 M04 M05 "
    "M08 M09 M12 M13 M30 M34 M35 M60 M61, and neither M06 nor M00 appears. A pause that does not "
    "pause means the next tool cuts at the previous tool's depth. Export each operation as its own "
    "program and change tools at the pendant between them."
)


def _strip_default_preamble(gcode):
    """Replace RotatoCAM's standard modal line with this control's.

    Only needed on 0.69.2.223.13, whose renderer has no ``preamble_modes`` argument. The line is
    emitted verbatim by the renderer, so matching it exactly is safe; if it is not found, nothing is
    changed and the caller has already had the correct output.
    """
    for unit in ("G21", "G20"):
        old = unit + " G90 G94\n"
        if old in gcode:
            return gcode.replace(old, "\n".join(_PREAMBLE) + "\n", 1)
    return gcode


def _render(fn, *args, **kw):
    """Call the shared renderer, using ``preamble_modes`` where the installed version has it."""
    try:
        return fn(*args, preamble_modes=_PREAMBLE, **kw)
    except TypeError:
        return _strip_default_preamble(fn(*args, **kw))


class HC204APost(Post):
    """HolzProfi CNC6090 / HC-204A - 4-axis linkage, rotary A about machine Y."""

    controller_key = "hc204a"
    display_name = "HolzProfi CNC6090 / HC-204A"

    def _check(self, options):
        if getattr(options, "output_units", "mm") == "inch":
            raise ValueError(_INCH_MSG)

    def post(self, toolpath, options):
        self._check(options)
        return _render(render, toolpath, options, _ROTARY_LETTER, _DIALECT,
                       end_code="M30", wrap_percent=True, comment_style="paren",
                       force_g94=True)

    def post_program(self, toolpath, spans, options):
        self._check(options)
        # The renderer emits the T/M6/M0 tool-change block between EVERY pair of operations, not
        # only when the tool number changes, so the refusal is on the count.
        if len(spans) > 1:
            raise ValueError(_TOOLCHANGE_MSG)
        return _render(render_program, toolpath, spans, options, _ROTARY_LETTER, _DIALECT,
                       end_code="M30", wrap_percent=True, comment_style="paren",
                       force_g94=True)
