from __future__ import print_function
import os, subprocess

from webassets.filter import ExternalTool
from webassets.cache import FilesystemCache


__all__ = ('Sass', 'SCSS')


class Sass(ExternalTool):
    """Converts `Sass <http://sass-lang.com/>`_ markup to real CSS.

    Requires the Sass executable to be available externally. To install
    it, you might be able to do::

         $ sudo gem install sass

    By default, this works as an "input filter", meaning ``sass`` is
    called for each source file in the bundle. This is because the
    path of the source file is required so that @import directives
    within the Sass file can be correctly resolved.

    However, it is possible to use this filter as an "output filter",
    meaning the source files will first be concatenated, and then the
    Sass filter is applied in one go. This can provide a speedup for
    bigger projects.

    To use Sass as an output filter::

        from webassets.filter import get_filter
        sass = get_filter('sass', as_output=True)
        Bundle(...., filters=(sass,))

    However, if you want to use the output filter mode and still also
    use the @import directive in your Sass files, you will need to
    pass along the ``load_paths`` argument, which specifies the path
    to which the imports are relative to (this is implemented by
    changing the working directory before calling the ``sass``
    executable)::

        sass = get_filter('sass', as_output=True, load_paths='/tmp')

    With ``as_output=True``, the resulting concatenation of the Sass
    files is piped to Sass via stdin (``cat ... | sass --stdin ...``)
    and may cause applications to not compile if import statements are
    given as relative paths.

    For example, if a file ``foo/bar/baz.scss`` imports file
    ``foo/bar/bat.scss`` (same directory) and the import is defined as
    ``@import "bat";`` then Sass will fail compiling because Sass
    has naturally no information on where ``baz.scss`` is located on
    disk (since the data was passed via stdin) in order for Sass to
    resolve the location of ``bat.scss``::

        Traceback (most recent call last):
        ...
        webassets.exceptions.FilterError: sass: subprocess had error: stderr=(sass):1: File to import not found or unreadable: bat. (Sass::SyntaxError)
               Load paths:
                 /path/to/project-foo
                on line 1 of standard input
          Use --trace for backtrace.
        , stdout=, returncode=65

    To overcome this issue, the full path must be provided in the
    import statement, ``@import "foo/bar/bat"``, then webassets
    will pass the ``load_paths`` argument (e.g.,
    ``/path/to/project-foo``) to Sass via its ``-I`` flags so Sass can
    resolve the full path to the file to be imported:
    ``/path/to/project-foo/foo/bar/bat``

    Support configuration options:

    SASS_BIN
        The path to the Sass binary. If not set, the filter will
        try to run ``sass`` as if it's in the system path.

    SASS_STYLE
        The style for the output CSS. Can be one of ``expanded`` (default),
        ``nested``, ``compact`` or ``compressed``.

    SASS_DEBUG_INFO
        If set to ``True``, will cause Sass to output debug information
        to be used by the FireSass Firebug plugin. Corresponds to the
        ``--debug-info`` command line option of Sass.

        Note that for this, Sass uses ``@media`` rules, which are
        not removed by a CSS compressor. You will thus want to make
        sure that this option is disabled in production.

        By default, the value of this option will depend on the
        environment ``DEBUG`` setting.

    SASS_LINE_COMMENTS
        Passes ``--line-comments`` flag to sass which emit comments in the
        generated CSS indicating the corresponding source line.

	Note that this option is disabled by Sass if ``--style compressed`` or
        ``--debug-info`` options are provided.

        Enabled by default. To disable, set empty environment variable
        ``SASS_LINE_COMMENTS=`` or pass ``line_comments=False`` to this filter.

    SASS_AS_OUTPUT
        By default, this works as an "input filter", meaning ``sass`` is
        called for each source file in the bundle. This is because the
        path of the source file is required so that @import directives
        within the Sass file can be correctly resolved.

        However, it is possible to use this filter as an "output filter",
        meaning the source files will first be concatenated, and then the
        Sass filter is applied in one go. This can provide a speedup for
        bigger projects.

        It will also allow you to share variables between files.

    SASS_LOAD_PATHS
        It should be a list of paths relatives to Environment.directory or absolute paths.
        Order matters as sass will pick the first file found in path order.
        These are fed into the -I flag of the sass command and
        is used to control where sass imports code from.

    SASS_LIBS
        It should be a list of paths relatives to Environment.directory or absolute paths.
        These are fed into the -r flag of the sass command and
        is used to require ruby libraries before running sass.
    """
    # TODO: If an output filter could be passed the list of all input
    # files, the filter might be able to do something interesting with
    # it (for example, determine that all source files are in the same
    # directory).

    name = 'sass'
    options = {
        'binary': 'SASS_BIN',
        'use_scss': ('scss', 'SASS_USE_SCSS'),
        'use_compass': ('use_compass', 'SASS_COMPASS'),
        'debug_info': 'SASS_DEBUG_INFO',
        'as_output': 'SASS_AS_OUTPUT',
        'load_paths': 'SASS_LOAD_PATHS',
        'libs': 'SASS_LIBS',
        'style': 'SASS_STYLE',
        'line_comments': 'SASS_LINE_COMMENTS',
    }
    max_debug_level = None

    def resolve_path(self, path):
        return self.ctx.resolver.resolve_source(self.ctx, path)

    def _apply_sass(self, _in, out, cd=None):
        # Switch to source file directory if asked, so that this directory
        # is by default on the load path. We could pass it via -I, but then
        # files in the (undefined) wd could shadow the correct files.
        orig_cwd = os.getcwd()
        child_cwd = orig_cwd
        if cd:
            child_cwd = cd

        args = [self.binary or 'sass',
                '--stdin',
                '--style', self.style or 'expanded']
        if self.line_comments is None or self.line_comments:
            args.append('--line-comments')
        if isinstance(self.ctx.cache, FilesystemCache):
            args.extend(['--cache-location',
                         os.path.join(orig_cwd, self.ctx.cache.directory, 'sass')])
        elif not cd:
            # Without a fixed working directory, the location of the cache
            # is basically undefined, so prefer not to use one at all.
            args.extend(['--no-cache'])
        if (self.ctx.environment.debug if self.debug_info is None else self.debug_info):
            args.append('--debug-info')
        if self.use_scss:
            args.append('--scss')
        if self.use_compass:
            args.append('--compass')
        for path in self.load_paths or []:
            if os.path.isabs(path):
                abs_path = path
            else:
                abs_path = self.resolve_path(path)
            args.extend(['-I', abs_path])
        for lib in self.libs or []:
            if os.path.isabs(lib):
                abs_path = lib
            else:
                abs_path = self.resolve_path(lib)
            args.extend(['-r', abs_path])

        return self.subprocess(args, out, _in, cwd=child_cwd)

    def input(self, _in, out, source_path, output_path, **kw):
        if self.as_output:
            out.write(_in.read())
        else:
            self._apply_sass(_in, out, os.path.dirname(source_path))

    def output(self, _in, out, **kwargs):
        if not self.as_output:
            out.write(_in.read())
        else:
            self._apply_sass(_in, out)


class SCSS(Sass):
    """Version of the ``sass`` filter that uses the SCSS syntax.
    """

    name = 'scss'

    def __init__(self, *a, **kw):
        assert not 'scss' in kw
        kw['scss'] = True
        super(SCSS, self).__init__(*a, **kw)
