from distutils.spawn import find_executable
from os import environ
from os.path import join
from multiprocessing import cpu_count

from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException, build_platform


class Arch:

    command_prefix = None
    '''The prefix for NDK commands such as gcc.'''

    arch = ""
    '''Name of the arch such as: `armeabi-v7a`, `arm64-v8a`, `x86`...'''

    arch_cflags = []
    '''Specific arch `cflags`, expect to be overwrote in subclass if needed.'''

    common_cflags = [
        '-target {target}',
        '-fomit-frame-pointer'
    ]

    common_cppflags = [
        '-DANDROID',
        '-I{ctx.ndk.sysroot_include_dir}',
        '-I{python_includes}',
    ]

    common_ldflags = ['-L{ctx_libs_dir}']

    common_ldlibs = ['-lm']

    common_ldshared = [
        '-pthread',
        '-shared',
        '-Wl,-O1',
        '-Wl,-Bsymbolic-functions',
    ]

    def __init__(self, ctx):
        self.ctx = ctx

        # Allows injecting additional linker paths used by any recipe.
        # This can also be modified by recipes (like the librt recipe)
        # to make sure that some sort of global resource is available &
        # linked for all others.
        self.extra_global_link_paths = []

    def __str__(self):
        return self.arch

    @property
    def ndk_lib_dir(self):
        return join(self.ctx.ndk.sysroot_lib_dir, self.command_prefix)

    @property
    def ndk_lib_dir_versioned(self):
        return join(self.ndk_lib_dir, str(self.ctx.ndk_api))

    @property
    def include_dirs(self):
        return [
            "{}/{}".format(
                self.ctx.include_dir,
                d.format(arch=self))
            for d in self.ctx.include_dirs]

    @property
    def target(self):
        # As of NDK r19, the toolchains installed by default with the
        # NDK may be used in-place. The make_standalone_toolchain.py script
        # is no longer needed for interfacing with arbitrary build systems.
        # See: https://developer.android.com/ndk/guides/other_build_systems
        return '{triplet}{ndk_api}'.format(
            triplet=self.command_prefix, ndk_api=self.ctx.ndk_api
        )

    @property
    def clang_exe(self):
        """Full path of the clang compiler depending on the android's ndk
        version used."""
        return self.get_clang_exe()

    @property
    def clang_exe_cxx(self):
        """Full path of the clang++ compiler depending on the android's ndk
        version used."""
        return self.get_clang_exe(plus_plus=True)

    def get_clang_exe(self, with_target=False, plus_plus=False):
        """Returns the full path of the clang/clang++ compiler, supports two
        kwargs:

          - `with_target`: prepend `target` to clang
          - `plus_plus`: will return the clang++ compiler (defaults to `False`)
        """
        compiler = 'clang'
        if with_target:
            compiler = '{target}-{compiler}'.format(
                target=self.target, compiler=compiler
            )
        if plus_plus:
            compiler += '++'
        return join(self.ctx.ndk.llvm_bin_dir, compiler)

    def get_env(self, with_flags_in_cc=True):
        env = {}

        # HOME: User's home directory
        #
        # Many tools including p4a store outputs in the user's home
        # directory. This is found from the HOME environment variable
        # and falls back to the system account database. Setting HOME
        # can be used to globally divert these tools to use a different
        # path. Furthermore, in containerized environments the user may
        # not exist in the account database, so if HOME isn't set than
        # these tools will fail.
        if 'HOME' in environ:
            env['HOME'] = environ['HOME']

        # CFLAGS/CXXFLAGS: the processor flags
        env['CFLAGS'] = ' '.join(self.common_cflags).format(target=self.target)
        if self.arch_cflags:
            # each architecture may have has his own CFLAGS
            env['CFLAGS'] += ' ' + ' '.join(self.arch_cflags)
        env['CXXFLAGS'] = env['CFLAGS']

        # CPPFLAGS (for macros and includes)
        env['CPPFLAGS'] = ' '.join(self.common_cppflags).format(
            ctx=self.ctx,
            command_prefix=self.command_prefix,
            python_includes=join(
                self.ctx.get_python_install_dir(self.arch),
                'include/python{}'.format(self.ctx.python_recipe.version[0:3]),
            ),
        )

        # LDFLAGS: Link the extra global link paths first before anything else
        # (such that overriding system libraries with them is possible)
        env['LDFLAGS'] = (
            ' '
            + " ".join(
                [
                    "-L'"
                    + link_path.replace("'", "'\"'\"'")
                    + "'"  # no shlex.quote in py2
                    for link_path in self.extra_global_link_paths
                ]
            )
            + ' ' + ' '.join(self.common_ldflags).format(
                ctx_libs_dir=self.ctx.get_libs_dir(self.arch)
            )
        )

        # LDLIBS: Library flags or names given to compilers when they are
        # supposed to invoke the linker.
        env['LDLIBS'] = ' '.join(self.common_ldlibs)

        # CCACHE
        ccache = ''
        if self.ctx.ccache and bool(int(environ.get('USE_CCACHE', '1'))):
            # print('ccache found, will optimize builds')
            ccache = self.ctx.ccache + ' '
            env['USE_CCACHE'] = '1'
            env['NDK_CCACHE'] = self.ctx.ccache
            env.update(
                {k: v for k, v in environ.items() if k.startswith('CCACHE_')}
            )

        # Compiler: `CC` and `CXX` (and make sure that the compiler exists)
        env['PATH'] = self.ctx.env['PATH']
        cc = find_executable(self.clang_exe, path=env['PATH'])
        if cc is None:
            print('Searching path are: {!r}'.format(env['PATH']))
            raise BuildInterruptingException(
                'Couldn\'t find executable for CC. This indicates a '
                'problem locating the {} executable in the Android '
                'NDK, not that you don\'t have a normal compiler '
                'installed. Exiting.'.format(self.clang_exe))

        if with_flags_in_cc:
            env['CC'] = '{ccache}{exe} {cflags}'.format(
                exe=self.clang_exe,
                ccache=ccache,
                cflags=env['CFLAGS'])
            env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
                execxx=self.clang_exe_cxx,
                ccache=ccache,
                cxxflags=env['CXXFLAGS'])
        else:
            env['CC'] = '{ccache}{exe}'.format(
                exe=self.clang_exe,
                ccache=ccache)
            env['CXX'] = '{ccache}{execxx}'.format(
                execxx=self.clang_exe_cxx,
                ccache=ccache)

        # Android's LLVM binutils
        env['AR'] = self.ctx.ndk.llvm_ar
        env['RANLIB'] = self.ctx.ndk.llvm_ranlib
        env['STRIP'] = f'{self.ctx.ndk.llvm_strip} --strip-unneeded'
        env['READELF'] = self.ctx.ndk.llvm_readelf
        env['OBJCOPY'] = self.ctx.ndk.llvm_objcopy

        env['MAKE'] = 'make -j{}'.format(str(cpu_count()))

        # Android's arch/toolchain
        env['ARCH'] = self.arch
        env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))

        # Custom linker options
        env['LDSHARED'] = env['CC'] + ' ' + ' '.join(self.common_ldshared)

        # Host python (used by some recipes)
        hostpython_recipe = Recipe.get_recipe(
            'host' + self.ctx.python_recipe.name, self.ctx)
        env['BUILDLIB_PATH'] = join(
            hostpython_recipe.get_build_dir(self.arch),
            'native-build',
            'build',
            'lib.{}-{}'.format(
                build_platform,
                self.ctx.python_recipe.major_minor_version_string,
            ),
        )

        # for reproducible builds
        if 'SOURCE_DATE_EPOCH' in environ:
            for k in 'LC_ALL TZ SOURCE_DATE_EPOCH PYTHONHASHSEED BUILD_DATE BUILD_TIME'.split():
                if k in environ:
                    env[k] = environ[k]

        return env


