#!/usr/bin/env python
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
# 
# The contents of this file are subject to the Mozilla Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
# 
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
# 
# The Original Code is Komodo code.
# 
# The Initial Developer of the Original Code is ActiveState Software Inc.
# Portions created by ActiveState Software Inc are Copyright (C) 2000-2007
# ActiveState Software Inc. All Rights Reserved.
# 
# Contributor(s):
#   ActiveState Software Inc
#   Sergey Kislyakov
# 
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
# 
# ***** END LICENSE BLOCK *****

"""CSS support for CodeIntel"""

import os
from os.path import isfile, isdir, exists, dirname, abspath, splitext, join
import sys
import stat
import string
from cStringIO import StringIO
import logging
import traceback
from pprint import pprint
import operator
import re

import SilverCity
from SilverCity.Lexer import Lexer
from SilverCity import ScintillaConstants
from SilverCity.ScintillaConstants import (
    SCE_CSS_DIRECTIVE, SCE_CSS_DOUBLESTRING, SCE_CSS_IDENTIFIER,
    SCE_CSS_IDENTIFIER2, SCE_CSS_OPERATOR, SCE_CSS_SINGLESTRING,
    SCE_CSS_TAG, SCE_CSS_UNKNOWN_IDENTIFIER, SCE_CSS_VALUE,
    SCE_UDL_CSS_COMMENT, SCE_UDL_CSS_DEFAULT, SCE_UDL_CSS_IDENTIFIER,
    SCE_UDL_CSS_NUMBER, SCE_UDL_CSS_OPERATOR, SCE_UDL_CSS_STRING,
    SCE_UDL_CSS_WORD, SCE_UDL_M_STRING, SCE_UDL_M_ATTRNAME, SCE_UDL_M_OPERATOR,
)
from SilverCity import Keywords

from ciElementTree import SubElement
from codeintel2.citadel import CitadelBuffer, CitadelLangIntel, ImportHandler
from codeintel2.common import *
from codeintel2.buffer import Buffer
from codeintel2.util import (OrdPunctLast, make_short_name_dict,
                             makePerformantLogger, gen_dirs_under_dirs)
from codeintel2.langintel import ParenStyleCalltipIntelMixin
from codeintel2.gencix_utils import *
from codeintel2.udl import UDLBuffer, is_udl_css_style
from codeintel2.accessor import AccessorCache

if _xpcom_:
    from xpcom.server import UnwrapObject



#---- globals

lang = "CSS"
log = logging.getLogger("codeintel.css")
makePerformantLogger(log)
WHITESPACE = tuple(" \t\r\n")  # care about '\v', '\f'?



#---- language support

# Taken from the Scite version 2.0.2 css.properties file
# Silvercity wants the # of wordlists to be the same as the
# number hardwired in the lexer, so that's why there are 5 empty lists.
raw_word_lists = [
    # CSS1 keywords
    """
    background background-attachment background-color background-image
    background-position background-repeat border border-bottom
    border-bottom-width border-color border-left border-left-width
    border-right border-right-width border-style border-top
    border-top-width border-width
    clear color display float font
    font-family font-size font-style font-variant font-weight height
    letter-spacing line-height list-style list-style-image
    list-style-position list-style-type margin margin-bottom margin-left
    margin-right margin-top padding padding-bottom padding-left
    padding-right padding-top text-align text-decoration text-indent
    text-transform vertical-align white-space width word-spacing
    """,
    # CSS pseudo-classes
    """
    active after before first first-child first-letter first-line
    focus hover lang left link right visited
    """,

    # CSS2 keywords
    """
    ascent azimuth baseline bbox border-bottom-color
    border-bottom-style border-collapse border-color border-left-color
    border-left-style border-right-color border-right-style
    border-spacing border-style border-top-color border-top-style
    bottom cap-height caption-side centerline clip content
    counter-increment counter-reset cue cue-after cue-before cursor
    definition-src descent direction elevation empty-cells
    font-size-adjust font-stretch left marker-offset marks mathline
    max-height max-width min-height min-width orphans outline
    outline-color outline-style outline-width overflow page
    page-break-after page-break-before page-break-inside panose-1
    pause pause-after pause-before pitch pitch-range play-during
    position quotes richness right size slope speak speak-header
    speak-numeral speak-punctuation speech-rate src stemh stemv stress
    table-layout text-shadow top topline unicode-bidi unicode-range
    units-per-em visibility voice-family volume widows widths x-height
    z-index
    """,
    # CSS3 Properties
    """
    border-top-left-radius
    border-top-right-radius
    border-bottom-left-radius
    border-bottom-right-radius
    border-radius
    """,
    # Pseudo-elements
    "",
    # Browser-Specific CSS Properties
    "",
    # Browser-Specific Pseudo-classes
    "",
    # Browser-Specific Pseudo-elements
    "",
    ]
    
class CSSLexer(Lexer):
    lang = "CSS"
    def __init__(self):
        self._properties = SilverCity.PropertySet()
        self._lexer = SilverCity.find_lexer_module_by_id(ScintillaConstants.SCLEX_CSS)
        self._keyword_lists = []
        for i in range(len(raw_word_lists)):
            self._keyword_lists.append(SilverCity.WordList(raw_word_lists[i]))

class _StraightCSSStyleClassifier(object):
    def is_css_style(self, style, accessorCacheBack=None):
        return True

    def is_default(self, style, accessorCacheBack=None):
        return style in self.default_styles

    def is_comment(self, style, accessorCacheBack=None):
        return style in self.comment_styles

    def is_string(self, style, accessorCacheBack=None):
        return style in self.string_styles

    def is_operator(self, style, accessorCacheBack=None):
        return style in self.operator_styles or \
               style == ScintillaConstants.SCE_CSS_IMPORTANT

    def is_identifier(self, style, accessorCacheBack=None):
        return style in self.identifier_styles

    def is_value(self, style, accessorCacheBack=None):
        return style in self.value_styles

    def is_tag(self, style, accessorCacheBack=None):
        return style in self.tag_styles

    def is_class(self, style, accessorCacheBack=None):
        return style in self.class_styles

    def is_number(self, style, accessorCacheBack=None):
        return style in self.number_styles
    
    def is_directive(self, style, accessorCacheBack=None):
        return style == ScintillaConstants.SCE_CSS_DIRECTIVE

    @property
    def default_styles(self):
        return (ScintillaConstants.SCE_CSS_DEFAULT, )

    @property
    def comment_styles(self):
        return (ScintillaConstants.SCE_CSS_COMMENT,)

    @property
    def string_styles(self):
        return (ScintillaConstants.SCE_CSS_SINGLESTRING,
                ScintillaConstants.SCE_CSS_DOUBLESTRING)

    @property
    def operator_styles(self):
        return (ScintillaConstants.SCE_CSS_OPERATOR, )

    @property
    def identifier_styles(self):
        return (ScintillaConstants.SCE_CSS_IDENTIFIER,
                ScintillaConstants.SCE_CSS_IDENTIFIER2,
                ScintillaConstants.SCE_CSS_UNKNOWN_IDENTIFIER)

    @property
    def value_styles(self):
        return (ScintillaConstants.SCE_CSS_VALUE,
                ScintillaConstants.SCE_CSS_NUMBER)

    @property
    def tag_styles(self):
        return (ScintillaConstants.SCE_CSS_TAG, )

    @property
    def class_styles(self):
        return (ScintillaConstants.SCE_CSS_CLASS, )

    @property
    def number_styles(self):
        return ()

    @property
    def ignore_styles(self):
        return (ScintillaConstants.SCE_CSS_DEFAULT,
                ScintillaConstants.SCE_CSS_COMMENT)

DebugStatus = False

