import os.path

from c_common import fsutil
from c_common.clsutil import classonly
import c_common.misc as _misc
from c_parser.info import (
    KIND,
    HighlevelParsedItem,
    Declaration,
    TypeDeclaration,
)
from c_parser.match import (
    is_type_decl,
)


IGNORED = _misc.Labeled('IGNORED')
UNKNOWN = _misc.Labeled('UNKNOWN')


class SystemType(TypeDeclaration):

    def __init__(self, name):
        super().__init__(None, name, None, None, _shortkey=name)


class Analyzed:
    _locked = False

    @classonly
    def is_target(cls, raw):
        if isinstance(raw, HighlevelParsedItem):
            return True
        else:
            return False

    @classonly
    def from_raw(cls, raw, **extra):
        if isinstance(raw, cls):
            if extra:
                # XXX ?
                raise NotImplementedError((raw, extra))
                #return cls(raw.item, raw.typedecl, **raw._extra, **extra)
            else:
                return info
        elif cls.is_target(raw):
            return cls(raw, **extra)
        else:
            raise NotImplementedError((raw, extra))

    @classonly
    def from_resolved(cls, item, resolved, **extra):
        if isinstance(resolved, TypeDeclaration):
            return cls(item, typedecl=resolved, **extra)
        else:
            typedeps, extra = cls._parse_raw_resolved(item, resolved, extra)
            if item.kind is KIND.ENUM:
                if typedeps:
                    raise NotImplementedError((item, resolved, extra))
            elif not typedeps:
                raise NotImplementedError((item, resolved, extra))
            return cls(item, typedeps, **extra or {})

    @classonly
    def _parse_raw_resolved(cls, item, resolved, extra_extra):
        if resolved in (UNKNOWN, IGNORED):
            return resolved, None
        try:
            typedeps, extra = resolved
        except (TypeError, ValueError):
            typedeps = extra = None
        if extra:
            # The resolved data takes precedence.
            extra = dict(extra_extra, **extra)
        if isinstance(typedeps, TypeDeclaration):
            return typedeps, extra
        elif typedeps in (None, UNKNOWN):
            # It is still effectively unresolved.
            return UNKNOWN, extra
        elif None in typedeps or UNKNOWN in typedeps:
            # It is still effectively unresolved.
            return typedeps, extra
        elif any(not isinstance(td, TypeDeclaration) for td in typedeps):
            raise NotImplementedError((item, typedeps, extra))
        return typedeps, extra

    def __init__(self, item, typedecl=None, **extra):
        assert item is not None
        self.item = item
        if typedecl in (UNKNOWN, IGNORED):
            pass
        elif item.kind is KIND.STRUCT or item.kind is KIND.UNION:
            if isinstance(typedecl, TypeDeclaration):
                raise NotImplementedError(item, typedecl)
            elif typedecl is None:
                typedecl = UNKNOWN
            else:
                typedecl = [UNKNOWN if d is None else d for d in typedecl]
        elif typedecl is None:
            typedecl = UNKNOWN
        elif typedecl and not isinstance(typedecl, TypeDeclaration):
            # All the other decls have a single type decl.
            typedecl, = typedecl
            if typedecl is None:
                typedecl = UNKNOWN
        self.typedecl = typedecl
        self._extra = extra
        self._locked = True

        self._validate()

    def _validate(self):
        item = self.item
        extra = self._extra
        # Check item.
        if not isinstance(item, HighlevelParsedItem):
            raise ValueError(f'"item" must be a high-level parsed item, got {item!r}')
        # Check extra.
        for key, value in extra.items():
            if key.startswith('_'):
                raise ValueError(f'extra items starting with {"_"!r} not allowed, got {extra!r}')
            if hasattr(item, key) and not callable(getattr(item, key)):
                raise ValueError(f'extra cannot override item, got {value!r} for key {key!r}')

    def __repr__(self):
        kwargs = [
            f'item={self.item!r}',
            f'typedecl={self.typedecl!r}',
            *(f'{k}={v!r}' for k, v in self._extra.items())
        ]
        return f'{type(self).__name__}({", ".join(kwargs)})'

    def __str__(self):
        try:
            return self._str
        except AttributeError:
            self._str, = self.render('line')
            return self._str

    def __hash__(self):
        return hash(self.item)

    def __eq__(self, other):
        if isinstance(other, Analyzed):
            return self.item == other.item
        elif isinstance(other, HighlevelParsedItem):
            return self.item == other
        elif type(other) is tuple:
            return self.item == other
        else:
            return NotImplemented

    def __gt__(self, other):
        if isinstance(other, Analyzed):
            return self.item > other.item
        elif isinstance(other, HighlevelParsedItem):
            return self.item > other
        elif type(other) is tuple:
            return self.item > other
        else:
            return NotImplemented

    def __dir__(self):
        names = set(super().__dir__())
        names.update(self._extra)
        names.remove('_locked')
        return sorted(names)

    def __getattr__(self, name):
        if name.startswith('_'):
            raise AttributeError(name)
        # The item takes precedence over the extra data (except if callable).
        try:
            value = getattr(self.item, name)
            if callable(value):
                raise AttributeError(name)
        except AttributeError:
            try:
                value = self._extra[name]
            except KeyError:
                pass
            else:
                # Speed things up the next time.
                self.__dict__[name] = value
                return value
            raise  # re-raise
        else:
            return value

    def __setattr__(self, name, value):
        if self._locked and name != '_str':
            raise AttributeError(f'readonly ({name})')
        super().__setattr__(name, value)

    def __delattr__(self, name):
        if self._locked:
            raise AttributeError(f'readonly ({name})')
        super().__delattr__(name)

    @property
    def decl(self):
        if not isinstance(self.item, Declaration):
            raise AttributeError('decl')
        return self.item

    @property
    def signature(self):
        # XXX vartype...
        ...

    @property
    def istype(self):
        return is_type_decl(self.item.kind)

    @property
    def is_known(self):
        if self.typedecl in (UNKNOWN, IGNORED):
            return False
        elif isinstance(self.typedecl, TypeDeclaration):
            return True
        else:
            return UNKNOWN not in self.typedecl

    def fix_filename(self, relroot=fsutil.USE_CWD, **kwargs):
        self.item.fix_filename(relroot, **kwargs)
        return self

    def as_rowdata(self, columns=None):
        # XXX finish!
        return self.item.as_rowdata(columns)

    def render_rowdata(self, columns=None):
        # XXX finish!
        return self.item.render_rowdata(columns)

    def render(self, fmt='line', *, itemonly=False):
        if fmt == 'raw':
            yield repr(self)
            return
        rendered = self.item.render(fmt)
        if itemonly or not self._extra:
            yield from rendered
            return
        extra = self._render_extra(fmt)
        if not extra:
            yield from rendered
        elif fmt in ('brief', 'line'):
            rendered, = rendered
            extra, = extra
            yield f'{rendered}\t{extra}'
        elif fmt == 'summary':
            raise NotImplementedError(fmt)
        elif fmt == 'full':
            yield from rendered
            for line in extra:
                yield f'\t{line}'
        else:
            raise NotImplementedError(fmt)

    def _render_extra(self, fmt):
        if fmt in ('brief', 'line'):
            yield str(self._extra)
        else:
            raise NotImplementedError(fmt)


