# -*- coding=utf-8 -*-
from __future__ import absolute_import, print_function

import copy
import platform

from collections import defaultdict

import attr

from packaging.version import Version, LegacyVersion
from packaging.version import parse as parse_version

from ..environment import SYSTEM_ARCH
from ..utils import (
    _filter_none, ensure_path, get_python_version, optional_instance_of
)


@attr.s
class PythonVersion(object):
    major = attr.ib(default=0)
    minor = attr.ib(default=None)
    patch = attr.ib(default=0)
    is_prerelease = attr.ib(default=False)
    is_postrelease = attr.ib(default=False)
    is_devrelease = attr.ib(default=False)
    is_debug = attr.ib(default=False)
    version = attr.ib(default=None, validator=optional_instance_of(Version))
    architecture = attr.ib(default=None)
    comes_from = attr.ib(default=None)
    executable = attr.ib(default=None)

    @property
    def version_sort(self):
        """version_sort tuple for sorting against other instances of the same class.

        Returns a tuple of the python version but includes a point for non-dev,
        and a point for non-prerelease versions.  So released versions will have 2 points
        for this value.  E.g. `(3, 6, 6, 2)` is a release, `(3, 6, 6, 1)` is a prerelease,
        `(3, 6, 6, 0)` is a dev release, and `(3, 6, 6, 3)` is a postrelease.
        """
        release_sort = 2
        if self.is_postrelease:
            release_sort = 3
        elif self.is_prerelease:
            release_sort = 1
        elif self.is_devrelease:
            release_sort = 0
        elif self.is_debug:
            release_sort = 1
        return (self.major, self.minor, self.patch if self.patch else 0, release_sort)

    @property
    def version_tuple(self):
        """Provides a version tuple for using as a dictionary key.

        :return: A tuple describing the python version meetadata contained.
        :rtype: tuple
        """

        return (
            self.major,
            self.minor,
            self.patch,
            self.is_prerelease,
            self.is_devrelease,
            self.is_debug
        )

    def matches(
        self, major=None, minor=None, patch=None, pre=False, dev=False, arch=None, debug=False
    ):
        if arch and arch.isdigit():
            arch = "{0}bit".format(arch)
        return (
            (major is None or self.major == major)
            and (minor is None or self.minor == minor)
            and (patch is None or self.patch == patch)
            and (pre is None or self.is_prerelease == pre)
            and (dev is None or self.is_devrelease == dev)
            and (arch is None or self.architecture == arch)
            and (debug is None or self.is_debug == debug)
        )

    def as_major(self):
        self_dict = attr.asdict(self, recurse=False, filter=_filter_none).copy()
        self_dict.update({"minor": None, "patch": None})
        return self.create(**self_dict)

    def as_minor(self):
        self_dict = attr.asdict(self, recurse=False, filter=_filter_none).copy()
        self_dict.update({"patch": None})
        return self.create(**self_dict)

    @classmethod
    def parse(cls, version):
        """Parse a valid version string into a dictionary

        Raises:
            ValueError -- Unable to parse version string
            ValueError -- Not a valid python version

        :param version: A valid version string
        :type version: str
        :return: A dictionary with metadata about the specified python version.
        :rtype: dict.
        """

        is_debug = False
        if version.endswith("-debug"):
            is_debug = True
            version, _, _ = version.rpartition("-")
        try:
            version = parse_version(str(version))
        except TypeError:
            raise ValueError("Unable to parse version: %s" % version)
        if not version or not version.release:
            raise ValueError("Not a valid python version: %r" % version)
            return
        if len(version.release) >= 3:
            major, minor, patch = version.release[:3]
        elif len(version.release) == 2:
            major, minor = version.release
            patch = None
        else:
            major = version.release[0]
            minor = None
            patch = None
        return {
            "major": major,
            "minor": minor,
            "patch": patch,
            "is_prerelease": version.is_prerelease,
            "is_postrelease": version.is_postrelease,
            "is_devrelease": version.is_devrelease,
            "is_debug": is_debug,
            "version": version,
        }

    @classmethod
    def from_path(cls, path):
        """Parses a python version from a system path.

        Raises:
            ValueError -- Not a valid python path

        :param path: A string or :class:`~pythonfinder.models.path.PathEntry`
        :type path: str or :class:`~pythonfinder.models.path.PathEntry` instance
        :param launcher_entry: A python launcher environment object.
        :return: An instance of a PythonVersion.
        :rtype: :class:`~pythonfinder.models.python.PythonVersion`
        """

        from .path import PathEntry

        if not isinstance(path, PathEntry):
            path = PathEntry.create(path, is_root=False, only_python=True)
        if not path.is_python:
            raise ValueError("Not a valid python path: %s" % path.path)
            return
        py_version = get_python_version(str(path.path))
        instance_dict = cls.parse(py_version)
        if not isinstance(instance_dict.get("version"), Version):
            raise ValueError("Not a valid python path: %s" % path.path)
            return
        architecture, _ = platform.architecture(path.path.as_posix())
        instance_dict.update({"comes_from": path, "architecture": architecture})
        return cls(**instance_dict)

    @classmethod
    def from_windows_launcher(cls, launcher_entry):
        """Create a new PythonVersion instance from a Windows Launcher Entry

        :param launcher_entry: A python launcher environment object.
        :return: An instance of a PythonVersion.
        :rtype: :class:`~pythonfinder.models.python.PythonVersion`
        """

        from .path import PathEntry

        creation_dict = cls.parse(launcher_entry.info.version)
        base_path = ensure_path(launcher_entry.info.install_path.__getattr__(""))
        default_path = base_path / "python.exe"
        if not default_path.exists():
            default_path = base_path / "Scripts" / "python.exe"
        exe_path = ensure_path(
            getattr(launcher_entry.info.install_path, "executable_path", default_path)
        )
        creation_dict.update(
            {
                "architecture": getattr(
                    launcher_entry.info, "sys_architecture", SYSTEM_ARCH
                ),
                "executable": exe_path,
            }
        )
        py_version = cls.create(**creation_dict)
        comes_from = PathEntry.create(exe_path, only_python=True)
        comes_from.py_version = copy.deepcopy(py_version)
        py_version.comes_from = comes_from
        return py_version

    @classmethod
    def create(cls, **kwargs):
        if "architecture" in kwargs:
            if kwargs["architecture"].isdigit():
                kwargs["architecture"] = "{0}bit".format(kwargs["architecture"])
        return cls(**kwargs)


@attr.s
class VersionMap(object):
    versions = attr.ib(default=attr.Factory(defaultdict(list)))

    def add_entry(self, entry):
        version = entry.as_python
        if version:
            entries = self.versions[version.version_tuple]
            paths = {p.path for p in self.versions.get(version.version_tuple, [])}
            if entry.path not in paths:
                self.versions[version.version_tuple].append(entry)

    def merge(self, target):
        for version, entries in target.versions.items():
            if version not in self.versions:
                self.versions[version] = entries
            else:
                current_entries = {p.path for p in self.versions.get(version)}
                new_entries = {p.path for p in entries}
                new_entries -= current_entries
                self.versions[version].append(
                    [e for e in entries if e.path in new_entries]
                )