from typing import Any, Dict, List

from .template_parser import (
    tokenize,
    is_django_block_tag,
)

from zulint.printer import GREEN, ENDC

import subprocess

def pretty_print_html(html: str, num_spaces: int = 4) -> str:
    # We use 1-based indexing for both rows and columns.
    tokens = tokenize(html)
    lines = html.split('\n')

    # We will keep a stack of "start" tags so that we know
    # when HTML ranges end.  Note that some start tags won't
    # be blocks from an indentation standpoint.
    stack: List[Dict[str, Any]] = []

    # Seed our stack with a pseudo entry to make depth calculations
    # easier.
    info: Dict[str, Any] = dict(
        block=False,
        depth=-1,
        line=-1,
        token_kind='html_start',
        tag='html',
        extra_indent=0,
        ignore_lines=[],
    )
    stack.append(info)

    # Our main job is to figure out offsets that we use to nudge lines
    # over by.
    offsets: Dict[int, int] = {}

    # Loop through our start/end tokens, and calculate offsets.  As
    # we proceed, we will push/pop info dictionaries on/off a stack.
    for token in tokens:

        if token.kind in ('html_start', 'handlebars_start', 'handlebars_singleton',
                          'html_singleton', 'django_start',
                          'jinja2_whitespace_stripped_type2_start',
                          'jinja2_whitespace_stripped_start') and stack[-1]['tag'] != 'pre':
            # An HTML start tag should only cause a new indent if we
            # are on a new line.
            if (token.tag not in ('extends', 'include', 'else', 'elif') and
                    (is_django_block_tag(token.tag) or
                        token.kind != 'django_start')):
                is_block = token.line > stack[-1]['line']

                if is_block:
                    if (((token.kind == 'handlebars_start' and
                            stack[-1]['token_kind'] == 'handlebars_start') or
                            (token.kind in {'django_start',
                                            'jinja2_whitespace_stripped_type2_start',
                                            'jinja2_whitespace_stripped_start'} and
                             stack[-1]['token_kind'] in {'django_start',
                                                         'jinja2_whitespace_stripped_type2_start',
                                                         'jinja2_whitespace_stripped_start'})) and
                            not stack[-1]['indenting']):
                        info = stack.pop()
                        info['depth'] = info['depth'] + 1
                        info['indenting'] = True
                        info['adjust_offset_until'] = token.line
                        stack.append(info)
                    new_depth = stack[-1]['depth'] + 1
                    extra_indent = stack[-1]['extra_indent']
                    line = lines[token.line - 1]
                    adjustment = len(line)-len(line.lstrip()) + 1
                    offset = (1 + extra_indent + new_depth * num_spaces) - adjustment
                    info = dict(
                        block=True,
                        depth=new_depth,
                        actual_depth=new_depth,
                        line=token.line,
                        tag=token.tag,
                        token_kind=token.kind,
                        line_span=token.line_span,
                        offset=offset,
                        extra_indent=token.col - adjustment + extra_indent,
                        extra_indent_prev=extra_indent,
                        adjustment=adjustment,
                        indenting=True,
                        adjust_offset_until=token.line,
                        ignore_lines=[]
                    )
                    if token.kind in ('handlebars_start', 'django_start'):
                        info.update(dict(depth=new_depth - 1, indenting=False))
                else:
                    info = dict(
                        block=False,
                        depth=stack[-1]['depth'],
                        actual_depth=stack[-1]['depth'],
                        line=token.line,
                        tag=token.tag,
                        token_kind=token.kind,
                        extra_indent=stack[-1]['extra_indent'],
                        ignore_lines=[]
                    )
                stack.append(info)
        elif (token.kind in ('html_end', 'handlebars_end', 'html_singleton_end',
                             'django_end', 'handlebars_singleton_end',
                             'jinja2_whitespace_stripped_end') and
              (stack[-1]['tag'] != 'pre' or token.tag == 'pre')):
            info = stack.pop()
            if info['block']:
                # We are at the end of an indentation block.  We
                # assume the whole block was formatted ok before, just
                # possibly at an indentation that we don't like, so we
                # nudge over all lines in the block by the same offset.
                start_line = info['line']
                end_line = token.line
                if token.tag == 'pre':
                    offsets[start_line] = 0
                    offsets[end_line] = 0
                    stack[-1]['ignore_lines'].append(start_line)
                    stack[-1]['ignore_lines'].append(end_line)
                else:
                    offsets[start_line] = info['offset']
                    line = lines[token.line - 1]
                    adjustment = len(line)-len(line.lstrip()) + 1
                    if adjustment == token.col and token.kind != 'html_singleton_end':
                        offsets[end_line] = (info['offset'] +
                                             info['adjustment'] -
                                             adjustment +
                                             info['extra_indent'] -
                                             info['extra_indent_prev'])
                    elif (start_line + info['line_span'] - 1 == end_line and
                            info['line_span'] > 1):
                        offsets[end_line] = (1 + info['extra_indent'] +
                                             (info['depth'] + 1) * num_spaces) - adjustment
                        # We would like singleton tags and tags which spread over
                        # multiple lines to have 2 space indentation.
                        offsets[end_line] -= 2
                    elif token.line != info['line']:
                        offsets[end_line] = info['offset']
                if token.tag != 'pre' and token.tag != 'script':
                    for line_num in range(start_line + 1, end_line):
                        # Be careful not to override offsets that happened
                        # deeper in the HTML within our block.
                        if line_num not in offsets:
                            line = lines[line_num - 1]
                            new_depth = info['depth'] + 1
                            if (line.lstrip().startswith('{{else}}') or
                                    line.lstrip().startswith('{% else %}') or
                                    line.lstrip().startswith('{% elif')):
                                new_depth = info['actual_depth']
                            extra_indent = info['extra_indent']
                            adjustment = len(line)-len(line.lstrip()) + 1
                            offset = (1 + extra_indent + new_depth * num_spaces) - adjustment
                            if line_num <= start_line + info['line_span'] - 1:
                                # We would like singleton tags and tags which spread over
                                # multiple lines to have 2 space indentation.
                                offset -= 2
                            offsets[line_num] = offset
                        elif (token.kind in ('handlebars_end', 'django_end') and
                                info['indenting'] and
                                line_num < info['adjust_offset_until'] and
                                line_num not in info['ignore_lines']):
                            offsets[line_num] += num_spaces
                elif token.tag != 'pre':
                    for line_num in range(start_line + 1, end_line):
                        if line_num not in offsets:
                            offsets[line_num] = info['offset']
                else:
                    for line_num in range(start_line + 1, end_line):
                        if line_num not in offsets:
                            offsets[line_num] = 0
                            stack[-1]['ignore_lines'].append(line_num)

    # Now that we have all of our offsets calculated, we can just
    # join all our lines together, fixing up offsets as needed.
    formatted_lines = []
    for i, line in enumerate(html.split('\n')):
        row = i + 1
        offset = offsets.get(row, 0)
        pretty_line = line
        if line.strip() == '':
            pretty_line = ''
        else:
            if offset > 0:
                pretty_line = (' ' * offset) + pretty_line
            elif offset < 0:
                pretty_line = pretty_line[-1 * offset:]
                assert line.strip() == pretty_line.strip()
        formatted_lines.append(pretty_line)

    return '\n'.join(formatted_lines)


def validate_indent_html(fn: str, fix: bool) -> int:
    with open(fn) as f:
        html = f.read()
    phtml = pretty_print_html(html)
    if not html.split('\n') == phtml.split('\n'):
        if fix:
            print(GREEN + "Automatically fixing problems..." + ENDC)
            with open(fn, 'w') as f:
                f.write(phtml)
            # Since we successfully fixed the issues, we exit with status 0
            return 0
        print('Invalid Indentation detected in file: '
              '%s\nDiff for the file against expected indented file:' % (fn,), flush=True)
        with subprocess.Popen(
                ['diff', fn, '-'],
                stdin=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                universal_newlines=True) as p:
            p.communicate(phtml)
        print()
        print("This problem can be fixed with the `--fix` option.")
        return 0
    return 1
