from collections import defaultdict
from datetime import datetime
from itertools import chain
import os.path
import sys

from glad.lang.common.loader import NullLoader
from glad.opener import URLOpener
from glad.util import api_name, Version
import glad


if sys.version_info >= (3, 0):
    from urllib.parse import urlencode
else:
    from urllib import urlencode


HEADER_TEMPLATE = '''
    {apis_named} loader generated by glad {version} on {date}.

    Language/Generator: {language}
    Specification: {specification}
    APIs: {apis}
    Profile: {profile}
    Extensions:
        {extensions}
    Loader: {loader}
    Local files: {local_files}
    Omit khrplatform: {omit_khrplatform}
    Reproducible: {reproducible}

    Commandline:
        {commandline}
    Online:
        {online}
'''


class Generator(object):
    NAME = None
    NAME_LONG = None
    URL = 'https://glad.dav1d.de'

    def __init__(self, path, spec, api, extension_names=None, loader=None,
                 opener=None, local_files=False, omit_khrplatform=False,
                 header_template=HEADER_TEMPLATE, **options):
        self.path = os.path.abspath(path)

        self.spec = spec
        for a in api:
            if a not in self.spec.features:
                raise ValueError(
                    'Unknown API "{0}" for specification "{1}", the following APIs are available "{2}".'
                    .format(a, self.spec.NAME, ', '.join(self.spec.features.keys()))
                )
        self.api = api
        self.extension_names = extension_names

        self.has_loader = not loader.disabled
        self.loader = loader
        if self.loader is None:
            self.loader = NullLoader

        self.opener = opener
        if self.opener is None:
            self.opener = URLOpener.default()

        self.local_files = local_files
        self.omit_khrplatform = omit_khrplatform

        self.options = options

        self._header_template = header_template

    def open(self):
        raise NotImplementedError

    def close(self):
        raise NotImplementedError

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def generate(self):
        features = list()
        for api, version in self.api.items():
            features.extend(self.spec.features[api])

            if version is None:
                version = list(self.spec.features[api].keys())[-1]
                self.api[api] = Version(*version)

            if version not in self.spec.features[api]:
                raise ValueError(
                    'Unknown version "{0}" for specification "{1}"'
                    .format(version, self.spec.NAME)
                )

        if self.extension_names is None:
           self.extension_names = list(chain.from_iterable(self.spec.extensions[a]
                                                           for a in self.api))

        # sort and eliminate duplicates
        self.extension_names = list(sorted(set(self.extension_names)))

        e = list(chain.from_iterable(self.spec.extensions[a] for a in self.api))
        for ext in self.extension_names:
            if ext not in e:
                raise ValueError(
                    'Invalid extension "{0}" for specification "{1}"'
                    .format(ext, self.spec.NAME)
                )

        self.generate_header()

        types = [t for t in self.spec.types if t.api is None or t.api in self.api]
        self.generate_types(types)

        f = list()
        for api, version in self.api.items():
            f.extend([value for key, value in self.spec.features[api].items()
                        if key <= version])
        enums, functions = merge(f)
        self.generate_features(f)

        extensions = list()
        for api in self.api:
            extensions.extend(self.spec.extensions[api][ext]
                              for ext in self.extension_names if ext
                              in self.spec.extensions[api])
        self.generate_extensions(extensions, enums, functions)

        fs = defaultdict(list)
        es = defaultdict(list)
        for api, version in self.api.items():
            fs[api].extend(
                [value for key, value in
                 self.spec.features[api].items() if key <= version]
            )
            es[api].extend(self.spec.extensions[api][ext]
                           for ext in self.extension_names if ext
                           in self.spec.extensions[api])
        self.generate_loader(fs, es)

    @property
    def header(self):
        apis_named = ', '.join(sorted(set(api_name(api) for api in self.api)))
        date = datetime.now().strftime('%c')
        language = self.NAME_LONG
        specification = self.spec.NAME
        apis = ', '.join('{}={}'.format(api, '.'.join(map(str, version))) for api, version in self.api.items())
        profile = getattr(self.spec, 'profile', '-')
        extensions = ',\n        '.join(sorted(self.extension_names))
        online = self.online
        if len(online) > 2000:
            online = 'Too many extensions'

        if self.options.get('reproducible', False):
            date = '-'

        return self._header_template.format(
            apis_named=apis_named,
            version=glad.__version__,
            date=date,
            language=language,
            specification=specification,
            apis=apis,
            profile=profile,
            extensions=extensions,
            loader=self.has_loader,
            local_files=self.local_files,
            omit_khrplatform=self.omit_khrplatform,
            reproducible=self.options.get('reproducible', False),
            commandline=self.commandline,
            online=online
        )

    @property
    def commandline(self):
        profile = getattr(self.spec, 'profile', None)
        if profile is not None:
            profile = '--profile="{}"'.format(profile)

        api = '--api="{}"'.format(','.join(
            '{}={}'.format(api, '.'.join(map(str, version))) for api, version in self.api.items())
        )
        generator = '--generator="{}"'.format(self.NAME)
        specification = '--spec="{}"'.format(self.spec.NAME)
        loader = '' if self.has_loader else '--no-loader'
        extensions = '--extensions="{}"'.format(','.join(sorted(self.extension_names)))
        local_files = '--local-files' if self.local_files else ''
        omit_khrplatform = '--omit-khrplatform' if self.omit_khrplatform else ''
        reproducible = '--reproducible' if self.options.get('reproducible', False) else ''

        return ' '.join(filter(None, [
            profile, api, generator, specification,
            loader, local_files, omit_khrplatform, extensions
        ]))

    @property
    def online(self):
        profile = getattr(self.spec, 'profile', None)
        if profile is not None:
            profile = ('profile', profile)

        api = [('api', s) for s in ('{}={}'.format(api, '.'.join(map(str, version))) for api, version in self.api.items())]
        generator = ('language', self.NAME)
        specification = ('specification', self.spec.NAME)
        loader = ('loader', 'on') if self.has_loader else None
        extensions = [('extensions', ext) for ext in self.extension_names]

        data = [profile, generator, specification, loader]
        data.extend(api)
        data.extend(extensions)
        data = list(filter(None, data))
        serialized = urlencode(data)

        # TODO: --local-files, --omit-khrplatform
        return '{}/#{}'.format(self.URL, serialized)

    def generate_header(self):
        raise NotImplementedError

    def generate_loader(self, features, extensions):
        raise NotImplementedError

    def generate_types(self, types):
        raise NotImplementedError

    def generate_features(self, features):
        raise NotImplementedError

    def generate_extensions(self, extensions, enums, functions):
        raise NotImplementedError


def merge(features):
    enums = set()
    functions = set()

    for feature in features:
        enums |= set(feature.enums)
        functions |= set(feature.functions)

    return enums, functions
