#-*- coding:utf-8 -*-

#  Pybik -- A 3 dimensional magic cube game.
#  Copyright © 2009-2013  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Ported from GNUbik
# Original filename: move-queue.c
# Original copyright and license: 2004  Dale Mellor, GPL3+



import re
from collections import namedtuple

from .debug import *    # pylint: disable=W0401, W0614


# A structure containing information about a movement taking place.
class MoveData (namedtuple('_MoveData', 'axis slice dir')):     # pylint: disable=W0232
    # axis  = 0...
    # slice = 0...dim-1 or -1 for all slices
    # dir   = 0 or 1
    def inverted(self):
        return self._replace(dir=not self.dir)  # pylint: disable=E1101

class MoveQueue(object):
    class MoveQueueItem(object):
        def __init__(self, data, mark_after=False, code=None, markup=None):
            self.data = MoveData(*data) or MoveData(0, 0, 0)
            self.mark_after = mark_after
            self.code = code
            self.markup = markup
        axis = property(lambda self: self.data.axis)
        slice = property(lambda self: self.data.slice)
        dir = property(lambda self: self.data.dir)
        def copy(self):
            return MoveQueue.MoveQueueItem(self.data,
                    mark_after=self.mark_after,
                    code=self.code,
                    markup=self.markup)
        def invert(self):
            self.data = self.data.inverted()
        def rotate_by(self, model, totalmove):
            rmove = model.rotate_move(totalmove.data, self.data)
            self.data = MoveData(*rmove)
            self.code = None
            self.markup = None
            
    def __init__(self):
        self.current_place = 0      # Number of steps of current from head.
        self.moves = []
        self.converter = FormatFlubrd
    def copy(self):
        movequeue = MoveQueue()
        movequeue.current_place = self.current_place
        movequeue.moves = [m.copy() for m in self.moves]
        movequeue.converter = self.converter
        return movequeue
        
    def at_start(self):
        return self.current_place == 0
    def at_end(self):
        return self.current_place == self.queue_length
    @property
    def _prev(self):
        return self.moves[self.current_place-1]
    @property
    def _current(self):
        return self.moves[self.current_place]
    @property
    def queue_length(self):
        return len(self.moves)
    def __str__(self):
        return 'MoveQueue(len=%s)' % self.queue_length

    # Routine to push a new MoveData object onto the _back_ of the given queue. A
    # new,  locally-managed copy is made of the incumbent datum. Return 1 (one) if
    # the operation is successful; 0 (zero) indicates that there was insufficient
    # memory to grow the queue.
    def push(self, move_data, mark_after=False, code=None, markup=None):
        new_element = self.MoveQueueItem(move_data,
                                mark_after=mark_after, code=code, markup=markup)
        self.moves.append(new_element)

    # Procedure to copy the incumbent datum to the current place in the queue,  and
    # drop all subsequent queue entries (so that the current element becomes the
    # tail). If current points past the end of the tail (including the case when
    # there are no data in the queue),  then a regular push operation is performed
    # at the tail. In all other cases 1 (one) will be returned.
    def push_current(self, move_data):
        # If there are no data in the queue,  then perform a standard push
        # operation. Also do this if we are at the tail.
        if not self.at_end():
            self.moves[self.current_place:] = []
        self.push(move_data)

    # Routine to get the data at the 'current' position in the queue. If there are
    # no data in the queue,  None will be returned.
    def current(self):
        return None if self.at_end() else self._current.data

    # Routine to retard the current pointer (move it towards the _head_ of the queue).
    def retard(self):
        if not self.at_start():
            self.current_place -= 1
    
    def rewind_start(self):
        self.current_place = 0
        
    def forward_end(self):
        self.current_place = self.queue_length
        
    # Remove the current object and all those that come afterwards.
    def truncate(self):
        self.moves[self.current_place:] = []
        
    def truncate_before(self):
        self.moves[:self.current_place] = []
        self.current_place = 0
        
    def reset(self):
        self.current_place = 0
        self.moves[:] = []
        
    def advance(self):
        if not self.at_end():
            self.current_place += 1
        
    def swapnext(self):
        cp = self.current_place
        if cp+1 < self.queue_length:
            ma, mb = self.moves[cp], self.moves[cp+1]
            ma.code = mb.code = None
            ma.data, mb.data = mb.data, ma.data
            self.advance()
            
    def swapprev(self):
        cp = self.current_place
        if 0 < cp < self.queue_length:
            ma, mb = self.moves[cp-1], self.moves[cp]
            ma.code = mb.code = None
            ma.data, mb.data = mb.data, ma.data
            self.retard()
            
    def invert(self):
        # a mark at the end of the moves is discarded because a mark at start is not supported
        mark = False
        for move in self.moves:
            move.invert()
            move.code = None
            move.markup = None
            move.mark_after, mark = mark, move.mark_after
        self.moves.reverse()
        if not (self.at_start() or self.at_end()):
            self.current_place = len(self.moves) - self.current_place
            
    def normalize_complete_rotations(self, model):
        totalmoves = []
        new_moves = []
        for i, move in enumerate(self.moves):
            if i == self.current_place:
                self.current_place = len(new_moves)
            if move.slice < 0:
                totalmoves.append(move)
            else:
                for totalmove in reversed(totalmoves):
                    move.rotate_by(model, totalmove)
                new_moves.append(move)
        self.moves = new_moves + totalmoves
        
    def is_mark_current(self):
        return self.at_start() or self._prev.mark_after
        
    def is_mark_after(self, pos):
        return self.moves[pos].mark_after
        
    def mark_current(self, mark=True):
        if not self.at_start():
            self._prev.mark_after = mark
            if self._prev.code is not None:
                self._prev.code = self._prev.code.replace(' ','')
                #FIXME: recreate self._prev.markup
                if mark:
                    self._prev.code += ' '
                    
    def mark_and_extend(self, other):
        if not other.moves:
            return -1
        self.mark_current()
        self.truncate()
        self.moves += other.moves
        return self.current_place + other.current_place
        
    def format(self, model):
        #TODO: arg to use explicit converter
        code = ''
        markup = ''
        pos = 0
        for i, move in enumerate(self.moves):
            if move.code is None:
                move.code, move.markup = self.converter.format(move, model)
            code += move.code
            markup += move.markup
            if i < self.current_place:
                pos = len(code)
        return code, pos, markup
        
    def parse_iter(self, code, pos, model):
        #TODO: arg to use explicit converter
        code = code.lstrip(' ')
        queue_pos = self.current_place
        move_code = ''
        for i, c in enumerate(code):
            if move_code and self.converter.isstart(c, model):
                data, mark, markup_code = self.converter.parse_move(move_code, model)
                if data is not None:
                    #FIXME: invalid chars at start get lost, other invalid chars are just ignored
                    self.push(data, mark, move_code, markup_code)
                yield data, queue_pos, i
                if i == pos:
                    queue_pos = self.queue_length
                move_code = c
            else:
                move_code += c
            if i < pos:
                queue_pos = self.queue_length + 1
        if move_code:
            data, mark, markup_code = self.converter.parse_move(move_code, model)
            if data is not None:
                self.push(data, mark, move_code, markup_code)
            if len(code)-len(move_code) < pos:
                queue_pos = self.queue_length
            yield data, queue_pos, len(code)
            
    def parse(self, code, pos, model):
        qpos = 0
        cpos = 0
        for res in self.parse_iter(code, pos, model):
            qpos = res[1]
            if cpos < pos:
                cpos = res[2]
        return qpos, cpos
        
        
