# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from style_variable_generator.base_generator import BaseGenerator
from style_variable_generator.model import Modes, VariableType
from style_variable_generator.color import (
    ColorBlend,
    ColorVar,
    ColorRGBVar,
    ColorRGB,
)
import collections


class CSSStyleGenerator(BaseGenerator):
    '''Generator for CSS Variables'''

    @staticmethod
    def GetName():
        return 'CSS'

    def Render(self):
        return self.ApplyTemplate(
            self, 'templates/css_generator.tmpl', self.GetParameters()
        )

    def GetParameters(self):
        if self.generate_single_mode:
            resolved_colors = self.model.colors.Flatten(resolve_missing=True)
            resolved_opacities = self.model.opacities.Flatten(
                resolve_missing=True
            )
            resolved_legacy_mappings = self.model.legacy_mappings.Flatten(
                resolve_missing=True
            )
            colors = {Modes.DEFAULT: resolved_colors[self.generate_single_mode]}
            legacy_mappings = {
                Modes.DEFAULT: resolved_legacy_mappings[
                    self.generate_single_mode
                ]
            }
            opacities = {
                Modes.DEFAULT: resolved_opacities[self.generate_single_mode]
            }
        else:
            colors = self.model.colors.Flatten()
            opacities = self.model.opacities.Flatten()
            legacy_mappings = self.model.legacy_mappings.Flatten()

        return {
            'opacities': opacities,
            'colors': colors,
            'legacy_mappings': legacy_mappings,
            'typefaces': self.model.typefaces,
            'font_faces': self.model.font_faces,
            'font_families': self.model.font_families,
            'untyped_css': self.model.untyped_css,
        }

    def GetFilters(self):
        return {
            'to_css_var_name': self.ToCSSVarName,
            'to_css_var_name_unscoped': self.ToCSSVarNameUnscoped,
            'css_opacity': self._CSSOpacity,
            'css_color_rgb': self.CSSColorRGB,
            'process_simple_ref': self.ProcessSimpleRef,
        }

    def GetGlobals(self):
        return {
            'css_color_var': self.CSSColorVar,
            'needs_rgb_variant': self.NeedsRGBVariant,
            'in_files': self.GetInputFiles(),
            'dark_mode_selector': self.generator_options.get(
                'dark_mode_selector', None
            ),
            'suppress_sources_comment': self.generator_options.get(
                'suppress_sources_comment', False
            ),
            'Modes': Modes,
        }

    def DefaultPreblend(self):
        return False

    def AddGeneratedVars(self, var_names, variable):
        def AddVarNames(name, variations):
            for v in variations:
                var_name = v.replace('$css_name', self.ToCSSVarName(name))
                if var_name in var_names:
                    raise ValueError(name + " is defined multiple times")
                var_names[var_name] = name

        variable_type = variable.variable_type
        if variable_type == VariableType.OPACITY:
            AddVarNames(variable.name, ['$css_name'])
        elif variable_type == VariableType.COLOR:
            AddVarNames(variable.name, ['$css_name', '$css_name-rgb'])
        elif variable_type == VariableType.UNTYPED_CSS:
            AddVarNames(variable.name, ['$css_name'])
        elif variable_type == VariableType.FONT_FAMILY:
            AddVarNames(variable.name, ['$css_name'])
        elif variable_type == VariableType.TYPEFACE:
            AddVarNames(
                variable.name,
                [
                    '$css_name-font',
                    '$css_name-font-family',
                    '$css_name-font-size',
                    '$css_name-font-weight',
                    '$css_name-line-height',
                ],
            )
        elif variable_type == VariableType.FONT_FACE:
            # Font faces are not individual attributes
            pass
        elif variable_type == VariableType.LEGACY_MAPPING:
            # No Clients should be directly using any of the legacy mappings.
            pass
        else:
            raise ValueError("GetGeneratedVars() for '%s' not implemented")

    def GetCSSVarNames(self):
        '''Returns a map of all generated names to the model names that
        generated them.
        '''
        var_names = dict()
        for variable in self.model.variable_map.values():
            self.AddGeneratedVars(var_names, variable)

        return var_names

    def ProcessSimpleRef(self, value):
        '''If |value| is a simple '$other_variable' reference, returns a
        CSS variable that points to '$other_variable'.'''
        if value.startswith('$'):
            ref_name = value[1:]
            assert ref_name in self.model.variable_map
            value = 'var({0})'.format(self.ToCSSVarName(ref_name))

        return value

    def _GetCSSVarPrefix(self, name):
        prefix = (
            self.model.variable_map[name]
            .context.get(CSSStyleGenerator.GetName(), {})
            .get('prefix')
        )
        return prefix + '-' if prefix else ''

    def ToCSSVarName(self, name):
        # This handles old_semantic_names as well as new.token-names.
        var_name = name.translate(str.maketrans('-_.', '_--'))

        return '--%s%s' % (self._GetCSSVarPrefix(name), var_name)

    def ToCSSVarNameUnscoped(self, name):
        return f'--{name}'

    def _CSSOpacity(self, opacity):
        if opacity.var:
            return 'var(%s)' % self.ToCSSVarName(opacity.var)

        return ('%f' % opacity.a).rstrip('0').rstrip('.')

    def CSSColorRGB(self, c):
        '''Returns the CSS rgb representation of |c|'''
        if isinstance(c, ColorVar):
            return 'var(%s-rgb)' % self.ToCSSVarName(c.var)

        if isinstance(c, ColorRGBVar):
            return 'var(%s-rgb)' % self.ToCSSVarName(c.ToVar())

        if isinstance(c, ColorRGB):
            return '%d, %d, %d' % (c.r, c.g, c.b)

        raise NotImplementedError(f'Cannot reduce {c} to RBG')

    def CSSBlendInputColor(self, c, mode):
        '''Resolves a color for use in a color-mix call.'''
        # TODO(b/278121949): Assert that the color is opaque.
        if isinstance(c, ColorVar):
            return 'var(%s)' % self.ToCSSVarName(c.var)
        if isinstance(c, ColorBlend):
            return self.ToBlendColor(c, mode)
        return 'rgb(%s)' % self.CSSColorRGB(c)

    def ToBlendColor(self, color, mode):
        '''Resolves a color blend. Allows for nested blends.'''
        assert isinstance(color, ColorBlend)
        blendPercentage = float(
            color.blendPercentage
            or self.ExtractOpacity(color.blended_colors[0], mode)
        )
        return 'color-mix(in srgb, %s %s%%, %s)' % (
            self.CSSBlendInputColor(color.blended_colors[0], mode),
            blendPercentage,
            self.CSSBlendInputColor(color.blended_colors[1], mode),
        )

    def ExtractOpacity(self, c, mode):
        if isinstance(c, ColorVar):
            return self.ExtractOpacity(
                self.model.colors.Resolve(c.var, mode), mode
            )
        if c.opacity:
            return self.model.opacities.ResolveOpacity(c.opacity, mode).a * 100

        # If we don't have opacity information assume we want to blend 100%.
        return 100

    def CSSColorVar(self, name, color, mode, unscoped=False):
        '''Returns the CSS color representation given a color name and color'''
        if unscoped:
            var_name = self.ToCSSVarNameUnscoped(name)
        else:
            var_name = self.ToCSSVarName(name)

        if isinstance(color, ColorVar):
            return 'var(%s)' % self.ToCSSVarName(color.var)

        if isinstance(color, ColorBlend):
            return self.ToBlendColor(color, mode)

        if (
            isinstance(color, ((ColorRGB, ColorRGBVar)))
            and color.opacity.a != 1
        ):
            return 'rgba(var(%s-rgb), %s)' % (
                var_name,
                self._CSSOpacity(color.opacity),
            )

        return 'rgb(var(%s-rgb))' % var_name

    def NeedsRGBVariant(self, color):
        return not isinstance(color, ColorBlend)
