from typing import List

from poetry.mixology import Resolver
from poetry.mixology.dependency_graph import DependencyGraph
from poetry.mixology.exceptions import ResolverError

from poetry.semver.version_parser import VersionParser

from .exceptions import SolverProblemError
from .operations import Install
from .operations import Uninstall
from .operations import Update
from .operations.operation import Operation

from .provider import Provider
from .ui import UI


class Solver:

    def __init__(self, package, pool, installed, locked, io):
        self._package = package
        self._pool = pool
        self._installed = installed
        self._locked = locked
        self._io = io

    def solve(self, requested, fixed=None):  # type: (...) -> List[Operation]
        resolver = Resolver(
            Provider(self._package, self._pool, self._io),
            UI(self._io)
        )

        base = None
        if fixed is not None:
            base = DependencyGraph()
            for fixed_req in fixed:
                base.add_vertex(fixed_req.name, fixed_req, True)

        try:
            graph = resolver.resolve(requested, base=base)
        except ResolverError as e:
            raise SolverProblemError(e)

        packages = [v.payload for v in graph.vertices.values()]

        # Setting info
        for vertex in graph.vertices.values():
            tags = self._get_tags_for_vertex(vertex, requested)
            if 'main' in tags['category']:
                vertex.payload.category = 'main'
            else:
                vertex.payload.category = 'dev'

            if not tags['optional']:
                vertex.payload.optional = False
            else:
                vertex.payload.optional = True

            # Finding the less restrictive requirements
            requirements = {}
            parser = VersionParser()
            for req_name, reqs in tags['requirements'].items():
                for req in reqs:
                    if req_name == 'python':
                        if 'python' not in requirements:
                            requirements['python'] = req
                            continue

                        previous = parser.parse_constraints(requirements['python'])
                        current = parser.parse_constraints(req)

                        if current.matches(previous):
                            requirements['python'] = req

                    if req_name == 'platform':
                        if 'platform' not in requirements:
                            requirements['platform'] = req
                            continue

            vertex.payload.requirements = requirements

        operations = []
        for package in packages:
            installed = False
            for pkg in self._installed.packages:
                if package.name == pkg.name:
                    installed = True
                    # Checking version
                    if package.version != pkg.version:
                        operations.append(Update(pkg, package))
                    else:
                        operations.append(
                            Install(package).skip('Already installed')
                        )

                    break

            if not installed:
                operations.append(Install(package))

        # Checking for removals
        for pkg in self._locked.packages:
            remove = True
            for package in packages:
                if pkg.name == package.name:
                    remove = False
                    break

            if remove:
                skip = True
                for installed in self._installed.packages:
                    if installed.name == pkg.name:
                        skip = False
                        break

                op = Uninstall(pkg)
                if skip:
                    op.skip('Not currently installed')

                operations.append(op)

        requested_names = [r.name for r in requested]

        return sorted(
            operations,
            key=lambda o: (
                1 if not o.package.name not in requested_names else 0,
                o.package.name
            )
        )

    def _get_tags_for_vertex(self, vertex, requested):
        tags = {
            'category': [],
            'optional': True,
            'requirements': {
                'python': [],
                'platform': []
            }
        }

        if not vertex.incoming_edges:
            # Original dependency
            for req in requested:
                if req.name == vertex.name:
                    tags['category'].append(req.category)
                    if not req.is_optional():
                        tags['optional'] = False

                    if req.python_versions != '*':
                        tags['requirements']['python'].append(str(req.python_constraint))

                    if req.platform != '*':
                        tags['requirements']['platform'].append(str(req.platform_constraint))

                    break
        else:
            for edge in vertex.incoming_edges:
                for req in edge.origin.payload.requires:
                    if req.name == vertex.payload.name:
                        if req.python_versions != '*':
                            tags['requirements']['python'].append(req.python_versions)

                        if req.platform != '*':
                            tags['requirements']['platform'].append(req.platform)

                sub_tags = self._get_tags_for_vertex(edge.origin, requested)

                tags['category'] += sub_tags['category']
                tags['optional'] = tags['optional'] and sub_tags['optional']
                requirements = sub_tags['requirements']
                tags['requirements']['python'] += requirements.get('python', [])
                tags['requirements']['platform'] += requirements.get('platform', [])

        return tags
