import glob
import os
from typing import Optional
from functools import lru_cache

import yaml
import platform
import helpers


class Scheme:
    def __init__(self):
        self.type = None
        self.name = None
        self.implementations = []

    def path(self, base='..'):
        return os.path.join(base, 'crypto_' + self.type, self.name)

    def namespace_prefix(self):
        return 'PQCLEAN_{}_'.format(self.name.upper()).replace('-', '')

    @staticmethod
    @lru_cache(maxsize=None)
    def by_name(scheme_name):
        for scheme in Scheme.all_schemes():
            if scheme.name == scheme_name:
                return scheme
        raise KeyError()

    @staticmethod
    @lru_cache(maxsize=1)
    def all_schemes():
        schemes = []
        schemes.extend(Scheme.all_schemes_of_type('kem'))
        schemes.extend(Scheme.all_schemes_of_type('sign'))
        return schemes

    @staticmethod
    @lru_cache(maxsize=1)
    def all_implementations():
        implementations = []
        for scheme in Scheme.all_schemes():
            implementations.extend(scheme.implementations)
        return implementations

    @staticmethod
    @lru_cache(maxsize=1)
    def all_supported_implementations():
        return [impl for impl in Scheme.all_implementations()
                if impl.supported_on_current_platform()]

    @staticmethod
    @lru_cache(maxsize=32)
    def all_schemes_of_type(type: str) -> list:
        schemes = []
        p = os.path.join('..', 'crypto_' + type)
        if os.path.isdir(p):
            for d in os.listdir(p):
                if os.path.isdir(os.path.join(p, d)):
                    if type == 'kem':
                        schemes.append(KEM(d))
                    elif type == 'sign':
                        schemes.append(Signature(d))
                    else:
                        assert('Unknown type')
        return schemes

    @lru_cache(maxsize=None)
    def metadata(self):
        metafile = os.path.join(self.path(), 'META.yml')
        try:
            with open(metafile, encoding='utf-8') as f:
                metadata = yaml.safe_load(f)
            return metadata
        except Exception as e:
            print("Can't open {}: {}".format(metafile, e))
            return None

    def __repr__(self):
        return "<{}({})>".format(self.type.title(), self.name)


class Implementation:

    def __init__(self, scheme, name):
        self.scheme = scheme
        self.name = name

    @lru_cache(maxsize=None)
    def metadata(self):
        for i in self.scheme.metadata()['implementations']:
            if i['name'] == self.name:
                return i

    def path(self, base='..') -> str:
        return os.path.join(self.scheme.path(base=base), self.name)

    def libname(self) -> str:
        if os.name == 'nt':
            return "lib{}_{}.lib".format(self.scheme.name, self.name)
        return "lib{}_{}.a".format(self.scheme.name, self.name)

    def cfiles(self) -> [str]:
        return glob.glob(os.path.join(self.path(), '*.c'))

    def hfiles(self) -> [str]:
        return glob.glob(os.path.join(self.path(), '*.h'))

    def ofiles(self) -> [str]:
        return glob.glob(os.path.join(self.path(),
                         '*.o' if os.name != 'nt' else '*.obj'))

    @staticmethod
    @lru_cache(maxsize=None)
    def by_name(scheme_name, implementation_name):
        scheme = Scheme.by_name(scheme_name)
        for implementation in scheme.implementations:
            if implementation.name == implementation_name:
                return implementation
        raise KeyError()

    @staticmethod
    @lru_cache(maxsize=None)
    def all_implementations(scheme: Scheme) -> list:
        implementations = []
        for d in os.listdir(scheme.path()):
            if os.path.isdir(os.path.join(scheme.path(), d)):
                implementations.append(Implementation(scheme, d))
        return implementations

    @staticmethod
    def all_supported_implementations(scheme: Scheme) -> list:
        return [impl for impl in Implementation.all_implementations(scheme)
                if impl.supported_on_current_platform()]

    def namespace_prefix(self):
        return '{}{}_'.format(self.scheme.namespace_prefix(),
                              self.name.upper()).replace('-', '')

    def supported_on_os(self, os: Optional[str] = None) -> bool:
        """Check if we support the OS

        If no OS is specified, then we run on the current OS
        """
        if os is None:
            os = platform.system()

        for platform_ in self.metadata().get('supported_platforms', []):
            if 'operating_systems' in platform_:
                if os not in platform_['operating_systems']:
                    return False

        return True

    @lru_cache(maxsize=10000)
    def supported_on_current_platform(self) -> bool:
        if 'supported_platforms' not in self.metadata():
            return True

        if platform.machine() == 'ppc':
            return False

        if not self.supported_on_os():
            return False

        cpuinfo = helpers.get_cpu_info()

        for platform_ in self.metadata()['supported_platforms']:
            if platform_['architecture'] == cpuinfo['arch'].lower():
                # Detect actually running on emulated i386
                if (platform_['architecture'] == 'x86_64' and
                        platform.architecture()[0] == '32bit'):
                    continue
                if all([flag in cpuinfo['flags']
                        for flag in platform_['required_flags']]):
                    return True
        return False

    def __str__(self):
        return "{} implementation of {}".format(self.name, self.scheme.name)

    def __repr__(self):
        return "<Implementation({}, {})>".format(self.scheme.name, self.name)


class KEM(Scheme):

    def __init__(self, name: str):
        self.type = 'kem'
        self.name = name
        self.implementations = Implementation.all_implementations(self)

    @staticmethod
    def all_kems() -> list:
        return Scheme.all_schemes_of_type('kem')


class Signature(Scheme):

    def __init__(self, name: str):
        self.type = 'sign'
        self.name = name
        self.implementations = Implementation.all_implementations(self)

    @staticmethod
    def all_sigs():
        return Scheme.all_schemes_of_type('sign')
