"""Constants and functions defined by the CSS specification, not specific to
Sass.
"""
from fractions import Fraction
from math import pi
import re

import six

# ------------------------------------------------------------------------------
# Built-in CSS color names
# See: http://www.w3.org/TR/css3-color/#svg-color

COLOR_NAMES = {
    'aliceblue': (240, 248, 255, 1),
    'antiquewhite': (250, 235, 215, 1),
    'aqua': (0, 255, 255, 1),
    'aquamarine': (127, 255, 212, 1),
    'azure': (240, 255, 255, 1),
    'beige': (245, 245, 220, 1),
    'bisque': (255, 228, 196, 1),
    'black': (0, 0, 0, 1),
    'blanchedalmond': (255, 235, 205, 1),
    'blue': (0, 0, 255, 1),
    'blueviolet': (138, 43, 226, 1),
    'brown': (165, 42, 42, 1),
    'burlywood': (222, 184, 135, 1),
    'cadetblue': (95, 158, 160, 1),
    'chartreuse': (127, 255, 0, 1),
    'chocolate': (210, 105, 30, 1),
    'coral': (255, 127, 80, 1),
    'cornflowerblue': (100, 149, 237, 1),
    'cornsilk': (255, 248, 220, 1),
    'crimson': (220, 20, 60, 1),
    'cyan': (0, 255, 255, 1),
    'darkblue': (0, 0, 139, 1),
    'darkcyan': (0, 139, 139, 1),
    'darkgoldenrod': (184, 134, 11, 1),
    'darkgray': (169, 169, 169, 1),
    'darkgreen': (0, 100, 0, 1),
    'darkkhaki': (189, 183, 107, 1),
    'darkmagenta': (139, 0, 139, 1),
    'darkolivegreen': (85, 107, 47, 1),
    'darkorange': (255, 140, 0, 1),
    'darkorchid': (153, 50, 204, 1),
    'darkred': (139, 0, 0, 1),
    'darksalmon': (233, 150, 122, 1),
    'darkseagreen': (143, 188, 143, 1),
    'darkslateblue': (72, 61, 139, 1),
    'darkslategray': (47, 79, 79, 1),
    'darkturquoise': (0, 206, 209, 1),
    'darkviolet': (148, 0, 211, 1),
    'deeppink': (255, 20, 147, 1),
    'deepskyblue': (0, 191, 255, 1),
    'dimgray': (105, 105, 105, 1),
    'dodgerblue': (30, 144, 255, 1),
    'firebrick': (178, 34, 34, 1),
    'floralwhite': (255, 250, 240, 1),
    'forestgreen': (34, 139, 34, 1),
    'fuchsia': (255, 0, 255, 1),
    'gainsboro': (220, 220, 220, 1),
    'ghostwhite': (248, 248, 255, 1),
    'gold': (255, 215, 0, 1),
    'goldenrod': (218, 165, 32, 1),
    'gray': (128, 128, 128, 1),
    'green': (0, 128, 0, 1),
    'greenyellow': (173, 255, 47, 1),
    'honeydew': (240, 255, 240, 1),
    'hotpink': (255, 105, 180, 1),
    'indianred': (205, 92, 92, 1),
    'indigo': (75, 0, 130, 1),
    'ivory': (255, 255, 240, 1),
    'khaki': (240, 230, 140, 1),
    'lavender': (230, 230, 250, 1),
    'lavenderblush': (255, 240, 245, 1),
    'lawngreen': (124, 252, 0, 1),
    'lemonchiffon': (255, 250, 205, 1),
    'lightblue': (173, 216, 230, 1),
    'lightcoral': (240, 128, 128, 1),
    'lightcyan': (224, 255, 255, 1),
    'lightgoldenrodyellow': (250, 250, 210, 1),
    'lightgreen': (144, 238, 144, 1),
    'lightgrey': (211, 211, 211, 1),
    'lightpink': (255, 182, 193, 1),
    'lightsalmon': (255, 160, 122, 1),
    'lightseagreen': (32, 178, 170, 1),
    'lightskyblue': (135, 206, 250, 1),
    'lightslategray': (119, 136, 153, 1),
    'lightsteelblue': (176, 196, 222, 1),
    'lightyellow': (255, 255, 224, 1),
    'lime': (0, 255, 0, 1),
    'limegreen': (50, 205, 50, 1),
    'linen': (250, 240, 230, 1),
    'magenta': (255, 0, 255, 1),
    'maroon': (128, 0, 0, 1),
    'mediumaquamarine': (102, 205, 170, 1),
    'mediumblue': (0, 0, 205, 1),
    'mediumorchid': (186, 85, 211, 1),
    'mediumpurple': (147, 112, 219, 1),
    'mediumseagreen': (60, 179, 113, 1),
    'mediumslateblue': (123, 104, 238, 1),
    'mediumspringgreen': (0, 250, 154, 1),
    'mediumturquoise': (72, 209, 204, 1),
    'mediumvioletred': (199, 21, 133, 1),
    'midnightblue': (25, 25, 112, 1),
    'mintcream': (245, 255, 250, 1),
    'mistyrose': (255, 228, 225, 1),
    'moccasin': (255, 228, 181, 1),
    'navajowhite': (255, 222, 173, 1),
    'navy': (0, 0, 128, 1),
    'oldlace': (253, 245, 230, 1),
    'olive': (128, 128, 0, 1),
    'olivedrab': (107, 142, 35, 1),
    'orange': (255, 165, 0, 1),
    'orangered': (255, 69, 0, 1),
    'orchid': (218, 112, 214, 1),
    'palegoldenrod': (238, 232, 170, 1),
    'palegreen': (152, 251, 152, 1),
    'paleturquoise': (175, 238, 238, 1),
    'palevioletred': (219, 112, 147, 1),
    'papayawhip': (255, 239, 213, 1),
    'peachpuff': (255, 218, 185, 1),
    'peru': (205, 133, 63, 1),
    'pink': (255, 192, 203, 1),
    'plum': (221, 160, 221, 1),
    'powderblue': (176, 224, 230, 1),
    'purple': (128, 0, 128, 1),
    'rebeccapurple': (0x66, 0x33, 0x99, 1),
    'red': (255, 0, 0, 1),
    'rosybrown': (188, 143, 143, 1),
    'royalblue': (65, 105, 225, 1),
    'saddlebrown': (139, 69, 19, 1),
    'salmon': (250, 128, 114, 1),
    'sandybrown': (244, 164, 96, 1),
    'seagreen': (46, 139, 87, 1),
    'seashell': (255, 245, 238, 1),
    'sienna': (160, 82, 45, 1),
    'silver': (192, 192, 192, 1),
    'skyblue': (135, 206, 235, 1),
    'slateblue': (106, 90, 205, 1),
    'slategray': (112, 128, 144, 1),
    'snow': (255, 250, 250, 1),
    'springgreen': (0, 255, 127, 1),
    'steelblue': (70, 130, 180, 1),
    'tan': (210, 180, 140, 1),
    'teal': (0, 128, 128, 1),
    'thistle': (216, 191, 216, 1),
    'tomato': (255, 99, 71, 1),
    'transparent': (0, 0, 0, 0),
    'turquoise': (64, 224, 208, 1),
    'violet': (238, 130, 238, 1),
    'wheat': (245, 222, 179, 1),
    'white': (255, 255, 255, 1),
    'whitesmoke': (245, 245, 245, 1),
    'yellow': (255, 255, 0, 1),
    'yellowgreen': (154, 205, 50, 1),
}
COLOR_LOOKUP = dict((v, k) for (k, v) in COLOR_NAMES.items())


# ------------------------------------------------------------------------------
# Built-in CSS units
# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#numeric-types

# Maps units to a set of common units per type, with conversion factors
BASE_UNIT_CONVERSIONS = {
    # Lengths
    'mm': (1, 'mm'),
    'cm': (10, 'mm'),
    'in': (Fraction(254, 10), 'mm'),
    'px': (Fraction(254, 960), 'mm'),
    'pt': (Fraction(254, 720), 'mm'),
    'pc': (Fraction(254, 60), 'mm'),

    # Angles
    'deg': (Fraction(1, 360), 'turn'),
    'grad': (Fraction(1, 400), 'turn'),
    'rad': (Fraction.from_float(pi / 2), 'turn'),
    'turn': (1, 'turn'),

    # Times
    'ms': (1, 'ms'),
    's':  (1000, 'ms'),

    # Frequencies
    'hz': (1, 'hz'),
    'khz': (1000, 'hz'),

    # Resolutions
    'dpi': (1, 'dpi'),
    'dpcm': (Fraction(254 / 100), 'dpi'),
    'dppx': (96, 'dpi'),
}


def get_conversion_factor(unit):
    """Look up the "base" unit for this unit and the factor for converting to
    it.

    Returns a 2-tuple of `factor, base_unit`.
    """
    if unit in BASE_UNIT_CONVERSIONS:
        return BASE_UNIT_CONVERSIONS[unit]
    else:
        return 1, unit


def convert_units_to_base_units(units):
    """Convert a set of units into a set of "base" units.

    Returns a 2-tuple of `factor, new_units`.
    """
    total_factor = 1
    new_units = []
    for unit in units:
        if unit not in BASE_UNIT_CONVERSIONS:
            continue

        factor, new_unit = BASE_UNIT_CONVERSIONS[unit]
        total_factor *= factor
        new_units.append(new_unit)

    new_units.sort()
    return total_factor, tuple(new_units)


def count_base_units(units):
    """Returns a dict mapping names of base units to how many times they
    appear in the given iterable of units.  Effectively this counts how
    many length units you have, how many time units, and so forth.
    """
    ret = {}
    for unit in units:
        factor, base_unit = get_conversion_factor(unit)

        ret.setdefault(base_unit, 0)
        ret[base_unit] += 1

    return ret


def cancel_base_units(units, to_remove):
    """Given a list of units, remove a specified number of each base unit.

    Arguments:
        units: an iterable of units
        to_remove: a mapping of base_unit => count, such as that returned from
            count_base_units

    Returns a 2-tuple of (factor, remaining_units).
    """

    # Copy the dict since we're about to mutate it
    to_remove = to_remove.copy()
    remaining_units = []
    total_factor = Fraction(1)

    for unit in units:
        factor, base_unit = get_conversion_factor(unit)
        if not to_remove.get(base_unit, 0):
            remaining_units.append(unit)
            continue

        total_factor *= factor
        to_remove[base_unit] -= 1

    return total_factor, remaining_units


# A fixed set of units can be omitted when the value is 0
# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#lengths
ZEROABLE_UNITS = frozenset((
    # Relative lengths
    'em', 'ex', 'ch', 'rem',
    # Viewport
    'vw', 'vh', 'vmin', 'vmax',
    # Absolute lengths
    'cm', 'mm', 'in', 'px', 'pt', 'pc',
))


# ------------------------------------------------------------------------------
# Built-in CSS function reference

# Known function names
BUILTIN_FUNCTIONS = frozenset([
    # CSS2
    'attr', 'counter', 'counters', 'url', 'rgb', 'rect',

    # CSS3 values: http://www.w3.org/TR/css3-values/
    'calc', 'min', 'max', 'cycle',

    # CSS3 colors: http://www.w3.org/TR/css3-color/
    'rgba', 'hsl', 'hsla',

    # CSS3 fonts: http://www.w3.org/TR/css3-fonts/
    'local', 'format',

    # CSS3 images: http://www.w3.org/TR/css3-images/
    'image', 'element',
    'linear-gradient', 'radial-gradient',
    'repeating-linear-gradient', 'repeating-radial-gradient',

    # CSS3 transforms: http://www.w3.org/TR/css3-transforms/
    'perspective',
    'matrix', 'matrix3d',
    'rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d',
    'translate', 'translateX', 'translateY', 'translateZ', 'translate3d',
    'scale', 'scaleX', 'scaleY', 'scaleZ', 'scale3d',
    'skew', 'skewX', 'skewY',

    # CSS3 transitions: http://www.w3.org/TR/css3-transitions/
    'cubic-bezier', 'steps',

    # CSS filter effects:
    # https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html
    'grayscale', 'sepia', 'saturate', 'hue-rotate', 'invert', 'opacity',
    'brightness', 'contrast', 'blur', 'drop-shadow', 'custom',

    # CSS shapes
    # https://www.w3.org/TR/css-shapes-1/
    'inset', 'circle', 'ellipse', 'polygon',

    # CSS4 image module:
    # http://dev.w3.org/csswg/css-images/
    'image-set', 'cross-fade',
    'conic-gradient', 'repeating-conic-gradient',

    # Others
    'color-stop',           # Older version of CSS3 gradients
    'mask',                 # ???
    'from', 'to',           # Very old WebKit gradient syntax
])


def is_builtin_css_function(name):
    """Returns whether the given `name` looks like the name of a builtin CSS
    function.

    Unrecognized functions not in this list produce warnings.
    """
    name = name.replace('_', '-')

    if name in BUILTIN_FUNCTIONS:
        return True

    # Vendor-specific functions (-foo-bar) are always okay
    if name[0] == '-' and '-' in name[1:]:
        return True

    return False


# ------------------------------------------------------------------------------
# CSS character set determination
# Based upon: http://www.w3.org/TR/CSS2/syndata.html#charset

