import re
from collections import namedtuple, OrderedDict
from enum import Enum
from six import iteritems, string_types
from six.moves import cStringIO as StringIO

from ... import path
from ... import safe_str
from ... import shell
from ... import iterutils
from ...platforms.host import platform_info
from ...tools.common import Command
from ...versioning import Version

__all__ = ['NinjaFile', 'Section', 'Syntax', 'Writer', 'Variable', 'var',
           'path_vars']

Rule = namedtuple('Rule', ['command', 'depfile', 'deps', 'description',
                           'generator', 'pool', 'restat'])
Build = namedtuple('Build', ['outputs', 'rule', 'inputs', 'implicit',
                             'order_only', 'variables'])

Syntax = Enum('Syntax', ['output', 'input', 'shell', 'clean'])
Section = Enum('Section', ['path', 'command', 'flags', 'other'])

_comment_tmpl = """
# Do not edit this file! It was automatically generated by bfg9000.
# Instead, you should edit the source file that created this:
# {}
""".strip()


class Writer(object):
    def __init__(self, stream, shell=shell):
        self.stream = stream
        self.shell = shell

    @staticmethod
    def escape_str(string, syntax):
        if '\n' in string:
            raise ValueError('illegal newline')

        if syntax == Syntax.output:
            return re.sub(r'([:$ ])', r'$\1', string)
        elif syntax == Syntax.input:
            return re.sub(r'([$ ])', r'$\1', string)
        elif syntax in [Syntax.shell, Syntax.clean]:
            return string.replace('$', '$$')

        raise ValueError(
            'unknown syntax {!r}'.format(syntax)
        )  # pragma: no cover

    def write_literal(self, string):
        self.stream.write(string)

    def write(self, thing, syntax, shell_quote=iterutils.default_sentinel):
        if shell_quote is iterutils.default_sentinel:
            shell_quote = self.shell.quote_info
        thing = safe_str.safe_str(thing)
        shelly = syntax == Syntax.shell
        escaped = False

        if isinstance(thing, safe_str.literal):
            escaped = True
            self.write_literal(thing.string)
        elif isinstance(thing, safe_str.shell_literal):
            escaped = True
            self.write_literal(self.escape_str(thing.string, syntax))
        elif isinstance(thing, string_types):
            if shelly and shell_quote:
                thing, escaped = shell_quote(thing)
            self.write_literal(self.escape_str(thing, syntax))
        elif isinstance(thing, safe_str.jbos):
            for i in thing.bits:
                escaped |= self.write(i, syntax, shell_quote)
        elif isinstance(thing, path.BasePath):
            out = Writer(StringIO())
            thing = thing.realize(path_vars, shelly)
            escaped = out.write(thing, syntax, self.shell.inner_quote_info)

            thing = out.stream.getvalue()
            if shelly and escaped:
                thing = self.shell.wrap_quotes(thing)
            self.write_literal(thing)
        else:
            raise TypeError(type(thing))

        return escaped

    def write_each(self, things, syntax, delim=safe_str.literal(' '),
                   prefix=None, suffix=None):
        for i in iterutils.tween(things, delim, prefix, suffix):
            self.write(i, syntax)

    def write_shell(self, thing, syntax=Syntax.shell, can_wrap=False):
        if ( can_wrap and isinstance(thing, shell.shell_list) and
             platform_info().family == 'windows' ):
            prefix = safe_str.shell_literal('cmd /s /c "')
            suffix = safe_str.shell_literal('"')
        else:
            prefix = suffix = None
        self.write_each(iterutils.iterate(thing), syntax, prefix=prefix,
                        suffix=suffix)


class Variable(safe_str.safe_string_ops):
    def __init__(self, name):
        self.name = re.sub(r'\W', '_', name)

    def use(self):
        return safe_str.literal('${{{}}}'.format(self.name))

    def _safe_str(self):
        return self.use()

    def __str__(self):
        raise NotImplementedError()

    def __repr__(self):
        return repr(self.use())

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

    def __eq__(self, rhs):
        return self.name == rhs.name

    def __ne__(self, rhs):
        return not (self == rhs)


def var(v):
    return v if isinstance(v, Variable) else Variable(v)


path_vars = {
    path.Root.srcdir  : Variable('srcdir'),
    path.Root.builddir: None,
}
path_vars.update({i: Variable(i.name) for i in path.InstallRoot})

# Only use destdir on platforms that actually support it (e.g. not Windows).
if platform_info().destdir:
    path_vars[path.DestDir.destdir] = Variable('DESTDIR')


