from __future__ import print_function
import sys
import re
import copy
from cssbeautifier.__version__ import __version__

#
# The MIT License (MIT)

# Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


class BeautifierOptions:
    def __init__(self):
        self.indent_size = 4
        self.indent_char = ' '
        self.indent_with_tabs = False
        self.preserve_newlines = False
        self.selector_separator_newline = True
        self.end_with_newline = False
        self.newline_between_rules = True
        self.space_around_combinator = False
        self.eol = 'auto'

        self.css = None
        self.js = None
        self.html = None

        # deprecated
        self.space_around_selector_separator = False

    def mergeOpts(self, targetType):
        finalOpts = copy.copy(self)

        local = getattr(finalOpts, targetType)
        if (local):
            delattr(finalOpts, targetType)
            for key in local:
                setattr(finalOpts, key, local[key])

        return finalOpts


    def __repr__(self):
        return \
"""indent_size = %d
indent_char = [%s]
indent_with_tabs = [%s]
preserve_newlines = [%s]
separate_selectors_newline = [%s]
end_with_newline = [%s]
newline_between_rules = [%s]
space_around_combinator = [%s]
""" % (self.indent_size, self.indent_char, self.indent_with_tabs, self.preserve_newlines,
    self.selector_separator_newline, self.end_with_newline, self.newline_between_rules,
    self.space_around_combinator)


def default_options():
    return BeautifierOptions()


def beautify(string, opts=default_options()):
    b = Beautifier(string, opts)
    return b.beautify()


def beautify_file(file_name, opts=default_options()):
    if file_name == '-':  # stdin
        stream = sys.stdin
    else:
        stream = open(file_name)
    content = ''.join(stream.readlines())
    b = Beautifier(content, opts)
    return b.beautify()


def usage(stream=sys.stdout):

    print("cssbeautifier.py@" + __version__ + """

CSS beautifier (http://jsbeautifier.org/)

""", file=stream)
    if stream == sys.stderr:
        return 1
    else:
        return 0

WHITE_RE = re.compile("^\s+$")
WORD_RE = re.compile("[\w$\-_]")


class Printer:

    def __init__(self, beautifier, indent_char, indent_size, default_indent=""):
        self.beautifier = beautifier
        self.newlines_from_last_ws_eat = 0
        self.indentSize = indent_size
        self.singleIndent = (indent_size) * indent_char
        self.indentLevel = 0
        self.nestedLevel = 0

        self.baseIndentString = default_indent
        self.output = []

    def __lastCharWhitespace(self):
        return len(self.output) > 0 and WHITE_RE.search(self.output[-1]) is not None

    def indent(self):
        self.indentLevel += 1
        self.baseIndentString += self.singleIndent

    def outdent(self):
        if self.indentLevel:
            self.indentLevel -= 1
            self.baseIndentString = self.baseIndentString[:-(len(self.singleIndent))]

    def push(self, string):
        self.output.append(string)

    def openBracket(self):
        self.singleSpace()
        self.output.append("{")
        if self.beautifier.eatWhitespace(True) == 0:
            self.newLine()

    def closeBracket(self,newLine):
        if newLine:
            self.newLine()
        self.output.append("}")
        self.beautifier.eatWhitespace(True)
        if self.beautifier.newlines_from_last_ws_eat == 0:
            self.newLine()

    def semicolon(self):
        self.output.append(";")

    def comment(self, comment):
        self.output.append(comment)

    def newLine(self, keepWhitespace=False):
        if len(self.output) > 0 :
            if not keepWhitespace and self.output[-1] != '\n':
                self.trim()
            elif self.output[-1] == self.baseIndentString:
                self.output.pop()

            self.output.append("\n")

            if len(self.baseIndentString) > 0:
                self.output.append(self.baseIndentString)

    def trim(self):
        while self.__lastCharWhitespace():
            self.output.pop()

    def singleSpace(self):
        if len(self.output) > 0 and not self.__lastCharWhitespace():
            self.output.append(" ")

    def preserveSingleSpace(self,isAfterSpace):
        if isAfterSpace:
            self.singleSpace()

    def result(self):
        if self.baseIndentString:
            return self.baseIndentString + "".join(self.output);
        else:
            return "".join(self.output)


