from copy import deepcopy

from django import template

from classytags.exceptions import ArgumentRequiredError
from classytags.exceptions import BreakpointExpected
from classytags.exceptions import TooManyArguments
from classytags.exceptions import TrailingBreakpoint


class Parser(object):
    """
    Argument parsing class. A new instance of this gets created each time a tag
    get's parsed.
    """
    def __init__(self, options):
        self.options = options.bootstrap()

    def parse(self, parser, tokens):
        """
        Parse a token stream
        """
        self.parser = parser
        self.bits = tokens.split_contents()
        self.tagname = self.bits.pop(0)
        self.kwargs = {}
        self.blocks = {}
        self.forced_next = None
        # Get the first chunk of arguments until the next breakpoint
        self.arguments = self.options.get_arguments()
        self.current_argument = None
        # get a copy of the bits (tokens)
        self.todo = list(self.bits)
        # parse the bits (tokens)
        breakpoint = False
        for bit in self.bits:
            breakpoint = self.handle_bit(bit)
        if breakpoint:
            raise TrailingBreakpoint(self.tagname, breakpoint)
        # finish the bits (tokens)
        self.finish()
        # parse block tags
        self.parse_blocks()
        return self.kwargs, self.blocks

    def handle_bit(self, bit):
        """
        Handle the current bit
        """
        breakpoint = False
        if self.forced_next is not None:
            if bit != self.forced_next:
                raise BreakpointExpected(self.tagname, [self.forced_next], bit)
        elif bit in self.options.reversed_combined_breakpoints:
            expected = self.options.reversed_combined_breakpoints[bit]
            raise BreakpointExpected(self.tagname, [expected], bit)
        # Check if the current bit is the next breakpoint
        if bit == self.options.next_breakpoint:
            self.handle_next_breakpoint(bit)
            breakpoint = bit
        # Check if the current bit is a future breakpoint
        elif bit in self.options.breakpoints:
            self.handle_breakpoints(bit)
            breakpoint = bit
        # Otherwise it's a 'normal' argument
        else:
            self.handle_argument(bit)
        if bit in self.options.combined_breakpoints:
            self.forced_next = self.options.combined_breakpoints[bit]
        else:
            self.forced_next = None
        # remove from todos
        del self.todo[0]
        return breakpoint

    def handle_next_breakpoint(self, bit):
        """
        Handle a bit which is the next breakpoint by checking the current
        breakpoint scope is finished or can be finished and then shift to the
        next scope.
        """
        # Check if any unhandled argument in the current breakpoint is required
        self.check_required()
        # Shift the breakpoint to the next one
        self.options.shift_breakpoint()
        # Get the next chunk of arguments
        self.arguments = self.options.get_arguments()
        if self.arguments:
            self.current_argument = self.arguments.pop(0)
        else:
            self.current_argument = None

    def handle_breakpoints(self, bit):
        """
        Handle a bit which is a future breakpoint by trying to finish all
        intermediate breakpoint codes as well as the current scope and then
        shift.
        """
        # While we're not at our target breakpoint
        while bit != self.options.current_breakpoint:
            # Check required arguments
            self.check_required()
            # Shift to the next breakpoint
            self.options.shift_breakpoint()
            self.arguments = self.options.get_arguments()
        self.current_argument = self.arguments.pop(0)

    def handle_argument(self, bit):
        """
        Handle the current argument.
        """
        # If we don't have an argument yet
        if self.current_argument is None:
            try:
                # try to get the next one
                self.current_argument = self.arguments.pop(0)
            except IndexError:
                # If we don't have any arguments, left, raise a
                # TooManyArguments error
                raise TooManyArguments(self.tagname, self.todo)
        # parse the current argument and check if this bit was handled by this
        # argument
        handled = self.current_argument.parse(self.parser, bit, self.tagname,
                                              self.kwargs)
        # While this bit is not handled by an argument
        while not handled:
            try:
                # Try to get the next argument
                self.current_argument = self.arguments.pop(0)
            except IndexError:
                # If there is no next argument but there are still breakpoints
                # Raise an exception that we expected a breakpoint
                if self.options.breakpoints:
                    raise BreakpointExpected(self.tagname,
                                             self.options.breakpoints, bit)
                elif self.options.next_breakpoint:
                    raise BreakpointExpected(self.tagname,
                                             [self.options.next_breakpoint],
                                             bit)
                else:
                    # Otherwise raise a TooManyArguments excption
                    raise TooManyArguments(self.tagname, self.todo)
            # Try next argument
            handled = self.current_argument.parse(self.parser, bit,
                                                  self.tagname, self.kwargs)

    def finish(self):
        """
        Finish up parsing by checking all remaining breakpoint scopes
        """
        # Check if there are any required arguments left in the current
        # breakpoint
        self.check_required()
        # While there are still breakpoints left
        while self.options.next_breakpoint:
            # Shift to the next breakpoint
            self.options.shift_breakpoint()
            self.arguments = self.options.get_arguments()
            # And check this breakpoints arguments for required arguments.
            self.check_required()

    def parse_blocks(self):
        """
        Parse template blocks for block tags.

        Example:
            {% a %} b {% c %} d {% e %} f {% g %}
             => pre_c: b
                pre_e: d
                pre_g: f
            {% a %} b {% f %}
             => pre_c: b
                pre_e: None
                pre_g: None
        """
        # if no blocks are defined, bail out
        if not self.options.blocks:
            return
        # copy the blocks
        blocks = deepcopy(self.options.blocks)
        identifiers = {}
        for block in blocks:
            identifiers[block] = block.collect(self)
        while blocks:
            current_block = blocks.pop(0)
            current_identifiers = identifiers[current_block]
            block_identifiers = list(current_identifiers)
            for block in blocks:
                block_identifiers += identifiers[block]
            nodelist = self.parser.parse(block_identifiers)
            token = self.parser.next_token()
            while token.contents not in current_identifiers:
                empty_block = blocks.pop(0)
                current_identifiers = identifiers[empty_block]
                self.blocks[empty_block.alias] = template.NodeList()
            self.blocks[current_block.alias] = nodelist

    def check_required(self):
        """
        Iterate over arguments, checking if they're required, otherwise
        populating the kwargs dictionary with their defaults.
        """
        for argument in self.arguments:
            if argument.required:
                raise ArgumentRequiredError(argument, self.tagname)
            else:
                self.kwargs[argument.name] = argument.get_default()