class _UDLCSSStyleClassifier(_StraightCSSStyleClassifier):
    def is_css_style(self, style, accessorCacheBack=None):
        return is_udl_css_style(style)

    def _is_html_style_attribute(self, ac, style):
        # Check to see if it's a html style attribute
        # Note: We are starting from the html string delimiter, i.e.:
        #   <body style=<|>"abc...
        DEBUG = DebugStatus
        # We may have already found out this is a style attribute, check it
        if getattr(ac, "is_html_style_attribute", False):
            return True
        p, ch, style = ac.getPrecedingPosCharStyle(style,
                        ignore_styles=self.ignore_styles)
        if DEBUG:
            print "  _is_html_style_attribute:: Prev style: %d, ch: %r" % (
                  style, ch, )
        if style == SCE_UDL_M_OPERATOR:
            p, ch, style = ac.getPrecedingPosCharStyle(style,
                            ignore_styles=self.ignore_styles)
            if style == SCE_UDL_M_ATTRNAME:
                p, name = ac.getTextBackWithStyle(style)
                if DEBUG:
                    print "  _is_html_style_attribute:: HTML Attribute: %r" % (
                          name, )
                if name == "style":
                    # Remember this is a html style attribute
                    ac.is_html_style_attribute = True
                    return True
        return False

    def is_identifier(self, style, accessorCacheBack=None):
        if style not in self.identifier_styles:
            return False

        # Previous style must be operator and one of "{;"
        ac = accessorCacheBack
        if ac is not None:
            DEBUG = DebugStatus
            #DEBUG = True
            pcs = ac.getCurrentPosCharStyle()
            if DEBUG:
                print "  is_identifier:: pcs: %r" % (pcs, )
            try:
                # Check that the preceding character before the identifier
                ppcs = ac.getPrecedingPosCharStyle(pcs[2],
                                                   ignore_styles=self.ignore_styles)
                if DEBUG:
                    print "  is_identifier:: ppcs: %r" % (ppcs, )
                if self.is_operator(ppcs[2]) and ppcs[1] in "{;":
                    return True
                elif ppcs[2] == SCE_UDL_M_STRING and \
                     self._is_html_style_attribute(ac, ppcs[2]):
                    return True
                if DEBUG:
                    print "  is_identifier:: Not an identifier style"
            finally:
                # Reset the accessor back to the current position
                ac.resetToPosition(pcs[0])
        return False

    def is_class(self, style, accessorCacheBack=None):
        ac = accessorCacheBack
        if ac is not None:
            pcs = ac.getCurrentPosCharStyle()
            print "  is_class:: pcs: %r" % (pcs, )
            if self.is_operator(pcs[2]) and pcs[1] in ">.;}{":
                return True
            try:
                DEBUG = DebugStatus
                # Check that the preceding character before the identifier is a "."
                ppcs = ac.getPrecedingPosCharStyle(pcs[2],
                                                   ignore_styles=self.ignore_styles)
                if DEBUG:
                    print "  is_class:: ppcs: %r" % (ppcs, )
                if ppcs[2] in self.identifier_styles:
                    ppcs = ac.getPrecedingPosCharStyle(ppcs[2],
                                                       ignore_styles=self.ignore_styles)
                    if self.is_operator(ppcs[2]) and ppcs[1] == ".":
                        return True
                    elif not is_udl_css_style(ppcs[2]):
                        return True
                # If there is no identifer, may be operator, which is okay
                elif not is_udl_css_style(ppcs[2]) or \
                     (self.is_operator(ppcs[2]) and ppcs[1] in "};"):
                    return True
                if DEBUG:
                    print "  is_class:: Not a class style"
            finally:
                # Reset the accessor back to the current position
                ac.resetToPosition(pcs[0])
        return False

    def is_tag(self, style, accessorCacheBack=None):
        ac = accessorCacheBack
        if ac is not None:
            # Tags follow operators or other tags
            # For use, we'll go back until we find an operator in "}>"
            if style in self.identifier_styles:
                DEBUG = DebugStatus
                p, ch, style = ac.getCurrentPosCharStyle()
                start_p = p
                min_p = max(0, p - 50)
                try:
                    while p > min_p:
                        # Check that the preceding character before the identifier is a "."
                        p, ch, style = ac.getPrecedingPosCharStyle(style,
                                            ignore_styles=self.ignore_styles)
                        if style in self.operator_styles:
                            # Thats good, we get our decision now
                            if ch in "}>":
                                return True
                            elif ch == ",":
                                # Might be following another tag, "div, div",
                                # http://bugs.activestate.com/show_bug.cgi?id=58637
                                continue
                            if DEBUG:
                                print "  is_tag:: Not a tag operator ch: %s" % (ch)
                            return False
                        elif not self.is_css_style(style):
                            if DEBUG:
                                print "  is_tag:: Not a css style: %d, ch: %r" % (style, ch, )
                            if style == SCE_UDL_M_STRING and \
                               self._is_html_style_attribute(ac, style):
                                return False
                            return True
                        elif style not in self.identifier_styles:
                            if DEBUG:
                                print "  is_tag:: Not a tag style, style: %d" % (style)
                            return False
                        # else: # Thats okay, we'll keep going
                finally:
                    # Reset the accessor back to the current position
                    ac.resetToPosition(start_p)
        return False

    @property
    def default_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_DEFAULT, )

    @property
    def comment_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_COMMENT,)

    @property
    def string_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_STRING, )

    @property
    def operator_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_OPERATOR, )

    @property
    def identifier_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_IDENTIFIER,
                ScintillaConstants.SCE_UDL_CSS_WORD)

    @property
    def value_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_WORD,
                ScintillaConstants.SCE_UDL_CSS_IDENTIFIER,
                ScintillaConstants.SCE_UDL_CSS_NUMBER)

    @property
    def tag_styles(self):
        return (ScintillaConstants.SCE_CSS_TAG, )

    @property
    def number_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_NUMBER, )

    @property
    def ignore_styles(self):
        return (ScintillaConstants.SCE_UDL_CSS_DEFAULT,
                ScintillaConstants.SCE_UDL_CSS_COMMENT)


StraightCSSStyleClassifier = _StraightCSSStyleClassifier()
UDLCSSStyleClassifier      = _UDLCSSStyleClassifier()