class NinjaFile(object):
    Section = Section

    def __init__(self, bfgfile):
        self._bfgfile = bfgfile

        self._min_version = None
        self._var_table = set()
        self._variables = {i: [] for i in Section}

        self._rules = OrderedDict()

        self._builds = []
        self._build_outputs = set()
        self._defaults = []

    def min_version(self, version):
        version = Version(version)
        if self._min_version is None or version > self._min_version:
            self._min_version = version

    def variable(self, name, value, section=Section.other, exist_ok=False):
        name = var(name)
        if self.has_variable(name):
            if not exist_ok:
                raise ValueError('variable {!r} already exists'.format(name))
        else:
            self._var_table.add(name)
            value = self._convert_args(value)
            self._variables[section].append((name, value))
        return name

    def cmd_var(self, cmd):
        return self.variable(cmd.command_var, cmd.command, Section.command,
                             exist_ok=True)

    def has_variable(self, name):
        return var(name) in self._var_table

    def rule(self, name, command, depfile=None, deps=None, description=None,
             generator=False, pool=None, restat=False):
        command = self._convert_args(command)

        if pool is not None:
            if pool == 'console':
                self.min_version('1.5')
            else:
                raise ValueError('unknown pool {!r}'.format(pool))

        if re.search(r'\W', name):
            raise ValueError('rule name contains invalid characters')

        if self.has_rule(name):
            raise ValueError('rule {!r} already exists'.format(name))

        self._rules[name] = Rule(command, depfile, deps, description,
                                 generator, pool, restat)

    def has_rule(self, name):
        return name in self._rules

    @staticmethod
    def _output_str(name):
        out = Writer(StringIO())
        out.write(name, Syntax.output)
        return out.stream.getvalue()

    def build(self, output, rule, inputs=None, implicit=None, order_only=None,
              variables=None):
        if rule != 'phony' and not self.has_rule(rule):
            raise ValueError('unknown rule {!r}'.format(rule))

        variables = {var(k): self._convert_args(v) for k, v in
                     iteritems(variables or {})}

        outputs = iterutils.listify(output)
        for i in outputs:
            out = self._output_str(i)
            if self.has_build(out):
                raise ValueError('build for {!r} already exists'.format(out))
            self._build_outputs.add(out)
        self._builds.append(Build(
            outputs, rule, iterutils.listify(inputs),
            iterutils.listify(implicit), iterutils.listify(order_only),
            variables
        ))

    def has_build(self, name):
        return name in self._build_outputs

    def default(self, paths):
        self._defaults.extend(iterutils.iterate(paths))

    def _convert_args(self, args):
        if iterutils.isiterable(args):
            return Command.convert_args(args, self.cmd_var)
        return args

    def _write_variable(self, out, name, value, syntax=Syntax.shell, indent=0,
                        can_wrap=False):
        out.write_literal(('  ' * indent) + name.name + ' = ')
        out.write_shell(value, syntax, can_wrap)
        out.write_literal('\n')

    def _write_rule(self, out, name, rule):
        out.write_literal('rule ' + name + '\n')

        self._write_variable(out, var('command'), rule.command, indent=1,
                             can_wrap=True)
        if rule.depfile:
            self._write_variable(out, var('depfile'), rule.depfile, indent=1)
        if rule.deps:
            self._write_variable(out, var('deps'), rule.deps, indent=1)
        if rule.description:
            self._write_variable(out, var('description'), rule.description,
                                 indent=1, syntax=Syntax.clean)
        if rule.generator:
            self._write_variable(out, var('generator'), '1', indent=1)
        if rule.pool:
            self._write_variable(out, var('pool'), rule.pool, indent=1)
        if rule.restat:
            self._write_variable(out, var('restat'), '1', indent=1)

    def _write_build(self, out, build):
        out.write_literal('build ')
        out.write_each(build.outputs, Syntax.output)
        out.write_literal(': ' + build.rule)

        lit = safe_str.literal
        out.write_each(build.inputs, Syntax.input, prefix=lit(' '))
        out.write_each(build.implicit, Syntax.input, prefix=lit(' | '))
        out.write_each(build.order_only, Syntax.input, prefix=lit(' || '))
        out.write_literal('\n')

        if build.variables:
            desc_var = var('description')
            for k, v in iteritems(build.variables):
                syntax = Syntax.clean if k == desc_var else Syntax.shell
                self._write_variable(out, k, v, indent=1, syntax=syntax)

    def write(self, out):
        out = Writer(out)
        out.write_literal(_comment_tmpl.format(self._bfgfile) + '\n\n')

        if self._min_version:
            self._write_variable(
                out, var('ninja_required_version'), str(self._min_version)
            )
            out.write_literal('\n')

        for section in Section:
            # The built-in paths don't need shell quoting because they're used
            # by other paths, which *are* quoted.
            syntax = Syntax.clean if section == Section.path else Syntax.shell
            for name, value in self._variables[section]:
                self._write_variable(out, name, value, syntax)
            if self._variables[section]:
                out.write_literal('\n')

        for name, rule in iteritems(self._rules):
            self._write_rule(out, name, rule)
            out.write_literal('\n')

        for build in self._builds:
            self._write_build(out, build)
            out.write_literal('\n')

        if self._defaults:
            out.write_literal('default ')
            out.write_each(self._defaults, Syntax.input)
            out.write_literal('\n')
