# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os
import re
from collections.abc import Iterable

import six


class Makefile(object):
    """Provides an interface for writing simple makefiles

    Instances of this class are created, populated with rules, then
    written.
    """

    def __init__(self):
        self._statements = []

    def create_rule(self, targets=()):
        """
        Create a new rule in the makefile for the given targets.
        Returns the corresponding Rule instance.
        """
        targets = list(targets)
        for target in targets:
            assert isinstance(target, six.text_type)
        rule = Rule(targets)
        self._statements.append(rule)
        return rule

    def add_statement(self, statement):
        """
        Add a raw statement in the makefile. Meant to be used for
        simple variable assignments.
        """
        assert isinstance(statement, six.text_type)
        self._statements.append(statement)

    def dump(self, fh, removal_guard=True):
        """
        Dump all the rules to the given file handle. Optionally (and by
        default), add guard rules for file removals (empty rules for other
        rules' dependencies)
        """
        all_deps = set()
        all_targets = set()
        for statement in self._statements:
            if isinstance(statement, Rule):
                statement.dump(fh)
                all_deps.update(statement.dependencies())
                all_targets.update(statement.targets())
            else:
                fh.write("%s\n" % statement)
        if removal_guard:
            guard = Rule(sorted(all_deps - all_targets))
            guard.dump(fh)


class _SimpleOrderedSet(object):
    """
    Simple ordered set, specialized for used in Rule below only.
    It doesn't expose a complete API, and normalizes path separators
    at insertion.
    """

    def __init__(self):
        self._list = []
        self._set = set()

    def __nonzero__(self):
        return bool(self._set)

    def __bool__(self):
        return bool(self._set)

    def __iter__(self):
        return iter(self._list)

    def __contains__(self, key):
        return key in self._set

    def update(self, iterable):
        def _add(iterable):
            emitted = set()
            for i in iterable:
                i = i.replace(os.sep, "/")
                if i not in self._set and i not in emitted:
                    yield i
                    emitted.add(i)

        added = list(_add(iterable))
        self._set.update(added)
        self._list.extend(added)


class Rule(object):
    """Class handling simple rules in the form:
    target1 target2 ... : dep1 dep2 ...
    command1 command2 ...
    """

    def __init__(self, targets=()):
        self._targets = _SimpleOrderedSet()
        self._dependencies = _SimpleOrderedSet()
        self._commands = []
        self.add_targets(targets)

    def add_targets(self, targets):
        """Add additional targets to the rule."""
        assert isinstance(targets, Iterable) and not isinstance(
            targets, six.string_types
        )
        targets = list(targets)
        for target in targets:
            assert isinstance(target, six.text_type)
        self._targets.update(targets)
        return self

    def add_dependencies(self, deps):
        """Add dependencies to the rule."""
        assert isinstance(deps, Iterable) and not isinstance(deps, six.string_types)
        deps = list(deps)
        for dep in deps:
            assert isinstance(dep, six.text_type)
        self._dependencies.update(deps)
        return self

    def add_commands(self, commands):
        """Add commands to the rule."""
        assert isinstance(commands, Iterable) and not isinstance(
            commands, six.string_types
        )
        commands = list(commands)
        for command in commands:
            assert isinstance(command, six.text_type)
        self._commands.extend(commands)
        return self

    def targets(self):
        """Return an iterator on the rule targets."""
        # Ensure the returned iterator is actually just that, an iterator.
        # Avoids caller fiddling with the set itself.
        return iter(self._targets)

    def dependencies(self):
        """Return an iterator on the rule dependencies."""
        return iter(d for d in self._dependencies if d not in self._targets)

    def commands(self):
        """Return an iterator on the rule commands."""
        return iter(self._commands)

    def dump(self, fh):
        """
        Dump the rule to the given file handle.
        """
        if not self._targets:
            return
        fh.write("%s:" % " ".join(self._targets))
        if self._dependencies:
            fh.write(" %s" % " ".join(self.dependencies()))
        fh.write("\n")
        for cmd in self._commands:
            fh.write("\t%s\n" % cmd)


# colon followed by anything except a slash (Windows path detection)
_depfilesplitter = re.compile(r":(?![\\/])")


def read_dep_makefile(fh):
    """
    Read the file handler containing a dep makefile (simple makefile only
    containing dependencies) and returns an iterator of the corresponding Rules
    it contains. Ignores removal guard rules.
    """

    rule = ""
    for line in fh.readlines():
        line = six.ensure_text(line)
        assert not line.startswith("\t")
        line = line.strip()
        if line.endswith("\\"):
            rule += line[:-1]
        else:
            rule += line
            split_rule = _depfilesplitter.split(rule, 1)
            if len(split_rule) > 1 and split_rule[1].strip():
                yield Rule(split_rule[0].strip().split()).add_dependencies(
                    split_rule[1].strip().split()
                )
            rule = ""

    if rule:
        raise Exception("Makefile finishes with a backslash. Expected more input.")


def write_dep_makefile(fh, target, deps):
    """
    Write a Makefile containing only target's dependencies to the file handle
    specified.
    """
    mk = Makefile()
    rule = mk.create_rule(targets=[target])
    rule.add_dependencies(deps)
    mk.dump(fh, removal_guard=True)