class CSSLangIntel(CitadelLangIntel, ParenStyleCalltipIntelMixin):
    lang = "CSS"

    @LazyClassAttribute
    def CSS_ATTRIBUTES(self):
        # CSS attributes:
        #     key (string) is the css property (attribute) name
        #     value (list) is the possible css property (attribute) values
        from codeintel2 import constants_css3 as constants_css
        from codeintel2 import constants_css_microsoft_extensions
        from codeintel2 import constants_css_moz_extensions
        from codeintel2 import constants_css_webkit_extensions
        attrs = constants_css.CSS_ATTR_DICT.copy()
        attrs.update(constants_css_microsoft_extensions.CSS_MICROSOFT_SPECIFIC_ATTRS_DICT)
        attrs.update(constants_css_moz_extensions.CSS_MOZ_SPECIFIC_ATTRS_DICT)
        attrs.update(constants_css_webkit_extensions.CSS_WEBKIT_SPECIFIC_ATTRS_DICT)
        return attrs

    @LazyClassAttribute
    def CSS_PROPERTY_NAMES(self):
        # Setup the names triggered for "property-names"
        return sorted(self.CSS_ATTRIBUTES.keys(), key=OrdPunctLast)

    @LazyClassAttribute
    def CSS_PROPERTY_ATTRIBUTE_CALLTIPS_DICT(self):
        # Calltips for css property attributes
        from codeintel2 import constants_css3 as constants_css
        from codeintel2 import constants_css_microsoft_extensions
        from codeintel2 import constants_css_moz_extensions
        from codeintel2 import constants_css_webkit_extensions
        calltips = constants_css.CSS_PROPERTY_ATTRIBUTE_CALLTIPS_DICT.copy()
        calltips.update(constants_css_microsoft_extensions.CSS_MICROSOFT_SPECIFIC_CALLTIP_DICT)
        calltips.update(constants_css_moz_extensions.CSS_MOZ_SPECIFIC_CALLTIP_DICT)
        calltips.update(constants_css_webkit_extensions.CSS_WEBKIT_SPECIFIC_CALLTIP_DICT)
        return calltips

    @LazyClassAttribute
    def CSS_HTML_TAG_NAMES(self):
        # Tag names
        return sorted(Keywords.hypertext_elements.split())

    @LazyClassAttribute
    def CSS_PSEUDO_CLASS_NAMES(self):
        # pseudo-class-names
        from codeintel2 import constants_css3 as constants_css
        return sorted(constants_css.CSS_PSEUDO_CLASS_NAMES, key=OrdPunctLast)

    @LazyClassAttribute
    def CSS_AT_RULE_NAMES(self):
        # at rules

        return sorted(["import", "media", "charset", "font-face", "page", "namespace", "keyframes"],
                      key=OrdPunctLast)
    
    @LazyClassAttribute
    def SCSS_AT_RULE_NAMES(self):
        return sorted(self.CSS_AT_RULE_NAMES +
                      ["extend", "at-root", "debug", "warn", "error", "if",
                       "for", "each", "while", "mixin", "include",
                       "function"], key=OrdPunctLast)

    def preceding_trg_from_pos(self, buf, pos, curr_pos):
        DEBUG = DebugStatus # not using 'logging' system, because want to be fast
        #DEBUG = True # not using 'logging' system, because want to be fast

        if DEBUG:
            print "\npreceding_trg_from_pos -- pos: %d, curr_pos: %d" % (
                    pos, curr_pos, )
        if isinstance(buf, UDLBuffer):
            styleClassifier = UDLCSSStyleClassifier
        else:
            styleClassifier = StraightCSSStyleClassifier
        ac = AccessorCache(buf.accessor, curr_pos+1, fetchsize=50)
        currTrg = self._trg_from_pos(buf, (curr_pos == pos) and pos or pos+1,
                                     implicit=False, DEBUG=DEBUG,
                                     ac=ac, styleClassifier=styleClassifier)
        if DEBUG:
            print "  currTrg: %r" % (currTrg, )

        # If we're not looking for a previous trigger, or else the current
        # trigger position is for a calltip, then do not look any further.
        if (pos == curr_pos) or (currTrg and currTrg.form == TRG_FORM_CALLTIP):
            return currTrg
        # Else, work our way backwards from pos.

        ac.resetToPosition(pos+1)
        p, ch, style = ac.getPrevPosCharStyle()
        if DEBUG:
            print "  preceding_trg_from_pos: p: %r, ch: %r, style: %r" % (p, ch, style)
        min_p = max(0, p - 200)
        ignore_styles = styleClassifier.comment_styles + \
                        styleClassifier.string_styles + \
                        styleClassifier.number_styles
        while p > min_p and styleClassifier.is_css_style(style):
            p, ch, style = ac.getPrecedingPosCharStyle(style, ignore_styles=ignore_styles, max_look_back=100)
            if DEBUG:
                print "  preceding_trg_from_pos: Trying preceding p: %r, ch: %r, style: %r" % (p, ch, style)
            if ch and (isident(ch) or ch in ":( \t"):
                trg = self._trg_from_pos(buf, p+1, implicit=False, DEBUG=DEBUG,
                                         ac=ac, styleClassifier=styleClassifier)
                if trg is not None:
                    if DEBUG:
                        print "trg: %r" % (trg, )
                    if currTrg is not None:
                        if currTrg.type != trg.type:
                            if DEBUG:
                                print "  Next trigger is a different type, ending search"
                            return None
                        elif currTrg.form != trg.form:
                            return trg
                        elif DEBUG:
                            print "  Found same trigger again, continuing " \
                                  "looking for a different trigger"
                    else:
                        return trg
        return None

    def _trg_from_pos(self, buf, pos, implicit=True, DEBUG=False, ac=None, styleClassifier=None):
        #DEBUG = True # not using 'logging' system, because want to be fast
        if DEBUG:
            print "\n----- CSS _trg_from_pos(pos=%r, implicit=%r) -----"\
                  % (pos, implicit)
        try:
            if pos == 0:
                return None

            if ac is None:
                ac = AccessorCache(buf.accessor, pos, fetchsize=50)
            else:
                ac.resetToPosition(pos)
            # Ensure this variable is initialized as False, it is used by UDL
            # for checking if the css style is inside of a html tag, example:
            #   <p style="mycss: value;" />
            # When it's found that it is such a case, this value is set True
            ac.is_html_style_attribute = False

            last_pos, last_char, last_style = ac.getPrevPosCharStyle()
            if DEBUG:
                print "  _trg_from_pos:: last_pos: %s" % last_pos
                print "  last_char: %r" % last_char
                print "  last_style: %s" % last_style
    
            # The easy ones are triggering after any of '#.[: '.
            # For speed, let's get the common ' ' out of the way. The only
            # trigger on space is 'complete-property-values'.

            if styleClassifier.is_default(last_style):
                if DEBUG:
                    print "  _trg_from_pos:: Default style: %d, ch: %r" % (last_style, last_char)
                # Move backwards resolving ambiguity, default on "property-values"
                min_pos = max(0, pos - 200)
                while last_pos > min_pos:
                    last_pos, last_char, last_style = ac.getPrevPosCharStyle()
                    if styleClassifier.is_operator(last_style, ac) or styleClassifier.is_value(last_style, ac):
                        if DEBUG:
                            print " _trg_from_pos: space => property-values"
                        return Trigger("CSS", TRG_FORM_CPLN, "property-values",
                                       pos, implicit)
                    elif styleClassifier.is_tag(last_style, ac):
                        if DEBUG:
                            print " _trg_from_pos: space => tag-names"
                        return Trigger("CSS", TRG_FORM_CPLN, "tag-names",
                               pos, implicit)
                    elif styleClassifier.is_identifier(last_style, ac):
                        if DEBUG:
                            print " _trg_from_pos: space => property-names"
                        return Trigger("CSS", TRG_FORM_CPLN, "property-names",
                               pos, implicit)
                if DEBUG:
                    print " _trg_from_pos: couldn't resolve space, settling on property-names"
                return Trigger("CSS", TRG_FORM_CPLN, "property-values",
                                   pos, implicit)

            elif styleClassifier.is_operator(last_style, ac):
                # anchors
                if DEBUG:
                    print "  _trg_from_pos:: OPERATOR style"
                if last_char == '#':
                    return Trigger("CSS", TRG_FORM_CPLN, "anchors",
                                   pos, implicit)

                elif last_char == ':':
                    try:
                        p, ch, style = ac.getPrevPosCharStyle(ignore_styles=styleClassifier.ignore_styles)
                        if DEBUG:
                            print "  _trg_from_pos:: Looking at p: %d, ch: %r, style: %d" % (p, ch, style)
                    except IndexError:
                        style = None
                    if DEBUG:
                        print "  _trg_from_pos:: style: %r" % (style)
                    if style is None or \
                       not styleClassifier.is_identifier(style, ac):
                    #if style is None or \
                    #   not styleClassifier.is_css_style(style) or \
                    #   styleClassifier.is_class(style, ac):
                        # complete for pseudo-class-names
                        return Trigger("CSS", TRG_FORM_CPLN, "pseudo-class-names",
                                       pos, implicit)
                    else:
                    #if styleClassifier.is_identifier(style, ac):
                        # calltip for property-values
                        return Trigger("CSS", TRG_FORM_CALLTIP, "property-values",
                                       pos, implicit)

                # class-names
                elif last_char == '.':
                    return Trigger("CSS", TRG_FORM_CPLN, "class-names",
                                   pos, implicit)

                # at-rule
                elif last_char == '@':
                    #p, ch, style = ac.getPrevPosCharStyle(ignore_styles=styleClassifier.comment_styles)
                    # XXX - Should check not beyond first rule set
                    #     - Should check not within a rule block.
                    return Trigger("CSS", TRG_FORM_CPLN, "at-rule",
                                   pos, implicit)

                elif last_char == '/':
                    try:
                        p, ch, style = ac.getPrevPosCharStyle()
                    except IndexError:
                        pass
                    else:
                        if ch == "<":
                            # Looks like start of closing '</style>'
                            # tag. While typing this the styling will
                            # still be in the CSS range.
                            return Trigger(buf.m_lang, TRG_FORM_CPLN,
                                           "end-tag", pos, implicit)

            # tag-names
            elif styleClassifier.is_tag(last_style, ac):
                # We trigger on tag names of specified length >= 1 char
                if DEBUG:
                    print "  _trg_from_pos:: TAG style"
                p, ch, style = last_pos, last_char, last_style
                try:
                    while p >= 0:
                        if DEBUG:
                            print "  _trg_from_pos:: Looking at p: %d, ch: %r, style: %d" % (p, ch, style)
                        if not isident(ch):
                            p += 1
                            break
                        elif style != last_style:
                            if DEBUG:
                                print "  _trg_from_pos:: Current style is not a tag: %d" % (style)
                            return None
                        p, ch, style = ac.getPrevPosCharStyle()
                except IndexError:
                    p = 0
                return Trigger("CSS", TRG_FORM_CPLN, "tag-names",
                               p, implicit)

            elif styleClassifier.is_identifier(last_style, ac):
                if DEBUG:
                    print "  _trg_from_pos:: IDENTIFIER style"
                # property-names
                #print "here", accessor.text_range(0, pos)
                # We trigger on identifier names with any length >= 1 char
                pos = last_pos
                while pos >= 0:
                    pos, ch, style = ac.getPrevPosCharStyle()
                    if not isident(ch):
                        break
                    elif style != last_style:
                        return None
                extentLength = last_pos - pos
                # cover ": " following the identifier if it's there (since we
                # add it to the autocomplete in _async_eval_at_trg)
                following_text = ac.text_range(last_pos + 1, last_pos + 3)
                for idx, char in enumerate(": "):
                    try:
                        if following_text[idx] == char:
                            extentLength += 1
                        else:
                            break
                    except IndexError:
                        break
                return Trigger("CSS", TRG_FORM_CPLN, "property-names",
                               pos+1, implicit, extentLength=extentLength)

            elif styleClassifier.is_value(last_style, ac):
                p, ch, style = ac.getPrevPosCharStyle(ignore_styles=styleClassifier.comment_styles)
                if DEBUG:
                    print "  _trg_from_pos:: VALUE style"
                    print "  _trg_from_pos::   p: %s" % p
                    print "  _trg_from_pos::   ch: %r" % ch
                    print "  _trg_from_pos::   style: %s" % style
                    ac.dump()
                # Implicit triggering only happens on a whitespace character
                # after any one of these ":,%) " characters
                # Note: last_char can be a value style yet also be whitespace
                #       in straight CSS.
                if last_char in WHITESPACE:
                    return Trigger("CSS", TRG_FORM_CPLN, "property-values",
                                   last_pos+1, implicit)
                elif ch in WHITESPACE or ch in ":,%)":
                    # Check to ensure this is not a pseudo-class! Bug:
                    #   http://bugs.activestate.com/show_bug.cgi?id=71073
                    if ch == ":":
                        # Last style must be an identifier then!
                        pp, pch, pstyle = ac.getPrevPosCharStyle(
                                ignore_styles=styleClassifier.ignore_styles)
                        if DEBUG:
                            print "pp: %d, pch: %r, pstyle: %d" % (pp, pch,
                                                                   pstyle)
                        if not styleClassifier.is_identifier(pstyle, ac):
                            # This is likely a pseudo-class definition then,
                            # no trigger here.
                            if DEBUG:
                                print "pseudo-class style found, no trigger."
                            return None
                    return Trigger("CSS", TRG_FORM_CPLN, "property-values",
                                   p+1, implicit)
                # For explicit, we can also be inside a property already
                if not implicit and isident(ch):
                    # If there is already part of a value there, we need to move
                    # the trigger point "p" to the start of the value.
                    while isident(ch):
                        p, ch, style = ac.getPrevPosCharStyle()
                    return Trigger("CSS", TRG_FORM_CPLN, "property-values",
                                   p+1, implicit)
                return None

            elif DEBUG:
                print "  _trg_from_pos:: Unexpected style: %d, ch: %r" % (last_style, last_char)

            # XXX "at-property-names" - Might be used later
            #elif last_style == SCE_CSS_DIRECTIVE:
            #    # property-names
            #    # We trigger on identifier names with length == 3
            #    #print "here", accessor.text_range(0, pos)
            #    if pos >= 4 and accessor.char_at_pos(pos - 4) == ' ' and \
            #       self._is_ident_of_length(accessor, pos, length=3):
            #        # We are good for completion
            #        if DEBUG:
            #            print "Got a trigger for 'at-property-names'"
            #        return Trigger("CSS", TRG_FORM_CPLN, "at-property-names",
            #                       pos-3, implicit)

        except IndexError:
            # Wen't out of range of buffer before we found anything useful
            pass

        if DEBUG:
            print "----- CSS trg_from_pos() -----"
        return None

    def trg_from_pos(self, buf, pos, implicit=True, ac=None):
        DEBUG = DebugStatus # not using 'logging' system, because want to be fast
        if isinstance(buf, UDLBuffer):
            # This is CSS content in a multi-lang buffer.
            return self._trg_from_pos(buf, pos, implicit, DEBUG, ac, UDLCSSStyleClassifier)
        else:
            return self._trg_from_pos(buf, pos, implicit, DEBUG, ac, StraightCSSStyleClassifier)

    def _async_eval_at_trg(self, buf, trg, ctlr, styleClassifier):
        # Note: Currently this is NOT asynchronous. I believe that is fine
        # as long as evaluation is fast -- because the IDE UI thread could
        # be blocked on this. If processing might be slow (e.g. scanning
        # a number of project files for appropriate anchors, etc.), then
        # this should be made asynchronous.
        DEBUG = DebugStatus
        #DEBUG = True
        if DEBUG:
            print "\n----- async_eval_at_trg(trg=%r) -----"\
                  % (trg)

        # Setup the AccessorCache
        extra = trg.extra
        ac = None
        #print "Extra: %r" % (extra)
        if isinstance(extra, dict):
            extra = extra.get("extra", None)
            if isinstance(extra, dict):
                ac = extra.get("ac", None)
                if ac and DEBUG:
                    print "  _async_eval_at_trg:: Trigger had existing AC"
                    ac.dump()
        if ac is None:
            if DEBUG:
                print "  _async_eval_at_trg:: Created new trigger!"
            ac = AccessorCache(buf.accessor, trg.pos, fetchsize=20)

        ctlr.start(buf, trg)
        pos = trg.pos

        try:
            if trg.id == ("CSS", TRG_FORM_CPLN, "tag-names"):
                if DEBUG:
                    print "  _async_eval_at_trg:: 'tag-names'"
                cplns = self.CSS_HTML_TAG_NAMES
                if DEBUG:
                    print "  _async_eval_at_trg:: cplns:", cplns
                if cplns:
                    ctlr.set_cplns( [ ("element", v) for v in cplns ] )
                ctlr.done("success")
            elif trg.id == ("CSS", TRG_FORM_CPLN, "anchors") or \
                 trg.id == ("CSS", TRG_FORM_CPLN, "class-names"):
                cplns = set() # prevent duplicates (#foo, #foo:hover, etc.).
                ilk = CSSSelector.ID if trg.id[2] == "anchors" else \
                      CSSSelector.CLASS
                
                # Autocomplete anchors or class names from the current file.
                if self.lang in buf.blob_from_lang:
                    blob = buf.blob_from_lang[self.lang]
                    for elem in blob:
                        if elem.get("ilk") == ilk:
                            cplns.add((ilk, elem.get("target")))
                
                # Autocomplete anchors or class names from all CSS files in the
                # project or the current directory and its sub-directories.
                cwd = buf.env and buf.env.get_proj_base_dir() or dirname(buf.path)
                if cwd != "<Unsaved>":
                    import_handler = \
                        self.mgr.citadel.import_handler_from_lang(trg.lang)
                    importables = import_handler.find_importables_in_dir(cwd, buf.env)
                    files = [i[0] for i in importables.values()]
                    for file in files:
                        try:
                            buf = self.mgr.buf_from_path(file, lang=trg.lang)
                            blob = buf.blob_from_lang[trg.lang]
                            for elem in blob:
                                if elem.get("ilk") == ilk:
                                    cplns.add((ilk, elem.get("target")))
                        except (EnvironmentError, CodeIntelError), ex:
                            # This can occur if the path does not exist, such as
                            # a broken symlink, or we don't have permission to
                            # read the file, or the file does not contain text.
                            continue                            
                
                # Sort and return completions.
                if cplns:
                    ctlr.set_cplns(sorted([v for v in cplns]))
                ctlr.done("success")
            elif trg.id == ("CSS", TRG_FORM_CPLN, "property-names"):
                cplns = self.CSS_PROPERTY_NAMES
                if cplns:
                    # Note: we add the colon as well - see bug 89913.
                    ctlr.set_cplns( [ ("property", v + ": ") for v in cplns ] )
                    # We want to show the property values after autocompleting.
                    trg.retriggerOnCompletion = True
                    #print "  _async_eval_at_trg:: cplns:", cplns
                ctlr.done("success")
            elif trg.id == ("CSS", TRG_FORM_CALLTIP, "property-values"):
                property, v1, v2 \
                    = self._extract_css_declaration(ac, styleClassifier, trg,
                                                    is_for_calltip=True)
                if DEBUG:
                    print "  _async_eval_at_trg:: Property name: %r" % \
                            (property, )
                try:
                    calltip = self.CSS_PROPERTY_ATTRIBUTE_CALLTIPS_DICT[property]
                    if DEBUG:
                        print "  _async_eval_at_trg:: calltip:", calltip
                    ctlr.set_calltips([calltip])
                except KeyError:
                    #print "Unknown CSS property: '%s'" % (property)
                    pass    # Ignore unknown CSS attributes
                ctlr.done("success")
            elif trg.id == ("CSS", TRG_FORM_CPLN, "property-values"):
                property, current_value, values \
                    = self._extract_css_declaration(ac, styleClassifier, trg)
                if DEBUG:
                    print "  _async_eval_at_trg:: XXX property: %r, " \
                          " current_value: %r, values: %r" % (property,
                                                              current_value,
                                                              values)
                try:
                    #print "\ndict:", self.CSS_ATTRIBUTES[property]
                    property_values = sorted(self.CSS_ATTRIBUTES[property],
                                             key=OrdPunctLast)
                    # Check if it matches anything, if not, dismiss the list
                    if current_value:
                        clen = len(current_value)
                        for v in property_values:
                            if clen <= len(v) and current_value == v[:clen]:
                                # Found a match
                                break
                        # Else, return the full list, even though no match made
                        # XXX - May want to cancel the CC list, any way to do this?
                    cplns = [("value", v)
                             for v in property_values
                             if v not in values or v == current_value]
                    ctlr.set_cplns(cplns)
                except KeyError:
                    if DEBUG: 
                        print "  _async_eval_at_trg:: Unknown CSS property: "\
                              "'%s'" % (property)
                    pass    # Ignore unknown CSS attributes
                ctlr.done("success")
    
                #XXX Handling for property not in list.
            elif trg.id == ("CSS", TRG_FORM_CPLN, "pseudo-class-names"):
                cplns = [("pseudo-class", v)
                         for v in self.CSS_PSEUDO_CLASS_NAMES]
                ctlr.set_cplns(cplns)
                ctlr.done("success")
            elif trg.id == ("CSS", TRG_FORM_CPLN, "at-rule"):
                if self.lang != "SCSS" and self.lang != "Sass":
                    cplns = [("rule", v)
                             for v in self.CSS_AT_RULE_NAMES]
                else:
                    cplns = [("rule", v)
                             for v in self.SCSS_AT_RULE_NAMES]
                ctlr.set_cplns(cplns)
                ctlr.done("success")
    
            # Punt - Lower priority
            #elif trg.id == ("CSS", TRG_FORM_CPLN, "units"):
    
            # Punt - Fancy
            #elif trg.id == ("CSS", TRG_FORM_CPLN, "import-url"):
    
            # Punt - uncommon
            #elif trg.id == ("CSS", TRG_FORM_CPLN, "attr-names"):
            #elif trg.id == ("CSS", TRG_FORM_CPLN, "attr-values"):
    
            else:
                raise NotImplementedError("not yet implemented: completion for "
                                          "most css triggers: trg.id: %s" % (trg.id,))
        except IndexError:
            # Tried to go out of range of buffer, nothing appropriate found
            if DEBUG:
                print "  _async_eval_at_trg:: ** Out of range error **"
            ctlr.done("success")

    def async_eval_at_trg(self, buf, trg, ctlr):
        if isinstance(buf, UDLBuffer):
            # This is CSS content in a multi-lang buffer.
            return self._async_eval_at_trg(buf, trg, ctlr,
                                           UDLCSSStyleClassifier)
        else:
            return self._async_eval_at_trg(buf, trg, ctlr,
                                           StraightCSSStyleClassifier)

    def _get_all_anchors_names_in_project(self):
        #anchors = []
        #pos = 0
        #LENGTH = accessor.length
        #style = 0
        #func_style_at_pos = accessor.style_at_pos
        #func_char_at_pos = accessor.char_at_pos
        #while pos < LENGTH:
        #    if func_char_at_pos(pos) == '#' and \
        #       func_style_at_pos(pos) == SCE_CSS_OPERATOR:
        #        # Likely an anchor
        #        pass
        #    pos += 1
        #return anchors
        return []

    def _is_ident_of_length(self, accessor, pos, length=3):
        # Fourth char to left should not be an identifier
        if pos > length and isident(accessor.char_at_pos((pos - length) - 1)):
            return False
        # chars to left should all be identifiers
        for i in range(pos - 1, (pos - length) -1, -1):
            if not isident(accessor.char_at_pos(i)):
                return False
        return True

    def _extract_css_declaration(self, ac, styleClassifier, trg,
                                 is_for_calltip=False):
        """Extract the CSS declaration around the given position.

        Returns a 3-tuple:
            (<property>, <current_value>, <value_list>)

        If is_for_calltip is true, we do not bother to parse out the values, so
        <current_value> and <value_list> will be empty.

        The value gets parsed into <value_list>, a list of individual values.
        Comments and strings are striped from the return value.

        If the <current_value> is '', then the trigger position is
        ready to start a new value.
        """
        DEBUG = DebugStatus
        #DEBUG = True
        #PERF: Use accessor.gen_chars_and_styles() if possible.
        try:
            ac.resetToPosition(trg.pos)
            p, ch, style = ac.getPrevPosCharStyle()
            if not styleClassifier.is_operator(style, ac):
                if DEBUG:
                    print "Current ch is not an operator, so getting the " \
                          "preceeding one, p: %d, ch: %r, style: %d" % \
                          (p, ch, style, )
                p, ch, style = ac.getPrevPosCharStyle(
                                    ignore_styles=styleClassifier.ignore_styles)
        except IndexError:
            # This occurs when already at the end of the buffer, so we reset to
            # the last buffer position then
            ac.resetToPosition(trg.pos - 1)
            p, ch, style = ac.getCurrentPosCharStyle()
        if DEBUG:
            print """------ _extract_css_declaration -----"""
            print "  _extract_css_declaration:: Trg.pos: %d" % (trg.pos)
            #ac._debug = True
            print "  _extract_css_declaration:: pos: %r" % (p)
            print "  _extract_css_declaration:: ch: %r" % (ch)
            print "  _extract_css_declaration:: style: %r" % (style)
            ac.dump()
        # Walk back to ':' operator.
        num_close_parenthesis = 0
        min_pos = max(0, trg.pos - 200)  # Lookback up to 200 chars in total
        while p >= min_pos:
            #print "ch: %r, style: %d" % (ch, style, )
            if ch == ':' and styleClassifier.is_operator(style, ac):
                break
            elif num_close_parenthesis > 0:
                if ch == "(":
                    num_close_parenthesis -= 1
                    if DEBUG:
                        print "Found matching open paren," \
                              " num_close_parenthesis now: %d" % (
                                    num_close_parenthesis)
                elif DEBUG:
                    print "Ignoring everything inside the parenthesis"
            elif ch == "(" and (styleClassifier.is_operator(style) or
                                styleClassifier.is_value(style)):
                if DEBUG:
                    print "Already inside a paren, no cpln's then."
                    #XXX SCSS and Less support arithmetic expressions
                return (None, None, None)
            elif ch == ")" and (styleClassifier.is_operator(style) or
                                styleClassifier.is_value(style)):
                num_close_parenthesis += 1
                if DEBUG:
                    print "Found close paren, need to skip over contents," \
                          " num_close_parenthesis: %d" % (
                                num_close_parenthesis)
            elif styleClassifier.is_operator(style):
                if ch not in ":,%":
                    if DEBUG:
                        print "%s: couldn't find ':' operator, found invalid " \
                              "operator: %d %r %d" % (trg.name, p, ch, style)
                    #TODO: SCSS and Less support arithmetic expressions
                    return (None, None, None)
            elif styleClassifier.is_string(style):
                # Used to skip over string items in property values
                if DEBUG:
                    print "Found string style, ignoring it"
            elif not (styleClassifier.is_value(style) or styleClassifier.is_default(style)):
                # old CSS lexer: everything betwee ":" and ';' used to be a value.
                if DEBUG:
                    print "%s: couldn't find ':' operator, found invalid " \
                          "style: pcs: %d %r %d" % (trg.name, p, ch, style)
                return (None, None, None)
            p, ch, style = ac.getPrevPosCharStyle(
                                    ignore_styles=styleClassifier.ignore_styles)
        else:
            if DEBUG:
                print "%s: couldn't find ':' operator within 200 chars, " \
                      "giving up" % (trg.name)
            return (None, None, None)

        if DEBUG:
            print "  _extract_css_declaration:: Found ':' at pos: %d" % (p)
        # Parse out the property name.
        colan_pos = p
        p, ch, style = ac.getPrecedingPosCharStyle(style,
                                    ignore_styles=styleClassifier.ignore_styles,
                                    max_look_back=150)
        if style not in styleClassifier.identifier_styles:
            if DEBUG:
                print "  _extract_css_declaration:: No identifier style found" \
                      " before ':', found style %d instead" % (style)
            return (None, None, None)
        p, property = ac.getTextBackWithStyle(style)
        property = property.strip()

        if is_for_calltip:
            # We have all the info we need
            if DEBUG:
                print "  _extract_css_declaration:: Returning property: %r" % (
                            property)
            return (property, '', [])

        # Walk forward parsing the value information, ends when we hit a ";" or
        # have gone ahead a maximum of 200 chars.
        ac.resetToPosition(colan_pos)
        prev_pos, prev_ch, prev_style = ac.getCurrentPosCharStyle()
        from_pos = prev_pos
        p = colan_pos
        # Value info, list of tuples (pos, text)
        value_info = []
        max_p = p + 200
        try:
            while p < max_p:
                p, ch, style = ac.getNextPosCharStyle(max_look_ahead=100, ignore_styles=styleClassifier.comment_styles)
                if p is None or not styleClassifier.is_css_style(style):
                    # Went past max_look_ahead, just use what we've got then
                    if DEBUG:
                        print "%s: css value reached max length or end of " \
                              "document: trg.pos %d" % (trg.name, trg.pos)
                    value_info.append((from_pos, ac.text_range(from_pos, p)))
                    break
    
                # Sass test
                if ch == "\n" and self.lang == "Sass" and styleClassifier.is_default(style):
                    value_info.append((from_pos, ac.text_range(from_pos, p)))
                    break
                if ch in WHITESPACE or styleClassifier.is_string(style):
                    if not prev_ch in WHITESPACE and not styleClassifier.is_string(prev_style):
                        value_info.append((from_pos, ac.text_range(from_pos, p)))
                    from_pos = p+1
                elif styleClassifier.is_operator(style):
                    if ch in ";{}":
                        value_info.append((from_pos, ac.text_range(from_pos, p)))
                        break
                    # Other chars should be okay to collect
                elif not styleClassifier.is_value(style) and \
                     style not in styleClassifier.ignore_styles:
                    if DEBUG:
                        print "%s: invalid style found: pos %d, style: %d" % (
                                 trg.name, trg.pos, style)
                    return (None, None, None)
                prev_pos, prev_ch, prev_style = p, ch, style
            else:
                if DEBUG:
                    print "%s: css value too long: trg.pos %d" % (trg.name, trg.pos)
                return (None, None, None)
        except IndexError:
            if DEBUG:
                print "ran out of buffer"

        # Work out the values and the current value
        current_value = None
        values = []
        trg_pos = trg.pos
        for p, value in value_info:
            if value and _isident_first_char(value[0]):
                if DEBUG:
                    print "Is a valid value, p: %d, value: %r" % (p, value, )
                values.append(value)
                if current_value is None and trg_pos >= p and \
                   trg_pos <= p + len(value):
                    current_value = value

        if DEBUG:
            print "  _extract_css_declaration:: Returning property: %r, " \
                  "current_value: %r, values: %r" % (property, current_value,
                                                     values)
        return (property, current_value, values)