class FormatFlubrd: # pylint: disable=W0232
    re_flubrd = re.compile(r"(.)(\d*)(['-]?)([^ ]*)( *)(.*)")
    
    @classmethod
    def isstart(cls, char, model):
        return char.upper() in model.faces
    
    @staticmethod
    def _format_markup(mface, mslice, mdir, err1, mark, err2):
        if err1:
            err1 = '<span underline="error" color="red">%s</span>' % err1
        if err2:
            err2 = '<span underline="error">%s</span>' % err2
        return ''.join((mface, mslice, mdir, err1, mark, err2))
    
    @staticmethod
    def intern_to_normal_move(maxis, mslice, mdir, model):
        if mslice == -1:
            return model.symbolsI[maxis] if mdir else model.symbols[maxis], 0, 0, True
        elif mslice*2 > model.sizes[maxis]-1:
            return model.symbolsI[maxis], model.sizes[maxis]-1 - mslice, 1-mdir, False
        else:
            return model.symbols[maxis], mslice, mdir, False
    
    @classmethod
    def format(cls, move, model):
        mface, mslice, mdir, whole_cube = cls.intern_to_normal_move(move.axis, move.slice, move.dir, model)
        mface = mface.upper() if whole_cube else mface.lower()
        mslice = str(mslice+1) if mslice else ''
        mdir = '-' if mdir else ""
        mark = ' ' if move.mark_after else ''
        move_code = mface + mslice + mdir + mark
        markup_code = cls._format_markup(mface, mslice, mdir, '', mark, '')
        return move_code, markup_code
        
    @staticmethod
    def normal_to_intern_move(mface, mslice, mdir, whole_cube, model):
        if mface in model.symbols:
            return model.symbols.index(mface), (-1 if whole_cube else mslice), mdir
        elif mface in model.symbolsI:
            maxis = model.symbolsI.index(mface)
            return maxis, (-1 if whole_cube else model.sizes[maxis]-1 - mslice), 1-mdir
        else:
            return None, None, None
            
    @classmethod
    def parse_move(cls, move_code, model):
        #debug('move_code: '+move_code)
        mface, mslice, mdir, err1, mark, err2 = cls.re_flubrd.match(move_code).groups()
        markup_code = cls._format_markup(mface, mslice, mdir, err1, mark, err2)
        whole_cube = mface.isupper()
        mface = mface.upper()
        mslice = int(mslice or 1) - 1
        mdir = int(bool(mdir))
        mark = bool(mark)
        # move finished, normalize it
        maxis, mslice2, mdir = cls.normal_to_intern_move(mface, mslice, mdir, whole_cube, model)
        if maxis is not None and 0 <= mslice < model.sizes[maxis]:
            return MoveData(maxis, mslice2, mdir), mark, markup_code
        debug('Error parsing formula')
        return None, False, move_code
        
    @staticmethod
    def parse(code, pos, model):
        moves = MoveQueue()
        qpos, cpos = moves.parse(code, pos, model)
        return moves, qpos, cpos
        
        