class Beautifier:

    def __init__(self, source_text, opts=default_options()):
        # This is not pretty, but given how we did the version import
        # it is the only way to do this without having setup.py fail on a missing six dependency.
        self.six = __import__("six")

        # in javascript, these two differ
        # in python they are the same, different methods are called on them
        self.lineBreak = re.compile(self.six.u("\r\n|[\n\r\u2028\u2029]"))
        self.allLineBreaks = self.lineBreak

        if not source_text:
            source_text = ''

        opts = opts.mergeOpts('css')

        # Continue to accept deprecated option
        opts.space_around_combinator = opts.space_around_combinator or opts.space_around_selector_separator

        self.opts = opts
        self.indentSize = opts.indent_size
        self.indentChar = opts.indent_char
        self.pos = -1
        self.ch = None

        if self.opts.indent_with_tabs:
            self.indentChar = "\t"
            self.indentSize = 1

        if self.opts.eol == 'auto':
            self.opts.eol = '\n'
            if self.lineBreak.search(source_text or ''):
                self.opts.eol = self.lineBreak.search(source_text).group()

        self.opts.eol = self.opts.eol.replace('\\r', '\r').replace('\\n', '\n')

        # HACK: newline parsing inconsistent. This brute force normalizes the input newlines.
        self.source_text = re.sub(self.allLineBreaks, '\n', source_text)

        # https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
        # also in CONDITIONAL_GROUP_RULE below
        self.NESTED_AT_RULE = [ \
            "@page", \
            "@font-face", \
            "@keyframes", \
            "@media", \
            "@supports", \
            "@document"]
        self.CONDITIONAL_GROUP_RULE = [ \
            "@media", \
            "@supports", \
            "@document"]

        m = re.search("^[\t ]*", self.source_text)
        baseIndentString = m.group(0)
        self.printer = Printer(self, self.indentChar, self.indentSize, baseIndentString)

    def next(self):
        self.pos = self.pos + 1
        if self.pos < len(self.source_text):
            self.ch = self.source_text[self.pos]
        else:
            self.ch = ''
        return self.ch

    def peek(self,skipWhitespace=False):
        start = self.pos
        if skipWhitespace:
            self.eatWhitespace()
        result = ""
        if self.pos + 1 < len(self.source_text):
            result = self.source_text[self.pos + 1]
        if skipWhitespace:
            self.pos = start - 1
            self.next()

        return result

    def eatString(self, endChars):
        start = self.pos
        while self.next():
            if self.ch == "\\":
                self.next()
            elif self.ch in endChars:
                break
            elif self.ch == "\n":
                break
        return self.source_text[start:self.pos] + self.ch

    def peekString(self, endChar):
        start = self.pos
        st = self.eatString(endChar)
        self.pos = start - 1
        self.next()
        return st

    def eatWhitespace(self, pn=False):
        result = 0
        while WHITE_RE.search(self.peek()) is not None:
            self.next()
            if self.ch == "\n" and pn and self.opts.preserve_newlines:
                self.printer.newLine(True)
                result += 1
        self.newlines_from_last_ws_eat = result
        return result

    def skipWhitespace(self):
        result = ''
        if self.ch and WHITE_RE.search(self.ch):
            result = self.ch

        while WHITE_RE.search(self.next()) is not None:
            result += self.ch
        return result

    def eatComment(self):
        start = self.pos
        singleLine = self.peek() == "/"
        self.next()
        while self.next():
            if not singleLine and self.ch == "*" and self.peek() == "/":
                self.next()
                break
            elif singleLine and self.ch == "\n":
                return self.source_text[start:self.pos]
        return self.source_text[start:self.pos] + self.ch

    def lookBack(self, string):
        past = self.source_text[self.pos - len(string):self.pos]
        return past.lower() == string

    # Nested pseudo-class if we are insideRule
    # and the next special character found opens
    # a new block
    def foundNestedPseudoClass(self):
        i = self.pos + 1
        openParen = 0
        while i < len(self.source_text):
            ch = self.source_text[i]
            if ch == "{":
                return True
            elif ch == "(":
                # pseudoclasses can contain ()
                openParen += 1
            elif ch == ")":
                if openParen == 0:
                    return False
                openParen -= 1
            elif ch == ";" or ch == "}":
                return False
            i += 1;

        return False

    def beautify(self):
        printer = self.printer
        insideRule = False
        insidePropertyValue = False
        enteringConditionalGroup = False
        top_ch = ''
        last_top_ch = ''
        parenLevel = 0

        while True:
            whitespace = self.skipWhitespace()
            isAfterSpace = whitespace != ''
            isAfterNewline = '\n' in whitespace
            last_top_ch = top_ch
            top_ch = self.ch

            if not self.ch:
                break
            elif self.ch == '/' and self.peek() == '*':
                header = printer.indentLevel == 0

                if not isAfterNewline or header:
                    printer.newLine()


                comment = self.eatComment()
                printer.comment(comment)
                printer.newLine()
                if header:
                    printer.newLine(True)
            elif self.ch == '/' and self.peek() == '/':
                if not isAfterNewline and last_top_ch != '{':
                    printer.trim()

                printer.singleSpace()
                printer.comment(self.eatComment())
                printer.newLine()
            elif self.ch == '@':
                printer.preserveSingleSpace(isAfterSpace)

                # deal with less propery mixins @{...}
                if self.peek(True) == '{':
                    printer.push(self.eatString('}'));
                else:
                    printer.push(self.ch)
                    # strip trailing space, if present, for hash property check
                    variableOrRule = self.peekString(": ,;{}()[]/='\"")

                    if variableOrRule[-1] in ": ":
                        # wwe have a variable or pseudo-class, add it and insert one space before continuing
                        self.next()
                        variableOrRule = self.eatString(": ")
                        if variableOrRule[-1].isspace():
                            variableOrRule = variableOrRule[:-1]
                        printer.push(variableOrRule)
                        printer.singleSpace();

                    if variableOrRule[-1].isspace():
                        variableOrRule = variableOrRule[:-1]

                    # might be a nesting at-rule
                    if variableOrRule in self.NESTED_AT_RULE:
                        printer.nestedLevel += 1
                        if variableOrRule in self.CONDITIONAL_GROUP_RULE:
                            enteringConditionalGroup = True
            elif self.ch == '#' and self.peek() == '{':
                printer.preserveSingleSpace(isAfterSpace)
                printer.push(self.eatString('}'));
            elif self.ch == '{':
                if self.peek(True) == '}':
                    self.eatWhitespace()
                    self.next()
                    printer.singleSpace()
                    printer.push("{")
                    printer.closeBracket(False)
                    if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
                        printer.newLine(True)
                else:
                    printer.indent()
                    printer.openBracket()
                    # when entering conditional groups, only rulesets are allowed
                    if enteringConditionalGroup:
                        enteringConditionalGroup = False
                        insideRule = printer.indentLevel > printer.nestedLevel
                    else:
                        # otherwise, declarations are also allowed
                        insideRule = printer.indentLevel >= printer.nestedLevel
            elif self.ch == '}':
                printer.outdent()
                printer.closeBracket(True)
                insideRule = False
                insidePropertyValue = False
                if printer.nestedLevel:
                    printer.nestedLevel -= 1
                if self.newlines_from_last_ws_eat < 2 and self.opts.newline_between_rules and printer.indentLevel == 0:
                    printer.newLine(True)
            elif self.ch == ":":
                self.eatWhitespace()
                if (insideRule or enteringConditionalGroup) and \
                        not (self.lookBack('&') or self.foundNestedPseudoClass()) and \
                        not self.lookBack('('):
                    # 'property: value' delimiter
                    # which could be in a conditional group query
                    printer.push(":")
                    if not insidePropertyValue:
                        insidePropertyValue = True
                        printer.singleSpace()
                else:
                    # sass/less parent reference don't use a space
                    # sass nested pseudo-class don't use a space

                    # preserve space before pseudoclasses/pseudoelements, as it means "in any child"
                    if (self.lookBack(' ')) and (printer.output[-1] != ' '):
                        printer.push(" ")
                    if self.peek() == ":":
                        # pseudo-element
                        self.next()
                        printer.push("::")
                    else:
                        # pseudo-element
                        printer.push(":")
            elif self.ch == '"' or self.ch == '\'':
                printer.preserveSingleSpace(isAfterSpace)
                printer.push(self.eatString(self.ch))
            elif self.ch == ';':
                insidePropertyValue = False
                printer.semicolon()
                if self.eatWhitespace(True) == 0:
                    printer.newLine()
            elif self.ch == '(':
                # may be a url
                if self.lookBack("url"):
                    printer.push(self.ch)
                    self.eatWhitespace()
                    if self.next():
                        if self.ch is not ')' and self.ch is not '"' \
                        and self.ch is not '\'':
                            printer.push(self.eatString(')'))
                        else:
                            self.pos = self.pos - 1
                else:
                    parenLevel += 1
                    printer.preserveSingleSpace(isAfterSpace)
                    printer.push(self.ch)
                    self.eatWhitespace()
            elif self.ch == ')':
                printer.push(self.ch)
                parenLevel -= 1
            elif self.ch == ',':
                printer.push(self.ch)
                if self.eatWhitespace(True) == 0 and not insidePropertyValue and self.opts.selector_separator_newline and parenLevel < 1:
                    printer.newLine()
                else:
                    printer.singleSpace()
            elif (self.ch == '>' or self.ch == '+' or self.ch == '~') and \
                not insidePropertyValue and parenLevel < 1:
                # handle combinator spacing
                if self.opts.space_around_combinator:
                    printer.singleSpace()
                    printer.push(self.ch)
                    printer.singleSpace()
                else:
                    printer.push(self.ch)
                    self.eatWhitespace()
                    # squash extra whitespace
                    if self.ch and WHITE_RE.search(self.ch):
                        self.ch = ''
            elif self.ch == ']':
                printer.push(self.ch)
            elif self.ch == '[':
                printer.preserveSingleSpace(isAfterSpace)
                printer.push(self.ch)
            elif self.ch == '=':
                # no whitespace before or after
                self.eatWhitespace()
                printer.push('=')
                if WHITE_RE.search(self.ch):
                    self.ch = ''
            else:
                printer.preserveSingleSpace(isAfterSpace)
                printer.push(self.ch)

        sweet_code = re.sub('[\r\n\t ]+$', '', printer.result())

        # establish end_with_newline
        if self.opts.end_with_newline:
            sweet_code += '\n'

        if not self.opts.eol == '\n':
            sweet_code = sweet_code.replace('\n', self.opts.eol)

        return sweet_code