class CSSBuffer(CitadelBuffer):
    lang = "CSS"
    sce_prefixes = ["SCE_CSS_"]
    # Removed '(' - double braces for completions that contain a '(' (bug 80063)
    # Removed '.' - conflict with floating point values: .5em (bug 80126)
    # Removed '{' - gets in way of "rule {" style declarations (bug 82358)
    # Removed '#' - gets in the way of hex colors and id selectors (bug 82968)
    # Removed '>' - gets in the way of child selectors (bug 87403)
    cpln_fillup_chars = " '\";},/"
    cpln_stop_chars = " ('\";:{},.>/"


class CSSImportHandler(ImportHandler):
    """
    Handles finding CSS files contained within a particular directory.
    """
    sep = '/'

    def find_importables_in_dir(self, dir, env=None):
        """
        Returns all CSS files in the given directory and its sub-directories.
        The return value is a dictionary of the form:
            {'/path/to/css/file1': ('/path/to/css/file1', None, False),
             '/path/to/css/file2': ('/path/to/css/file2', None, False),
             ...}
        """
        from os.path import join
        from fnmatch import fnmatch

        if dir == "<Unsaved>":
            return {}

        # Update environment if null
        if not env:
            env = self.mgr.env

        importables = {}
        patterns = env.assoc_patterns_from_lang("CSS")

        # Get excludes
        excluded_dir_names = set()
        excluded_names = set()
        for excludes_pref in env.get_all_prefs("import_exclude_matches"):
            if excludes_pref:
                for exclude_item in excludes_pref.split(";"):
                    # Add to main exclude list
                    if exclude_item not in excluded_names:
                            excluded_names.add(exclude_item)
                    # Exclude anything with a star or a dot (likely a file)
                    if not re.search("[.*]", exclude_item):
                        if exclude_item not in excluded_dir_names:
                            excluded_dir_names.add(exclude_item)

        # Get depth
        max_depth = env.get_pref("codeintel_max_recursive_dir_depth", 10)
        if not max_depth:
            max_depth = 10

        # Get directories
        dirs = gen_dirs_under_dirs([dir],
            max_depth=max_depth,
            interesting_file_patterns=["*"],
            exclude_dirs=[],
            excluded_dir_names=excluded_dir_names
        )
        for dirpath in dirs:
            for name in os.listdir(dirpath):
                # Does it look like a CSS filename?
                for pattern in patterns:
                    if fnmatch(name, pattern):
                        # Check if excluded
                        excluded = False
                        for excluded_name in excluded_names:
                            if fnmatch(name, excluded_name):
                                excluded = True
                                break
                        if excluded:
                            break

                        # Add to list
                        name = join(dirpath, name)
                        importables[name] = (name, None, False)
                        break
        return importables