class Analysis:

    _item_class = Analyzed

    @classonly
    def build_item(cls, info, resolved=None, **extra):
        if resolved is None:
            return cls._item_class.from_raw(info, **extra)
        else:
            return cls._item_class.from_resolved(info, resolved, **extra)

    @classmethod
    def from_results(cls, results):
        self = cls()
        for info, resolved in results:
            self._add_result(info, resolved)
        return self

    def __init__(self, items=None):
        self._analyzed = {type(self).build_item(item): None
                          for item in items or ()}

    def __repr__(self):
        return f'{type(self).__name__}({list(self._analyzed.keys())})'

    def __iter__(self):
        #yield from self.types
        #yield from self.functions
        #yield from self.variables
        yield from self._analyzed

    def __len__(self):
        return len(self._analyzed)

    def __getitem__(self, key):
        if type(key) is int:
            for i, val in enumerate(self._analyzed):
                if i == key:
                    return val
            else:
                raise IndexError(key)
        else:
            return self._analyzed[key]

    def fix_filenames(self, relroot=fsutil.USE_CWD, **kwargs):
        if relroot and relroot is not fsutil.USE_CWD:
            relroot = os.path.abspath(relroot)
        for item in self._analyzed:
            item.fix_filename(relroot, fixroot=False, **kwargs)

    def _add_result(self, info, resolved):
        analyzed = type(self).build_item(info, resolved)
        self._analyzed[analyzed] = None
        return analyzed
