# 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 typing import TYPE_CHECKING
from dataclasses import dataclass

from efro.error import CleanError
from efro.terminal import Clr

if TYPE_CHECKING:
    from typing import Optional, List, Dict


@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


def _emit_group_build_lines(targets: List[Target], basename: str) -> List[str]:
    """Gen a group build target."""
    del basename  # Unused.
    out: List[str] = []
    if not targets:
        return out
    all_dsts = set()
    for target in targets:
        all_dsts.add(target.dst)
    out.append('resources: \\\n  ' + ' \\\n  '.join(
        dst.replace(' ', '\\ ') for dst in sorted(all_dsts)) + '\n')
    return out


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


def _emit_group_efrocache_lines(targets: List[Target]) -> List[str]:
    """Gen a group clean target."""
    out: List[str] = []
    if not targets:
        return out
    all_dsts = set()
    for target in 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


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


def _add_windows_icon(targets: List[Target], generic: bool, oculus: bool,
                      inputs: bool) -> None:

    sizes = [256, 128, 96, 64, 48, 32, 16]
    all_icons = []
    for size in sizes:
        dst_base = 'build'
        src = os.path.join('src', '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:
            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 [
        (ROOT_DIR + '/ballisticacore-windows/Generic/BallisticaCore.ico',
         generic),
        (ROOT_DIR + '/ballisticacore-windows/Oculus/BallisticaCore.ico',
         oculus),
    ]:
        cmd = ('convert ' + ''.join([' "' + f + '"'
                                     for f in all_icons]) + ' "' + path + '"')
        if enable:
            targets.append(Target(src=all_icons, dst=path, cmd=cmd))


def _add_ios_app_icon(targets: List[Target]) -> 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('src', 'icon', 'icon_flat.png')
        dst = os.path.join(
            ROOT_DIR, 'ballisticacore-xcode', 'BallisticaCore 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 + '"'])
        targets.append(Target(src=[src], dst=dst, cmd=cmd))


def _add_macos_app_icon(targets: List[Target]) -> 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])
        src = os.path.join(RES_DIR, 'src', 'icon', 'icon_clipped.png')
        dst = os.path.join(
            ROOT_DIR,
            'ballisticacore-xcode',
            'BallisticaCore 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 + '"'])
        targets.append(Target(src=[src], dst=dst, cmd=cmd))


def _add_android_app_icon(targets: List[Target],
                          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(RES_DIR, 'src', 'icon', src_name)
        dst = os.path.join(ROOT_DIR, 'ballisticacore-android',
                           'BallisticaCore', 'src', variant_name, 'res',
                           'mipmap-' + size[0], 'ic_launcher.png')
        cmd = ' '.join(
            [RESIZE_CMD,
             str(res),
             str(res), '"' + src + '"', '"' + dst + '"'])
        targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))


def _add_android_app_icon_new(targets: List[Target],
                              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(RES_DIR, 'src', 'icon', src_fg_name)
        dst = os.path.join(ROOT_DIR, 'ballisticacore-android',
                           'BallisticaCore', 'src', variant_name, 'res',
                           'mipmap-' + size[0], 'ic_launcher_foreground.png')
        cmd = ' '.join(
            [RESIZE_CMD,
             str(res),
             str(res), '"' + src + '"', '"' + dst + '"'])
        targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True))

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


def _add_android_cardboard_app_icon(targets: List[Target]) -> None:
    _add_android_app_icon(targets=targets,
                          src_name='icon_clipped_vr.png',
                          variant_name='cardboard')


def _add_android_cardboard_app_icon_new(targets: List[Target]) -> None:
    _add_android_app_icon_new(targets=targets,
                              src_fg_name='icon_android_layered_fg_vr.png',
                              variant_name='cardboard')


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


def _add_apple_tv_top_shelf(targets: List[Target]) -> 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(RES_DIR, 'src', 'banner',
                               'banner_' + instance[0] + '.png')
            dst = os.path.join(
                ROOT_DIR,
                'ballisticacore-xcode',
                'BallisticaCore 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 + '"'
            ])
            targets.append(Target(src=[src], dst=dst, cmd=cmd))


def _add_apple_tv_3d_icon(targets: List[Target]) -> None:
    res = (400, 240)
    for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']:
        for scale in [1, 2]:
            src = os.path.join(RES_DIR, 'src', 'icon_appletv',
                               layer.lower() + '.png')
            dst = os.path.join(
                ROOT_DIR,
                'ballisticacore-xcode',
                'BallisticaCore 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 + '"'
            ])
            targets.append(Target(src=[src], dst=dst, cmd=cmd))


def _add_apple_tv_store_icon(targets: List[Target]) -> None:
    res = (1280, 768)
    for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']:
        for scale in [1]:
            src = os.path.join(RES_DIR, 'src', 'icon_appletv',
                               layer.lower() + '.png')
            dst = os.path.join(
                ROOT_DIR,
                'ballisticacore-xcode',
                'BallisticaCore 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 + '"'
            ])
            targets.append(Target(src=[src], dst=dst, cmd=cmd))


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


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


def update(projroot: str, check: bool) -> None:
    """main script entry point"""
    # pylint: disable=too-many-locals
    # pylint: disable=too-many-statements

    from efrotools import getconfig

    # Operate out of root dist dir for consistency.
    os.chdir(projroot)

    public = getconfig(Path('.'))['public']
    assert isinstance(public, bool)

    fname = 'resources/Makefile'
    with open(fname, encoding='utf-8') as infile:
        original = infile.read()
    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)
    targets: List[Target] = []
    basename = 'public'
    our_lines_public = (_empty_line_if(bool(targets)) +
                        _emit_group_build_lines(targets, basename) +
                        _emit_group_clean_lines(targets, basename) +
                        [t.emit() for t in targets])

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

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

    if out == original:
        print(f'{fname} is up to date.')
    else:
        if check:
            if bool(False):
                print(f'FOUND------\n{original}\nEND FOUND--------\n'
                      f'EXPECTED------\n{out}\nEND EXPECTED-------\n')
            raise CleanError(f"ERROR: file is out of date: '{fname}'.")
        print(f'{Clr.SBLU}Updating: {fname}{Clr.RST}')
        with open(fname, 'w', encoding='utf-8') as outfile:
            outfile.write(out)