class CSSCILEDriver(CILEDriver):
    """
    Invokes the CSS Code Intelligence Language Engine (CILE) on CSS content.
    """
    lang = lang
    
    def scan_purelang(self, buf):
        """
        Scans a CSS-only buffer, returning its CIX representation.
        @param buf The buffer to scan.
        """
        cile = CSSCile(buf.path, self.lang)
        cile.scan_purelang(buf.accessor.text)
        cixroot = createCixRoot()
        cile.appendToCixRoot(cixroot)
        return cixroot
    
    def scan_css_tokens(self, file_elem, blob_name, css_tokens):
        """
        Scans the given CSS tokens parsed from a multi-language buffer,
        appending that CSS's CIX representation to the given CixFile.
        @param file_elem The CixFile to append CIX entries to.
        @param blob_name The filepath to use for CIX entries.
        @param css_tokens The CSS tokens parsed from a multi-language buffer.
        """
        cixmodule = createCixModule(file_elem, blob_name, self.lang,
                                    src=file_elem.get("path"))
        cile = CSSCile(blob_name, self.lang, UDLCSSStyleClassifier)
        for css_token in css_tokens:
            cile.handle_token(**css_token)
        cile.appendToCixModule(cixmodule)
    
class CSSSelector:
    """
    A CILE object that represents a CSS selector.
    """
    ELEMENT = "element"
    CLASS = "class"
    ID = "id"
    # Matcher for multiple ID and Class names in a selector.
    CSS_ID_OR_CLASS = re.compile('([#.][A-Za-z0-9_-]+)(::?[A-Za-z-]+(\\([^\\)]+\\))?|(?:\\[[^\\]]+\\])+)?')
    # Matcher for a single ID or Class name in a selector (at its end).
    CSS_IDENT = re.compile('([#.]?[A-Za-z0-9_-]+|&)(::?[A-Za-z-]+(\\([^\\)]+\\))?|(?:\\[[^\\]]+\\])+)?$')
    
    def __init__(self, text, line, target):
        """
        Creates a new representation for a CSS selector with the given text
        and type that occurs on the given line number.
        @param text The text of the CSS selector
        @param line The line number the CSS selector starts on.
        @param target The element, class, or id (i.e. target) of the CSS
               selector. This is the symbol used for autocompletions.
        """
        self.text = text
        self.line = line
        
        self.type = self.ELEMENT
        if target.startswith('#'):
            self.type = self.ID
            self.target = target[1:]
        elif target.startswith('.'):
            self.type = self.CLASS
            self.target = target[1:]
        else:
            self.target = target
        
        self.variables = [] # Less/SCSS only
        
    def addVariable(self, variable):
        """
        Adds the given CSSVariable to this CSS selector.
        This is only applicable in Less/SCSS.
        @param variable The CSSVariable to add.
        """
        self.variables.append(variable)
        
    def appendElementTreeTo(self, cixmodule):
        """
        Creates a CIX representation of this selector and appends it to the
        given CixModule.
        @param cixmodule The CixModule to append the CIX representation of this
               selector to.
        """
        cixobject = SubElement(cixmodule, "scope", ilk=self.type, name=self.text)
        cixobject.attrib["target"] = self.target
        cixobject.attrib["line"] = str(self.line + 1)
        
        for variable in self.variables:
            variable.appendElementTreeTo(cixobject)

