from __future__ import unicode_literals

import os
import re

from django.utils import six
from django.utils.six.moves import range

from reviewboard.diffviewer.processors import (filter_interdiff_opcodes,
                                               post_process_filtered_equals)


class MoveRange(object):
    """Stores information on a move range.

    This will store the start and end of the range, and all groups that
    are a part of it.
    """
    def __init__(self, start, end, groups=[]):
        self.start = start
        self.end = end
        self.groups = groups

    @property
    def last_group(self):
        return self.groups[-1]

    def add_group(self, group, group_index):
        if self.groups[-1] != group:
            self.groups.append((group, group_index))

    def __repr__(self):
        return '<MoveRange(%d, %d, %r)>' % (self.start, self.end, self.groups)


class DiffOpcodeGenerator(object):
    ALPHANUM_RE = re.compile(r'\w')
    WHITESPACE_RE = re.compile(r'\s')

    MOVE_PREFERRED_MIN_LINES = 2
    MOVE_MIN_LINE_LENGTH = 20

    TAB_SIZE = 8

    def __init__(self, differ, diff=None, interdiff=None, request=None,
                 **kwargs):
        """Initialize the opcode generator.

        Version Changed:
            3.0.18:
            Added the ``request`` and ``**kwargs`` parameters.

        Args:
            differ (reviewboard.diffviewer.differ.Differ):
                The differ being used to generate the diff.

            diff (bytes, optional):
                The raw contents for the diff.

            interdiff (bytes, optional):
                The raw contents for the diff on the other end of an
                interdiff range, if generating an interdiff.

            request (django.http.HttpRequest):
                The HTTP request from the client.

            **kwargs (dict):
                Additional keyword arguments, for future expansion.
        """
        self.differ = differ
        self.diff = diff
        self.interdiff = interdiff
        self.request = request

    def __iter__(self):
        """Returns opcodes from the differ with extra metadata.

        This is a wrapper around a differ's get_opcodes function, which returns
        extra metadata along with each range. That metadata includes
        information on moved blocks of code and whitespace-only lines.

        This returns a list of opcodes as tuples in the form of
        (tag, i1, i2, j1, j2, meta).
        """
        self.groups = []
        self.removes = {}
        self.inserts = []

        # Run the opcodes through the chain.
        opcodes = self.differ.get_opcodes()
        opcodes = self._apply_processors(opcodes)
        opcodes = self._generate_opcode_meta(opcodes)
        opcodes = self._apply_meta_processors(opcodes)

        self._group_opcodes(opcodes)
        self._compute_moves()

        for opcodes in self.groups:
            yield opcodes

    def _apply_processors(self, opcodes):
        """Apply any diff processors to the generated list of opcodes.

        If generating an interdiff, this will apply a filter to remove any
        unmodified lines.

        Args:
            opcodes (list of tuple):
                The list of generated diff opcodes to process.

        Yields:
            tuple:
            A processed opcode.
        """
        if self.diff and self.interdiff:
            # Filter out any lines unrelated to these changes from the
            # interdiff. This will get rid of any merge information.
            opcodes = filter_interdiff_opcodes(
                opcodes=opcodes,
                filediff_data=self.diff,
                interfilediff_data=self.interdiff,
                request=self.request)

        for opcode in opcodes:
            yield opcode

    def _generate_opcode_meta(self, opcodes):
        for tag, i1, i2, j1, j2 in opcodes:
            meta = {
                # True if this chunk is only whitespace.
                'whitespace_chunk': False,

                # List of tuples (i, j), with whitespace changes.
                'whitespace_lines': [],
            }

            if tag == 'replace':
                # replace groups are good for whitespace only changes.
                assert (i2 - i1) == (j2 - j1)

                for i, j in zip(range(i1, i2), range(j1, j2)):
                    if (self.WHITESPACE_RE.sub('', self.differ.a[i]) ==
                            self.WHITESPACE_RE.sub('', self.differ.b[j])):
                        # Both original lines are equal when removing all
                        # whitespace, so include their original line number in
                        # the meta dict.
                        meta['whitespace_lines'].append((i + 1, j + 1))

                # If all lines are considered to have only whitespace change,
                # the whole chunk is considered a whitespace-only chunk.
                if len(meta['whitespace_lines']) == (i2 - i1):
                    meta['whitespace_chunk'] = True
            elif tag == 'equal':
                for group in self._compute_chunk_indentation(i1, i2, j1, j2):
                    ii1, ii2, ij1, ij2, indentation_changes = group

                    if indentation_changes:
                        new_meta = dict({
                            'indentation_changes': indentation_changes,
                        }, **meta)
                    else:
                        new_meta = meta

                    yield tag, ii1, ii2, ij1, ij2, new_meta

                continue

            yield tag, i1, i2, j1, j2, meta

    def _apply_meta_processors(self, opcodes):
        if self.interdiff: