import re
from .scanner import ScannerParser, Matcher
from .inline_parser import ESCAPE_CHAR, LINK_LABEL
from .util import unikey

_NEW_LINES = re.compile(r'\r\n|\r')
_BLANK_LINES = re.compile(r'^ +$', re.M)

_TRIM_4 = re.compile(r'^ {1,4}')
_EXPAND_TAB = re.compile(r'^( {0,3})\t', flags=re.M)
_INDENT_CODE_TRIM = re.compile(r'^ {1,4}', flags=re.M)
_BLOCK_QUOTE_TRIM = re.compile(r'^ {0,1}', flags=re.M)
_BLOCK_QUOTE_LEADING = re.compile(r'^ *>', flags=re.M)
_BLOCK_TAGS = {
    'address', 'article', 'aside', 'base', 'basefont', 'blockquote',
    'body', 'caption', 'center', 'col', 'colgroup', 'dd', 'details',
    'dialog', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption',
    'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3',
    'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'iframe',
    'legend', 'li', 'link', 'main', 'menu', 'menuitem', 'meta', 'nav',
    'noframes', 'ol', 'optgroup', 'option', 'p', 'param', 'section',
    'source', 'summary', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
    'title', 'tr', 'track', 'ul'
}
_BLOCK_HTML_RULE6 = (
    r'</?(?:' + '|'.join(_BLOCK_TAGS) + r')'
    r'(?: +|\n|/?>)[\s\S]*?'
    r'(?:\n{2,}|\n*$)'
)
_BLOCK_HTML_RULE7 = (
    # open tag
    r'<(?!script|pre|style)([a-z][\w-]*)(?:'
    r' +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"|'
    r''' *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?'''
    r')*? */?>(?=\s*\n)[\s\S]*?(?:\n{2,}|\n*$)|'
    # close tag
    r'</(?!script|pre|style)[a-z][\w-]*\s*>(?=\s*\n)[\s\S]*?(?:\n{2,}|\n*$)'
)

_PARAGRAPH_SPLIT = re.compile(r'\n{2,}')
_LIST_BULLET = re.compile(r'^ *([\*\+-]|\d+[.)])')