class CSSVariable:
    """
    A CILE object that represents a Less/SCSS variable.
    """
    def __init__(self, text, line):
        """
        Creates a new representation for a Less/SCSS variable with the given
        name that occurs on the given line number.
        @param text The name of the Less/SCSS variable, including tht leading
               '@' or '$'.
        @param line The line number the variable starts on.
        """
        self.text = text
        self.name = text[1:]
        self.line = line
        
    def appendElementTreeTo(self, cixmodule):
        """
        Creates a CIX representation of this Less/SCSS variable and appends it
        to the given CixModule.
        @param cixmodule The CixModule to append the CIX representation of this
               variable to.
        """
        cixobject = SubElement(cixmodule, "variable", name=self.text)
        cixobject.attrib["name"] = self.name
        cixobject.attrib["line"] = str(self.line + 1)

class CSSFile:
    """
    A CILE object that represents a CSS file.
    """
    
    def __init__(self, path, lang):
        """
        Creates a new representation for the CSS file with the given path.
        @param path The path to the CSS file.
        """
        self.path = path
        self.lang = lang
        self.name = os.path.basename(path)
        self.parent = None
        self.cixname = self.__class__.__name__[2:].lower()
        
        # Selectors defined within this file.
        self.elements = {}
        self.classes = {}
        self.ids = {}
        # Less or SCSS variables defined within this file.
        self.variables = {}
        
    def addSelector(self, selector):
        """
        Adds the given CSSSelector to this CSS file.
        @param selector The CSSSelector to add.
        """
        if selector.type == CSSSelector.ELEMENT:
            self.elements[selector.text] = selector
        elif selector.type == CSSSelector.CLASS:
            self.classes[selector.text] = selector
        elif selector.type == CSSSelector.ID:
            self.ids[selector.text] = selector
        
    def addVariable(self, variable):
        """
        Adds the given CSSVariable to this CSS file.
        @param variable The CSSVariable to add.
        """
        self.variables[variable.text] = variable
        
    def appendCixToModule(self, cixmodule):
        """
        Appends the individual CixElements to the given CixModule.
        @param cixmodule The CixModule to append the CIX representation of this
               file to.
        """
        for v in sorted(self.elements.values() + self.classes.values() +
                        self.ids.values() + self.variables.values(),
                        key=operator.attrgetter("line", "text")):
            v.appendElementTreeTo(cixmodule)
        
    def appendCixFileTo(self, cixtree):
        """
        Creates a CixFile representation of this file and appends it to the
        given CixRoot.
        @param cixtree The CixRoot to append the CIX representation of this
               file to.
        """
        cixfile = createCixFile(cixtree, self.path, lang=self.lang)
        cixmodule = createCixModule(cixfile, self.name, self.lang, src=self.path)
        self.appendCixToModule(cixmodule)
    