class ArchARM(Arch):
    arch = "armeabi"
    command_prefix = 'arm-linux-androideabi'

    @property
    def target(self):
        target_data = self.command_prefix.split('-')
        return '{triplet}{ndk_api}'.format(
            triplet='-'.join(['armv7a', target_data[1], target_data[2]]),
            ndk_api=self.ctx.ndk_api,
        )


class ArchARMv7_a(ArchARM):
    arch = 'armeabi-v7a'
    arch_cflags = [
        '-march=armv7-a',
        '-mfloat-abi=softfp',
        '-mfpu=vfp',
        '-mthumb',
        '-fPIC',
    ]


class Archx86(Arch):
    arch = 'x86'
    command_prefix = 'i686-linux-android'
    arch_cflags = [
        '-march=i686',
        '-mssse3',
        '-mfpmath=sse',
        '-m32',
        '-fPIC',
    ]


class Archx86_64(Arch):
    arch = 'x86_64'
    command_prefix = 'x86_64-linux-android'
    arch_cflags = [
        '-march=x86-64',
        '-msse4.2',
        '-mpopcnt',
        '-m64',
        '-fPIC',
    ]


class ArchAarch_64(Arch):
    arch = 'arm64-v8a'
    command_prefix = 'aarch64-linux-android'
    arch_cflags = [
        '-march=armv8-a',
        '-fPIC'
        # '-I' + join(dirname(__file__), 'includes', 'arm64-v8a'),
    ]

    # Note: This `EXTRA_CFLAGS` below should target the commented `include`
    # above in `arch_cflags`. The original lines were added during the Sdl2's
    # bootstrap creation, and modified/commented during the migration to the
    # NDK r19 build system, because it seems that we don't need it anymore,
    # do we need them?
    # def get_env(self, with_flags_in_cc=True):
    #     env = super().get_env(with_flags_in_cc)
    #     env['EXTRA_CFLAGS'] = self.arch_cflags[-1]
    #     return env