# Released under the MIT License. See LICENSE for details.
#
"""Generate our resources Makefile.

(builds things like icons, banners, images, etc.)
"""

from __future__ import annotations

import os
from pathlib import Path
from dataclasses import dataclass

from efro.error import CleanError


# These paths need to be relative to the dir we're writing the Makefile to.
ROOT_DIR = '$(PROJ_DIR)'
TOOLS_DIR = '$(TOOLS_DIR)'
BUILD_DIR = '$(BUILD_DIR)'
RESIZE_CMD = os.path.join(TOOLS_DIR, 'pcommand resize_image')


@dataclass
class Target:
    """A target to be added to the Makefile."""

    src: list[str]
    dst: str
    cmd: str
    mkdir: bool = False

    def emit(self) -> str:
        """Gen a Makefile target."""
        out: str = self.dst.replace(' ', '\\ ')
        out += (
            ' : '
            + ' '.join(s for s in self.src)
            + (
                ('\n\t@mkdir -p "' + os.path.dirname(self.dst) + '"')
                if self.mkdir
                else ''
            )
            + '\n\t@'
            + self.cmd
            + '\n'
        )
        return out


class ResourcesMakefileGenerator:
    """Does the thing."""

    def __init__(
        self,
        projroot: str,
        existing_data: str,
        projname: str,
    ) -> None:
        from efrotools.project import getprojectconfig

        self.public = getprojectconfig(Path(projroot))['public']
        assert isinstance(self.public, bool)
        self.existing_data = existing_data
        self.projroot = projroot
        self.targets: list[Target] = []

        # Regular and lowercase project name.
        self.nameu = projname
        self.namel = projname.lower()

    def run(self) -> str:
        """Does the thing."""
        # fname = 'src/resources/Makefile'
        # with open(fname, encoding='utf-8') as infile:
        #     original = infile.read()
        original = self.existing_data
        lines = original.splitlines()

        auto_start_public = lines.index('# __AUTOGENERATED_PUBLIC_BEGIN__')
        auto_end_public = lines.index('# __AUTOGENERATED_PUBLIC_END__')
        auto_start_private = lines.index('# __AUTOGENERATED_PRIVATE_BEGIN__')
        auto_end_private = lines.index('# __AUTOGENERATED_PRIVATE_END__')

        # Public targets (full sources available in public)

        basename = 'public'
        our_lines_public = (
            _empty_line_if(bool(self.targets))
            + self._emit_group_build_lines(basename)
            + self._emit_group_clean_lines(basename)
            + [t.emit() for t in self.targets]
        )

        # Only rewrite the private section in the private repo; otherwise
        # keep the existing one intact.
        if self.public:
            our_lines_private = lines[auto_start_private + 1 : auto_end_private]
        else:
            # Private targets (available in public through efrocache)
            self.targets = []
            basename = 'private'
            self._add_windows_icon(generic=True, oculus=False, inputs=False)
            our_lines_private_1 = (
                _empty_line_if(bool(self.targets))
                + self._emit_group_build_lines(basename)
                + self._emit_group_clean_lines(basename)
                + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in self.targets]
                + self._emit_group_efrocache_lines()
            )

            # Private-internal targets (not available at all in public)
            self.targets = []
            basename = 'private-internal'
            self._add_windows_icon(generic=False, oculus=True, inputs=True)
            self._add_ios_app_icon()
            self._add_macos_app_icon()
            self._add_android_app_icon()
            self._add_android_app_icon_new()
            self._add_android_cardboard_app_icon()
            self._add_android_cardboard_app_icon_new()
            self._add_android_tv_banner()
            self._add_apple_tv_top_shelf()
            self._add_apple_tv_3d_icon()
            self._add_apple_tv_store_icon()
            self._add_google_vr_icon()
            self._add_macos_cursor()
            our_lines_private_2 = (
                ['# __PUBSYNC_STRIP_BEGIN__']
                + _empty_line_if(bool(self.targets))
                + self._emit_group_build_lines(basename)
                + self._emit_group_clean_lines(basename)
                + [t.emit() for t in self.targets]
                + ['# __PUBSYNC_STRIP_END__']
            )
            our_lines_private = our_lines_private_1 + our_lines_private_2

        filtered = (
            lines[: auto_start_public + 1]
            + our_lines_public
            + lines[auto_end_public : auto_start_private + 1]
            + our_lines_private
            + lines[auto_end_private:]
        )
        out = '\n'.join(filtered) + '\n'

        return out

    def _emit_group_build_lines(self, basename: str) -> list[str]:
        """Gen a group build target."""
        del basename  # Unused.
        out: list[str] = []
        if not self.targets:
            return out
        all_dsts = set()
        for target in self.targets:
            all_dsts.add(target.dst)
        out.append(
            "# Add this section's targets to the overall resources target.\n"
            'resources: \\\n  '
            + ' \\\n  '.join(
                dst.replace(' ', '\\ ') for dst in sorted(all_dsts)
            )
            + '\n'
        )
        return out

    def _emit_group_clean_lines(self, basename: str) -> list[str]:
        """Gen a group clean target."""
        out: list[str] = []
        if not self.targets:
            return out
        all_dsts = set()
        for target in self.targets:
            all_dsts.add(target.dst)
        out.append(
            f'clean-{basename}:\n\trm -f '
            + ' \\\n         '.join('"' + dst + '"' for dst in sorted(all_dsts))
            + '\n'
        )
        out.append(
            '# Include this section in an overall clean.\n'
            f'clean: clean-{basename}\n'
        )

        return out

    def _emit_group_efrocache_lines(self) -> list[str]:
        """Gen a group clean target."""
        out: list[str] = []
        if not self.targets:
            return out
        all_dsts = set()
        for target in self.targets:
            # We may need to make pipeline adjustments if/when we get filenames
            # with spaces in them.
            if ' ' in target.dst:
                raise CleanError(
                    'FIXME: need to account for spaces in filename'
                    f' "{target.dst}".'
                )
            all_dsts.add(target.dst)
        out.append(
            'efrocache-list:\n\t@echo '
            + ' \\\n        '.join('"' + dst + '"' for dst in sorted(all_dsts))
            + '\n'
        )
        out.append('efrocache-build: resources\n')

        return out

    def _add_windows_icon(
        self, generic: bool, oculus: bool, inputs: bool
    ) -> None:
        sizes = [256, 128, 96, 64, 48, 32, 16]
        all_icons = []
        for size in sizes:
            dst_base = BUILD_DIR
            src = os.path.join('icon', 'icon_clipped.png')
            dst = os.path.join(dst_base, 'win_icon_' + str(size) + '_tmp.png')
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(size),
                    str(size),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            all_icons.append(dst)
            if inputs:
                self.targets.append(
                    Target(src=[src], dst=dst, cmd=cmd, mkdir=True)
                )

        # Assemble all the bits we just made into .ico files.
        for path, enable in [
            (
                f'{ROOT_DIR}/{self.namel}-windows/Generic/{self.nameu}.ico',
                generic,
            ),
            (
                f'{ROOT_DIR}/{self.namel}-windows/Oculus/{self.nameu}.ico',
                oculus,
            ),
        ]:
            cmd = (
                'convert '
                + ''.join([' "' + f + '"' for f in all_icons])
                + ' "'
                + path
                + '"'
            )
            if enable:
                self.targets.append(Target(src=all_icons, dst=path, cmd=cmd))

    def _add_ios_app_icon(self) -> None:
        sizes = [
            # (20, 2),
            # (20, 3),
            # (29, 2),
            # (29, 3),
            # (40, 2),
            # (40, 3),
            # (60, 2),
            # (60, 3),
            # (20, 1),
            # (29, 1),
            # (40, 1),
            # (76, 1),
            # (76, 2),
            # (83.5, 2),
            (1024, 1),
        ]
        for size in sizes:
            res = int(size[0] * size[1])
            src = os.path.join('icon', 'icon_flat.png')
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-xcode',
                f'{self.nameu} Shared',
                'Assets.xcassets',
                'AppIcon iOS.appiconset',
                'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd))

    def _add_macos_app_icon(self) -> None:
        sizes = [
            (16, 1),
            (16, 2),
            (32, 1),
            (32, 2),
            (128, 1),
            (128, 2),
            (256, 1),
            (256, 2),
            (512, 1),
            (512, 2),
        ]
        for size in sizes:
            res = int(size[0] * size[1])
            # The largest size gets used by the Mac App Store, and lots
            # of games seem to fill their entire icon canvas instead of
            # sticking with the big-sur icon size, so ours looks kinda
            # small next to those if we don't do the same. Strangely,
            # iOS apps in the Mac App Store also show up large like that
            # (as of Nov 2023). So we use a separate as-big-as-possible
            # icon for our largest size only. The downside of this is
            # our icon changes in appearance if someone cranks the
            # finder view options icon size slider all the way up, but
            # who actually does that?
            srcname = (
                'icon_clipped_mac_app_store.png'
                if size[0] == 512
                else 'icon_clipped_mac.png'
            )
            src = os.path.join('icon', srcname)
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-xcode',
                f'{self.nameu} Shared',
                'Assets.xcassets',
                'AppIcon macOS.appiconset',
                'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd))

    def _add_android_app_icon(
        self,
        src_name: str = 'icon_clipped.png',
        variant_name: str = 'main',
    ) -> None:
        sizes = [
            ('mdpi', 48),
            ('hdpi', 72),
            ('xhdpi', 96),
            ('xxhdpi', 144),
            ('xxxhdpi', 192),
        ]
        for size in sizes:
            res = size[1]
            src = os.path.join('icon', src_name)
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-android',
                f'{self.nameu}',
                'src',
                variant_name,
                'res',
                'mipmap-' + size[0],
                'ic_launcher.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

    def _add_android_app_icon_new(
        self,
        src_fg_name: str = 'icon_android_layered_fg.png',
        src_bg_name: str = 'icon_android_layered_bg.png',
        variant_name: str = 'main',
    ) -> None:
        sizes = [
            ('mdpi', 108),
            ('hdpi', 162),
            ('xhdpi', 216),
            ('xxhdpi', 324),
            ('xxxhdpi', 432),
        ]
        for size in sizes:
            res = size[1]

            # Foreground component.
            src = os.path.join('icon', src_fg_name)
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-android',
                f'{self.nameu}',
                'src',
                variant_name,
                'res',
                'mipmap-' + size[0],
                'ic_launcher_foreground.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

            # Background component.
            src = os.path.join('icon', src_bg_name)
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-android',
                f'{self.nameu}',
                'src',
                variant_name,
                'res',
                'mipmap-' + size[0],
                'ic_launcher_background.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

    def _add_android_cardboard_app_icon(self) -> None:
        self._add_android_app_icon(
            src_name='icon_clipped_vr.png',
            variant_name='cardboard',
        )

    def _add_android_cardboard_app_icon_new(self) -> None:
        self._add_android_app_icon_new(
            src_fg_name='icon_android_layered_fg_vr.png',
            variant_name='cardboard',
        )

    def _add_android_tv_banner(self) -> None:
        res = (320, 180)
        src = os.path.join('banner', 'banner_16x9.png')
        dst = os.path.join(
            ROOT_DIR,
            f'{self.namel}-android',
            f'{self.nameu}',
            'src',
            'main',
            'res',
            'drawable-xhdpi',
            'banner.png',
        )
        cmd = ' '.join(
            [
                RESIZE_CMD,
                str(res[0]),
                str(res[1]),
                '"' + src + '"',
                '"' + dst + '"',
            ]
        )
        self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

    def _add_apple_tv_top_shelf(self) -> None:
        instances = [
            ('24x9', '', '', 1920, 720),
            ('29x9', ' Wide', '_wide', 2320, 720),
        ]
        for instance in instances:
            for scale in [1, 2]:
                res = (instance[3] * scale, instance[4] * scale)
                src = os.path.join('banner', 'banner_' + instance[0] + '.png')
                dst = os.path.join(
                    ROOT_DIR,
                    f'{self.namel}-xcode',
                    f'{self.nameu} Shared',
                    'Assets.xcassets',
                    'tvOS App Icon & Top Shelf Image.brandassets',
                    'Top Shelf Image' + instance[1] + '.imageset',
                    'shelf' + instance[2] + '_' + str(scale) + 'x.png',
                )
                cmd = ' '.join(
                    [
                        RESIZE_CMD,
                        str(res[0]),
                        str(res[1]),
                        '"' + src + '"',
                        '"' + dst + '"',
                    ]
                )
                self.targets.append(Target(src=[src], dst=dst, cmd=cmd))

    def _add_apple_tv_3d_icon(self) -> None:
        res = (400, 240)
        for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']:
            for scale in [1, 2]:
                src = os.path.join('icon_appletv', layer.lower() + '.png')
                dst = os.path.join(
                    ROOT_DIR,
                    f'{self.namel}-xcode',
                    f'{self.nameu} Shared',
                    'Assets.xcassets',
                    'tvOS App Icon & Top Shelf Image.brandassets',
                    'App Icon.imagestack',
                    layer + '.imagestacklayer',
                    'Content.imageset',
                    layer.lower() + '_' + str(scale) + 'x.png',
                )
                cmd = ' '.join(
                    [
                        RESIZE_CMD,
                        str(res[0] * scale),
                        str(res[1] * scale),
                        '"' + src + '"',
                        '"' + dst + '"',
                    ]
                )
                self.targets.append(Target(src=[src], dst=dst, cmd=cmd))

    def _add_apple_tv_store_icon(self) -> None:
        res = (1280, 768)
        for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']:
            for scale in [1]:
                src = os.path.join('icon_appletv', layer.lower() + '.png')
                dst = os.path.join(
                    ROOT_DIR,
                    f'{self.namel}-xcode',
                    f'{self.nameu} Shared',
                    'Assets.xcassets',
                    'tvOS App Icon & Top Shelf Image.brandassets',
                    'App Icon - App Store.imagestack',
                    layer + '.imagestacklayer',
                    'Content.imageset',
                    layer.lower() + '_' + str(scale) + 'x.png',
                )
                cmd = ' '.join(
                    [
                        RESIZE_CMD,
                        str(res[0] * scale),
                        str(res[1] * scale),
                        '"' + src + '"',
                        '"' + dst + '"',
                    ]
                )
                self.targets.append(Target(src=[src], dst=dst, cmd=cmd))

    def _add_google_vr_icon(self) -> None:
        res = (512, 512)
        for layer in ['vr_icon_background', 'vr_icon']:
            src = os.path.join('icon_googlevr', layer + '.png')
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-android',
                f'{self.nameu}',
                'src',
                'cardboard',
                'res',
                'drawable-nodpi',
                layer + '.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res[0]),
                    str(res[1]),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

    def _add_macos_cursor(self) -> None:
        sizes = [
            (64, 1),
            (64, 2),
        ]
        for size in sizes:
            res = int(size[0] * size[1])
            src = os.path.join('cursor.png')
            dst = os.path.join(
                ROOT_DIR,
                f'{self.namel}-xcode',
                f'{self.nameu} Shared',
                'Assets.xcassets',
                'Cursor macOS.imageset',
                'cursor_' + str(size[0]) + 'x' + str(size[1]) + '.png',
            )
            cmd = ' '.join(
                [
                    RESIZE_CMD,
                    str(res),
                    str(res),
                    '"' + src + '"',
                    '"' + dst + '"',
                ]
            )
            self.targets.append(Target(src=[src], dst=dst, cmd=cmd))


def _empty_line_if(condition: bool) -> list[str]:
    return [''] if condition else []