class BlockParser(ScannerParser):
    scanner_cls = Matcher

    NEWLINE = re.compile(r'\n+')
    DEF_LINK = re.compile(
        r' {0,3}\[(' + LINK_LABEL + r')\]:(?:[ \t]*\n)?[ \t]*'
        r'<?([^\s>]+)>?(?:[ \t]*\n)?'
        r'(?: +["(]([^\n]+)[")])? *\n+'
    )

    AXT_HEADING = re.compile(
        r' {0,3}(#{1,6})(?!#+)(?: *\n+|'
        r'\s+([^\n]*?)(?:\n+|\s+?#+\s*\n+))'
    )
    SETEX_HEADING = re.compile(r'([^\n]+)\n *(=|-){2,}[ \t]*\n+')
    THEMATIC_BREAK = re.compile(
        r' {0,3}((?:-[ \t]*){3,}|'
        r'(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})\n+'
    )

    INDENT_CODE = re.compile(r'(?:\n*)(?:(?: {4}| *\t)[^\n]+\n*)+')

    FENCED_CODE = re.compile(
        r'( {0,3})(`{3,}|~{3,})([^`\n]*)\n'
        r'(?:|([\s\S]*?)\n)'
        r'(?: {0,3}\2[~`]* *\n+|$)'
    )
    BLOCK_QUOTE = re.compile(
        r'(?: {0,3}>[^\n]*\n)+'
    )
    LIST_START = re.compile(
        r'( {0,3})([\*\+-]|\d{1,9}[.)])(?:[ \t]*|[ \t][^\n]+)\n+'
    )

    BLOCK_HTML = re.compile((
        r' {0,3}(?:'
        r'<(script|pre|style)[\s>][\s\S]*?(?:</\1>[^\n]*\n+|$)|'
        r'<!--(?!-?>)[\s\S]*?-->[^\n]*\n+|'
        r'<\?[\s\S]*?\?>[^\n]*\n+|'
        r'<![A-Z][\s\S]*?>[^\n]*\n+|'
        r'<!\[CDATA\[[\s\S]*?\]\]>[^\n]*\n+'
        r'|' + _BLOCK_HTML_RULE6 + '|' + _BLOCK_HTML_RULE7 + ')'
    ), re.I)

    LIST_MAX_DEPTH = 6
    BLOCK_QUOTE_MAX_DEPTH = 6
    RULE_NAMES = (
        'newline', 'thematic_break',
        'fenced_code', 'indent_code',
        'block_quote', 'block_html',
        'list_start',
        'axt_heading', 'setex_heading',
        'def_link',
    )

    def __init__(self):
        super(BlockParser, self).__init__()
        self.block_quote_rules = list(self.RULE_NAMES)
        self.list_rules = list(self.RULE_NAMES)

    def parse_newline(self, m, state):
        return {'type': 'newline', 'blank': True}

    def parse_thematic_break(self, m, state):
        return {'type': 'thematic_break', 'blank': True}

    def parse_indent_code(self, m, state):
        text = expand_leading_tab(m.group(0))
        code = _INDENT_CODE_TRIM.sub('', text)
        code = code.lstrip('\n')
        return self.tokenize_block_code(code, None, state)

    def parse_fenced_code(self, m, state):
        info = ESCAPE_CHAR.sub(r'\1', m.group(3))
        spaces = m.group(1)
        code = m.group(4) or ''
        if spaces and code:
            _trim_pattern = re.compile('^' + spaces, re.M)
            code = _trim_pattern.sub('', code)
        return self.tokenize_block_code(code + '\n', info, state)

    def tokenize_block_code(self, code, info, state):
        token = {'type': 'block_code', 'raw': code}
        if info:
            token['params'] = (info, )
        return token

    def parse_axt_heading(self, m, state):
        level = len(m.group(1))
        text = m.group(2) or ''
        text = text.strip()
        if set(text) == {'#'}:
            text = ''
        return self.tokenize_heading(text, level, state)

    def parse_setex_heading(self, m, state):
        level = 1 if m.group(2) == '=' else 2
        text = m.group(1)
        text = text.strip()
        return self.tokenize_heading(text, level, state)

    def tokenize_heading(self, text, level, state):
        return {'type': 'heading', 'text': text, 'params': (level,)}

    def get_block_quote_rules(self, depth):
        if depth > self.BLOCK_QUOTE_MAX_DEPTH - 1:
            rules = list(self.block_quote_rules)
            rules.remove('block_quote')
            return rules
        return self.block_quote_rules

    def parse_block_quote(self, m, state):
        depth = state.get('block_quote_depth', 0) + 1
        state['block_quote_depth'] = depth

        # normalize block quote text
        text = _BLOCK_QUOTE_LEADING.sub('', m.group(0))
        text = expand_leading_tab(text)
        text = _BLOCK_QUOTE_TRIM.sub('', text)
        text = cleanup_lines(text)

        rules = self.get_block_quote_rules(depth)
        children = self.parse(text, state, rules)
        state['block_quote_depth'] = depth - 1
        return {'type': 'block_quote', 'children': children}

    def get_list_rules(self, depth):
        if depth > self.LIST_MAX_DEPTH - 1:
            rules = list(self.list_rules)
            rules.remove('list_start')
            return rules
        return self.list_rules

    def parse_list_start(self, m, state, string):
        items = []
        spaces = m.group(1)
        marker = m.group(2)
        items, pos = _find_list_items(string, m.start(), spaces, marker)
        tight = '\n\n' not in ''.join(items).strip()

        ordered = len(marker) != 1
        if ordered:
            start = int(marker[:-1])
            if start == 1:
                start = None
        else:
            start = None

        list_tights = state.get('list_tights', [])
        list_tights.append(tight)
        state['list_tights'] = list_tights

        depth = len(list_tights)
        rules = self.get_list_rules(depth)
        children = [
            self.parse_list_item(item, depth, state, rules)
            for item in items
        ]
        list_tights.pop()
        params = (ordered, depth, start)
        token = {'type': 'list', 'children': children, 'params': params}
        return token, pos

    def parse_list_item(self, text, depth, state, rules):