# SPDX-FileCopyrightText: 2019-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

import string
import bpy
event_types = tuple(
    e.identifier.lower()
    for e in bpy.types.Event.bl_rna.properties["type"].enum_items_static
)
del bpy

# We don't normally care about which one.
event_types_alias = {
    "ctrl": "left_ctrl",
    "shift": "left_shift",
    "alt": "left_alt",

    # Collides with Python keywords.
    "delete": "del",
}


# Note, we could add support for other keys using control characters,
# for example: `\xF12` could be used for the F12 key.
#
# Besides this, we could encode symbols into a regular string using our own syntax
# which can mix regular text and key symbols.
#
# At the moment this doesn't seem necessary, no need to add it.
event_types_text = (
    ('ZERO', "0", False),
    ('ONE', "1", False),
    ('TWO', "2", False),
    ('THREE', "3", False),
    ('FOUR', "4", False),
    ('FIVE', "5", False),
    ('SIX', "6", False),
    ('SEVEN', "7", False),
    ('EIGHT', "8", False),
    ('NINE', "9", False),

    ('ONE', "!", True),
    ('TWO', "@", True),
    ('THREE', "#", True),
    ('FOUR', "$", True),
    ('FIVE', "%", True),
    ('SIX', "^", True),
    ('SEVEN', "&", True),
    ('EIGHT', "*", True),
    ('NINE', "(", True),
    ('ZERO', ")", True),

    ('MINUS', "-", False),
    ('MINUS', "_", True),

    ('EQUAL', "=", False),
    ('EQUAL', "+", True),

    ('ACCENT_GRAVE', "`", False),
    ('ACCENT_GRAVE', "~", True),

    ('LEFT_BRACKET', "[", False),
    ('LEFT_BRACKET', "{", True),

    ('RIGHT_BRACKET', "]", False),
    ('RIGHT_BRACKET', "}", True),

    ('SEMI_COLON', ";", False),
    ('SEMI_COLON', ":", True),

    ('PERIOD', ".", False),
    ('PERIOD', ">", True),

    ('COMMA', ",", False),
    ('COMMA', "<", True),

    ('QUOTE', "'", False),
    ('QUOTE', '"', True),

    ('SLASH', "/", False),
    ('SLASH', "?", True),

    ('BACK_SLASH', "\\", False),
    ('BACK_SLASH', "|", True),


    *((ch_upper, ch, False) for (ch_upper, ch) in zip(string.ascii_uppercase, string.ascii_lowercase)),
    *((ch, ch, True) for ch in string.ascii_uppercase),

    ('SPACE', " ", False),
    ('RET', "\n", False),
    ('TAB', "\t", False),
)

event_types_text_from_char = {ch: (ty, is_shift) for (ty, ch, is_shift) in event_types_text}
event_types_text_from_event = {(ty, is_shift): ch for (ty, ch, is_shift) in event_types_text}


class _EventBuilder:
    __slots__ = (
        "_shared_event_gen",
        "_event_type",
        "_parent",
    )

    def __init__(self, event_gen, ty):
        self._shared_event_gen = event_gen
        self._event_type = ty
        self._parent = None

    def __call__(self, count=1):
        assert count >= 0
        for _ in range(count):
            self.tap()
        return self._shared_event_gen

    def _key_press_release(self, do_press=False, do_release=False, unicode_override=None):
        assert (do_press or do_release)
        keys_held = self._shared_event_gen._event_types_held
        build_keys = []
        e = self
        while e is not None:
            build_keys.append(e._event_type.upper())
            e = e._parent
        build_keys.reverse()

        events = [None, None]
        for i, value in enumerate(('PRESS', 'RELEASE')):
            if value == 'RELEASE':
                build_keys.reverse()
            for event_type in build_keys:
                if value == 'PRESS':
                    keys_held.add(event_type)
                else:
                    keys_held.remove(event_type)

                if (not do_press) and value == 'PRESS':
                    continue
                if (not do_release) and value == 'RELEASE':
                    continue

                shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
                ctrl = 'LEFT_CTRL' in keys_held or 'RIGHT_CTRL' in keys_held
                shift = 'LEFT_SHIFT' in keys_held or 'RIGHT_SHIFT' in keys_held
                alt = 'LEFT_ALT' in keys_held or 'RIGHT_ALT' in keys_held
                oskey = 'OSKEY' in keys_held

                unicode = None
                if value == 'PRESS':
                    if ctrl is False and alt is False and oskey is False:
                        if unicode_override is not None:
                            unicode = unicode_override
                        else:
                            unicode = event_types_text_from_event.get((event_type, shift))
                            if unicode is None and shift:
                                # Some keys don't care about shift
                                unicode = event_types_text_from_event.get((event_type, False))

                event = self._shared_event_gen.window.event_simulate(
                    type=event_type,
                    value=value,
                    unicode=unicode,
                    shift=shift,
                    ctrl=ctrl,
                    alt=alt,
                    oskey=oskey,
                    x=self._shared_event_gen._mouse_co[0],
                    y=self._shared_event_gen._mouse_co[1],
                )
                events[i] = event
        return tuple(events)

    def tap(self):
        return self._key_press_release(do_press=True, do_release=True)

    def press(self):
        return self._key_press_release(do_press=True)[0]

    def release(self):
        return self._key_press_release(do_release=True)[1]

    def cursor_motion(self, coords):
        coords = list(coords)
        self._shared_event_gen.cursor_position_set(*coords[0], move=True)
        yield

        event = self.press()
        shift = event.shift
        ctrl = event.ctrl
        shift = event.shift
        alt = event.alt
        oskey = event.oskey
        yield

        for x, y in coords:
            self._shared_event_gen.window.event_simulate(
                type='MOUSEMOVE',
                value='NOTHING',
                unicode=None,
                shift=shift,
                ctrl=ctrl,
                alt=alt,
                oskey=oskey,
                x=x,
                y=y
            )
            yield
        self._shared_event_gen.cursor_position_set(x, y, move=False)
        self.release()
        yield

    def __getattr__(self, attr):
        attr = event_types_alias.get(attr, attr)
        if attr in event_types:
            e = _EventBuilder(self._shared_event_gen, attr)
            e._parent = self
            return e
        raise Exception(f"{attr!r} not found in {event_types!r}")