class CSSCile:
    """
    CSS Code Intelligence Language Engine (CILE).
    """
    
    def __init__(self, path, lang, styleClassifier=StraightCSSStyleClassifier):
        """
        Creates a new CILE for a CSS file with the given path.
        @param path The path of the CSS file this CILE is parsing.
        """
        if sys.platform == "win32":
            path = path.replace('\\', '/') # normalize
        self.path = path
        self.lang = lang
        self.styleClassifier = styleClassifier
        
        self.cile = CSSFile(path, lang)
        
        self.selector = []
        self.selectorStartLine = None
        # Track nested selector names in order to generate fully expanded
        # selector names (Less/SCSS only).
        self.nestedSelectors = []
        # Track nested scopes in order to add declared variables to the
        # appropriate scope (Less/SCSS only).
        self.nestedScopes = []
        self.whitespace = re.compile('^\\s+$')
        self.blockLevel = 0
        self.parenLevel = 0
        self.ignoreStatement = False
        
    def handle_token(self, style, text, start_column, start_line, **otherArgs):
        """
        Called for each token parsed by a Scintilla lexer.
        This CILE only looks for a subset of tokens.
        Parses out element, class, and id selectors as well as Less/SCSS
        variable definitions.
        @param style The style of the parsed token.
        @param text The text of the parsed token.
        @param start_column The column number the parsed token is on.
        @param start_line The line number the parsed token is on.
        @param otherArgs Other token properties.
        """
        if self.styleClassifier.is_operator(style):
            if text == '}' or ';' in text:
                if text == '}':
                    self.blockLevel = max(self.blockLevel - 1, 0)
                    if (len(self.nestedSelectors) > 0):
                        self.nestedSelectors.pop()
                        self.nestedScopes.pop()
                if (self.blockLevel == 0 or self.lang in ['Less', 'SCSS']):
                    self.selector = [] # start looking for selectors
                    self.selectorStartLine = None # need to reset on ';' (Less, SCSS)
                    self.ignoreStatement = False
                return
            elif (text == '{' or text == ',' or '(' in text) and \
                 self.selector and len(self.selector) > 0 and \
                 (self.blockLevel == 0 or self.lang in ['Less', 'SCSS']) and \
                 self.parenLevel == 0 and not self.ignoreStatement and \
                 not (self.lang == 'SCSS' and self.selector[0] == '@' and
                      len(self.selector) > 1 and
                      self.selector[1] == 'include'):
                selectorText = ''.join(self.selector).strip()
                self.nestedSelectors.append(selectorText)
                self.nestedScopes.append([])
                if CSSSelector.CSS_ID_OR_CLASS.search(selectorText):
                    # Parse out each id and class being used in the selector
                    # and create an individual selector for that target.
                    for selector in CSSSelector.CSS_ID_OR_CLASS.findall(selectorText):
                        if self.lang in ['Less', 'SCSS']:
                            # Use the fully expanded name.
                            selectorText = ' '.join(self.nestedSelectors).replace('&', '')
                        css_selector = CSSSelector(selectorText,
                                                   self.selectorStartLine,
                                                   selector[0])
                        self.cile.addSelector(css_selector)
                        self.nestedScopes[-1].append(css_selector)
                else:
                    # No ids or classes in the selector; use its last element as
                    # the target.
                    selector = CSSSelector.CSS_IDENT.search(selectorText)
                    if selector:
                        if self.lang in ['Less', 'SCSS']:
                            # Use the fully expanded name.
                            selectorText = ' '.join(self.nestedSelectors).replace('&', '')
                        css_selector = CSSSelector(selectorText,
                                                   self.selectorStartLine,
                                                   selector.group(1))
                        self.cile.addSelector(css_selector)
                        self.nestedScopes[-1].append(css_selector)
                    else:
                        log.warn("Unable to process CSS selector '%s'" % selectorText)
                
                if text == '{':
                    if self.lang in ['Less', 'SCSS']:
                        # Continue with the next, nested selector.
                        self.selector = []
                    else:
                        # Stop looking for selectors until encountering '}'.
                        self.selector = None
                    self.blockLevel += 1
                elif text == ',':
                    # Continue with the next selector.
                    self.selector = []
                    self.nestedSelectors.pop() # only nest on '{'
                elif '(' in text:
                    # Less mixin; stop looking for selectors until encountering
                    # ')'.
                    self.selector = None
                    self.parenLevel += 1
                self.selectorStartLine = None
                return
            elif text == '{':
                # '{' encountered without a valid selector.
                self.blockLevel += 1
            elif text == ')':
                self.parenLevel = max(self.parenLevel - 1, 0)
            elif text == ':' and self.selector and \
                 ((self.lang == 'Less' and self.selector[0] == '@') or \
                  (self.lang == 'SCSS' and self.selector[0] == '$')):
                # Less or SCSS variable. Add it to the file scope or local
                # scope(s).
                variable = CSSVariable(''.join(self.selector).strip(),
                                       self.selectorStartLine)
                if len(self.nestedSelectors) == 0:
                    self.cile.addVariable(variable) # file scope
                else:
                    for css_selector in self.nestedScopes[-1]:
                        css_selector.addVariable(variable) # local scope
                # Stop looking for selectors until encountering ';'.
                self.ignoreStatement = True
        elif self.styleClassifier.is_comment(style):
            # Embedded comments within selectors are unlikely, but handle them
            # anyway just in case.
            return
        
        if self.selector == None:
            return # not looking for selectors right now
        
        if len(self.selector) > 0 or not re.match(self.whitespace, text):
            if text[0] == ' ' and len(self.selector) > 0 and \
               self.selector[-1] == ':':
                # Less and SCSS allow for nested selectors. "foo:bar" should be
                # considered a selector, while "foo: bar" should not be.
                self.ignoreStatement = True
                return
            self.selector.append(text)
            if not self.selectorStartLine:
                self.selectorStartLine = start_line
        
    def scan_purelang(self, text):
        """
        Scans the given CSS buffer text, feeding parsed tokens to this CILE's
        processor.
        Instead of passing in multi-language text (e.g. HTML, CSS, JS),
        pre-parse out the CSS tokens and then feed them directly to
        `handle_token()`.
        @param text The CSS text to parse and process.
        """
        CSSLexer().tokenize_by_style(text, self.handle_token)
        
    def appendToCixRoot(self, cixroot):
        """
        Appends to the given CixRoot the CIX representation of the parsed CSS.
        @param cixroot The CixRoot to append to.
        """
        self.cile.appendCixFileTo(cixroot)
        
    def appendToCixModule(self, cixmodule):
        """
        Appends to the given CixModule the CIX representation of the parsed CSS.
        This is used to append parsed CSS to a multi-language file currently
        being processed.
        @param cixmodule The CixModule to append to.
        """
        self.cile.appendCixToModule(cixmodule)
    
    

#---- internal support stuff

_ident_chars_dictionary = dict((ch, 1) for ch in 
                               string.lowercase + string.uppercase + string.digits + "-")

def _isident_first_char(char):
    return isident(char) and char != "-" and (char < "0" or char > "9")

def isident(char):
    # In CSS2, identifiers  (including element names, classes, and IDs in
    # selectors) can contain only the characters [A-Za-z0-9] and ISO 10646
    # characters 161 and higher, plus the hyphen (-); they cannot start with a
    # hyphen or a digit
    return char in _ident_chars_dictionary or ord(char) >= 161

def _isdigit(char):
    return "0" <= char <= "9"

def _is_udl_css_ident(char):
    return "a" <= char <= "z" or "A" <= char <= "Z" \
            or char == "_" or char == "="



#---- registration

def register(mgr):
    """Register language support with the Manager."""
    mgr.set_lang_info(lang,
                      silvercity_lexer=CSSLexer(),
                      buf_class=CSSBuffer,
                      langintel_class=CSSLangIntel,
                      import_handler_class=CSSImportHandler,
                      cile_driver_class=CSSCILEDriver,
                      is_cpln_lang=True)