def determine_encoding(buf):
    """Return the appropriate encoding for the given CSS source, according to
    the CSS charset rules.

    `buf` may be either a string or bytes.
    """
    # The ultimate default is utf8; bravo, W3C
    bom_encoding = 'UTF-8'

    if not buf:
        # What
        return bom_encoding

    if isinstance(buf, six.text_type):
        # We got a file that, for whatever reason, produces already-decoded
        # text.  Check for the BOM (which is useless now) and believe
        # whatever's in the @charset.
        if buf[0] == '\ufeff':
            buf = buf[0:]

        # This is pretty similar to the code below, but without any encoding
        # double-checking.
        charset_start = '@charset "'
        charset_end = '";'
        if buf.startswith(charset_start):
            start = len(charset_start)
            end = buf.index(charset_end, start)
            return buf[start:end]
        else:
            return bom_encoding

    # BOMs
    if buf[:3] == b'\xef\xbb\xbf':
        bom_encoding = 'UTF-8'
        buf = buf[3:]
    if buf[:4] == b'\x00\x00\xfe\xff':
        bom_encoding = 'UTF-32BE'
        buf = buf[4:]
    elif buf[:4] == b'\xff\xfe\x00\x00':
        bom_encoding = 'UTF-32LE'
        buf = buf[4:]
    if buf[:4] == b'\x00\x00\xff\xfe':
        raise UnicodeError("UTF-32-2143 is not supported")
    elif buf[:4] == b'\xfe\xff\x00\x00':
        raise UnicodeError("UTF-32-2143 is not supported")
    elif buf[:2] == b'\xfe\xff':
        bom_encoding = 'UTF-16BE'
        buf = buf[2:]
    elif buf[:2] == b'\xff\xfe':
        bom_encoding = 'UTF-16LE'
        buf = buf[2:]

    # The spec requires exactly this syntax; no escapes or extra spaces or
    # other shenanigans, thank goodness.
    charset_start = '@charset "'.encode(bom_encoding)
    charset_end = '";'.encode(bom_encoding)
    if buf.startswith(charset_start):
        start = len(charset_start)
        end = buf.index(charset_end, start)
        encoded_encoding = buf[start:end]
        encoding = encoded_encoding.decode(bom_encoding)

        # Ensure that decoding with the specified encoding actually produces
        # the same @charset rule
        encoded_charset = buf[:end + len(charset_end)]
        if (encoded_charset.decode(encoding) !=
                encoded_charset.decode(bom_encoding)):
            raise UnicodeError(
                "@charset {0} is incompatible with detected encoding {1}"
                .format(bom_encoding, encoding))
    else:
        # With no @charset, believe the BOM
        encoding = bom_encoding

    return encoding


# ------------------------------------------------------------------------------
# Bits and pieces of the official CSS grammar

# These are the only pseudo-elements allowed to be specified with a single
# colon, for backwards compatibility
CSS2_PSEUDO_ELEMENTS = frozenset((
    ':after',
    ':before',
    ':first-line',
    ':first-letter',
))

# CSS escape sequences are either a backslash followed by a single character,
# or a backslash followed by one to six hex digits and a single optional
# whitespace.  Escaped newlines become nothing.
# Ref: http://dev.w3.org/csswg/css-syntax-3/#consume-an-escaped-code-point
escape_rx = re.compile(r"(?s)\\([0-9a-fA-F]{1,6})[\n\t ]?|\\(.)|\\\n")


def _unescape_one(match):
    if match.group(1) is not None:
        try:
            return six.unichr(int(match.group(1), 16))
        except ValueError:
            return (r'\U%08x' % int(match.group(1), 16)).decode(
                'unicode-escape')
    elif match.group(2) is not None:
        return match.group(2)
    else:
        return six.text_type()


def unescape(string):
    """Given a raw CSS string (i.e. taken directly from CSS source with no
    processing), eliminate all backslash escapes.
    """
    return escape_rx.sub(_unescape_one, string)


# ------------------------------------------------------------------------------
# Ad-hoc regexes specific to pyscss

_expr_glob_re = re.compile(r'''
    \#\{(.*?)\}                   # Global Interpolation only
''', re.VERBOSE)

# XXX these still need to be fixed; the //-in-functions thing is a chumpy hack
_ml_comment_re = re.compile(r'\/\*(.*?)\*\/', re.DOTALL)
_sl_comment_re = re.compile(r'(?<!\burl[(])(?<!\w{2}:)\/\/.*')

_escape_chars_re = re.compile(r'([^-a-zA-Z0-9_])')
_interpolate_re = re.compile(r'(#\{\s*)?(\$[-\w]+)(?(1)\s*\})')
_spaces_re = re.compile(r'\s+')
_collapse_properties_space_re = re.compile(r'([:#])\s*{')
_variable_re = re.compile('^\\$[-a-zA-Z0-9_]+$')

_strings_re = re.compile(r'([\'"]).*?\1')
# TODO i know, this is clumsy and won't always work; it's better than nothing
_urls_re = re.compile(r'url[(].*?[)]')

_has_placeholder_re = re.compile(r'(?<!\w)([a-z]\w*)?%')
_prop_split_re = re.compile(r'[:=]')
_has_code_re = re.compile('''
    (?:^|(?<=[{;}]))            # the character just before it should be a '{', a ';' or a '}'
    \s*                         # ...followed by any number of spaces
    (?:
        (?:
            \+
        | @include
        | @warn
        | @mixin
        | @function
        | @if
        | @else
        | @for
        | @each
        )
        (?![^(:;}]*['"])
    |
        @import
    )
''', re.VERBOSE)