class EventGenerate:
    __slots__ = (
        "window",

        "_mouse_co",
        "_event_types_held",
    )

    def __init__(self, window):
        self.window = window
        self._mouse_co = [0, 0]
        self._event_types_held = set()

        self.cursor_position_set(window.width // 2, window.height // 2)

    def cursor_position_set(self, x, y, move=False):
        self._mouse_co[:] = x, y
        if move:
            self.window.event_simulate(
                type='MOUSEMOVE',
                value='NOTHING',
                x=x,
                y=y,
            )

    def text(self, text):
        """ Type in entire phrases. """
        for ch in text:
            ty, shift = event_types_text_from_char[ch]
            ty = ty.lower()
            if shift:
                eb = getattr(_EventBuilder(self, 'left_shift'), ty)
            else:
                eb = _EventBuilder(self, ty)
            eb.tap()
        return self

    def text_unicode(self, text):
        # Since the only purpose of this key-press is to enter text
        # the key can be almost anything, use a key which isn't likely to be assigned ot any other action.
        #
        # If it were possible `EVT_UNKNOWNKEY` would be most correct
        # as dead keys map to this and still enter text.
        ty_dummy = 'F24'
        for ch in text:
            eb = _EventBuilder(self, ty_dummy)
            eb._key_press_release(do_press=True, do_release=True, unicode_override=ch)
        return self

    def __getattr__(self, attr):
        attr = event_types_alias.get(attr, attr)
        if attr in event_types:
            return _EventBuilder(self, attr)
        raise Exception(f"{attr!r} not found in {event_types!r}")

    def __del__(self):
        if self._event_types_held:
            print("'__del__' with keys held:", repr(self._event_types_held))


def run(
        event_iter, *,
        on_error=None,
        on_exit=None,
        on_step_command_pre=None,
        on_step_command_post=None,
):
    import bpy

    TICKS = 4  # 3 works, 4  to be on the safe side.

    def event_step():
        # Run once 'TICKS' is reached.
        if event_step._ticks < TICKS:
            event_step._ticks += 1
            return 0.0
        event_step._ticks = 0

        if on_step_command_pre:
            if event_step.run_events.gi_frame is not None:
                import shlex
                import subprocess
                subprocess.call(
                    shlex.split(
                        on_step_command_pre.replace(
                            "{file}", event_step.run_events.gi_frame.f_code.co_filename,
                        ).replace(
                            "{line}", str(event_step.run_events.gi_frame.f_lineno),
                        )
                    )
                )
        try:
            val = next(event_step.run_events, Ellipsis)
        except Exception:
            import traceback
            traceback.print_exc()
            if on_error is not None:
                on_error()
            if on_exit is not None:
                on_exit()
            return None

        if on_step_command_post:
            if event_step.run_events.gi_frame is not None:
                import shlex
                import subprocess
                subprocess.call(
                    shlex.split(
                        on_step_command_post.replace(
                            "{file}", event_step.run_events.gi_frame.f_code.co_filename,
                        ).replace(
                            "{line}", str(event_step.run_events.gi_frame.f_lineno),
                        )
                    )
                )

        if isinstance(val, EventGenerate) or val is None:
            return 0.0
        elif val is Ellipsis:
            if on_exit is not None:
                on_exit()
            return None
        else:
            raise Exception(f"{val!r} of type {type(val)!r} not supported")

    event_step.run_events = iter(event_iter)
    event_step._ticks = 0

    bpy.app.timers.register(event_step, first_interval=0.0)


def setup_default_preferences(preferences):
    """ Set preferences useful for automation.
    """
    preferences.view.show_splash = False
    preferences.view.smooth_view = 0
    preferences.view.use_save_prompt = False
    preferences.filepaths.use_auto_save_temporary_files = False
