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

#  Copyright © 2014  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/>.

from collections import deque


class PatternCubie (list):
    # value: (index, rotation)
    def __init__(self, model, position=None):
        self.model = model
        self.position = position
        
    def gray_conditions(self):
        for idx in range(self.model.cell_count):
            for rot in self.model.face_permutations.keys():
                pos = self.model.rotated_position[rot][idx]
                if pos == self.position:
                    yield idx, rot
                    
    def set_gray(self):
        self[:] = list(self.gray_conditions())
        return self
        
    def parse(self, position, condition):
        self.parse_or_condition(position, condition)
        assert self.position is not None, (self.position, position, condition)
        return self
        
    def __str__(self):
        pos = ''.join(self.model.cells_visible_faces[self.position])
        vals = []
        notconditions = list(self.not_conditions(self))
        if len(notconditions) == 1:
            conditions = notconditions
            prefix = '!'
        else:
            conditions = self
            prefix = ''
        grouped = {}
        for idx, rot in conditions:
            grouped.setdefault(idx, []).append((idx, rot))
        def cond2str(idx, rot):
            return ''.join(self.model.face_symbolic_to_face_color(face, rot) for face in pos).lower()
        for idx, conds in grouped.items():
            if len(conds) > 1 and set(conds) == set(self.rotated_conditions(idx)):
                grouped[idx] = '*' + cond2str(*conds[0])
            else:
                grouped[idx] = '|'.join(cond2str(*c) for c in conds)
        return '{}={}{}'.format(pos.lower(), prefix, '|'.join(grouped.values()))
        
    def parse_pattern_condition(self, position, condition, order=True):
        if order:
            def innergen():
                for pface, cface in zip(position, condition):
                    if '?' != cface != self.model.face_symbolic_to_face_color(pface, rot):
                        break
                else:
                    yield idx, rot
        else:
            def innergen():
                cond = condition
                for pface in position:
                    cface = self.model.face_symbolic_to_face_color(pface, rot)
                    if cface in cond:
                        cond = cond.replace(cface, '', 1)
                    elif '?' in cond:
                        cond = cond.replace('?', '', 1)
                    else:
                        break
                else:
                    yield idx, rot
        position = position.upper()
        condition = condition.upper()
        for idx in range(self.model.cell_count):
            for rot in self.model.face_permutations.keys():
                pos = self.model.rotated_position[rot][idx]
                if set(self.model.cells_visible_faces[pos]) == set(position):
                    if self.position is None:
                        self.position = pos
                    else:
                        assert self.position == pos, (self.position, pos)
                    yield from innergen()
                    
    def not_conditions(self, conditions):
        for c in self.gray_conditions():
            if c not in conditions:
                yield c
                
    def rotated_conditions(self, idx):
        for rot in self.model.face_permutations.keys():
            pos = self.model.rotated_position[rot][idx]
            if pos == self.position:
                yield idx, rot
                
    def parse_prefix_condition(self, position, condition):
        if condition.startswith('!'):
            if condition.startswith('!*'):
                conditions = [c for c in self.parse_pattern_condition(position, condition[2:], False)]
            else:
                conditions = [c for c in self.parse_pattern_condition(position, condition[1:], True)]
            citer = self.not_conditions(conditions)
        elif condition.startswith('*'):
            #TODO: Instead of testing only rotated conditions, test all permutations.
            #      This should not break existing rules, and would allow to match
            #      e.g. dfr and dfl. Could be done by comparing sorted strings after
            #      expanding patterns.
            #DONE: use this in plugins module
            citer = self.parse_pattern_condition(position, condition[1:], False)
        else:
            citer = self.parse_pattern_condition(position, condition, True)
        for c in citer:
            self.union(*c)
                
    def parse_or_condition(self, position, condition):
        for c in condition.split('|'):
            self.parse_prefix_condition(position, c)
            
    def copy(self):
        cubie = self.__class__(self.model, self.position)
        cubie.extend(list(self))
        return cubie
        
    def count(self):
        return len(self)
        
    def union(self, idx, rot):
        self.append((idx, rot))
        
    def fork(self, condition):
        assert self.position == condition.position, (self.position, condition.position)
        assert self
        accepts, rejects = self.__class__(self.model, self.position), self.__class__(self.model, self.position)
        for c in self:
            if c in condition:
                accepts.append(c)
            else:
                rejects.append(c)
        return accepts or None, rejects or None
        
    def identify_rotation_blocks(self, maxis, mslice):
        return mslice == -1 or mslice == self.model.cell_indices[self.position][maxis]
        
    def transform(self, move):
        assert self
        axis, slice_, dir_ = move.data
        pos = self.position
        newpos = None
        irs = []
        if self.identify_rotation_blocks(axis, slice_):
            for idx, rot in self:
                newpos_, rot = self.model.rotate_symbolic(axis, dir_, pos, rot)
                if newpos is None:
                    newpos = newpos_
                else:
                    assert newpos == newpos_
                irs.append((idx, rot))
            assert newpos is not None
            cubie = self.__class__(self.model, newpos)
            cubie.extend(irs)
            assert cubie
            return cubie
        else:
            return self
            
    def check(self):
        if not self:
            warn('cubie is empty')
        if len(self) != len(set(self)):
            assert False, 'cubie has duplicates'
        if self == list(self.gray_conditions()):
            assert False, 'cubie is gray'
        
        
class PatternCube (dict):
    # key: position
    # value: list of (index, rotation)
    def __init__(self, model):
        self.model = model
        # empty list matches everything, cubies added restrict matches
        # a PatternCube object that never matches is invalid, use None instead
        
    def parse(self, conditions):
        self.parse_and_condition(conditions)
        return self
        
    def parse_and_condition(self, conditions):
        for pos, cond in conditions:
            cubie = PatternCubie(self.model).parse(pos, cond)
            assert cubie
            self[cubie.position] = cubie
            
    def __str__(self):
        return ' '.join(str(v) for v in self.values())
        
    def __bool__(self):
        return True
        
    def copy(self):
        cube = self.__class__(self.model)
        for k, v in self.items():
            cube[k] = v.copy()
        return cube
        
    def count(self):
        cnt = 1
        for pos in range(self.model.cell_count):
            if pos in self:
                cnt *= self[pos].count()
            else:
                cnt *= PatternCubie(self.model, pos).set_gray().count()
        return cnt
        
    def fork(self, condition):
        accepts = self.copy()
        rejects = []
        for pos, cond in condition.items():
            # indices in self but not in condition can be ignored
            if pos in self:
                cubie = self[pos]
            else:
                cubie = PatternCubie(self.model, pos).set_gray()
            accept, reject = cubie.fork(cond)
            if accept:
                if reject:
                    accepts[pos] = accept
                    reject_ = self.copy()
                    reject_[pos] = reject
                    reject_.check()
                    rejects.append(reject_)
                # else: accept is full cubie, nothing to do
            else:
                if reject:
                    self.check()
                    return None, [self.copy()]
                else:
                    assert False, 'empty PatternCube not allowed'
        return accepts, rejects
        
    def transform(self, move):
        cube = self.__class__(self.model)
        for pos, cubie in list(self.items()):
            cubie = cubie.transform(move)
            cube[cubie.position] = cubie
        return cube
        
    def check(self):
        for pos, cubie in self.items():
            assert pos == cubie.position
            cubie.check()
            
    def check_empty_simple(self):
        for pos1, cubie1 in self.items():
            if len(cubie1) == 1:
                idx1, rot1 = cubie1[0]
                for pos2, cubie2 in self.items():
                    if pos1 != pos2:
                        if [idx2 for idx2, rot2 in cubie2 if idx1 == idx2]:
                            cubie2[:] = [(idx2, rot2) for idx2, rot2 in cubie2 if idx1 != idx2]
                            if not cubie2:
                                return True
        return False
        
    def check_empty_full(self):
        indexlist = []
        for cubie in self.values():
            indexlist.append(set(idx for idx, rot in cubie))
        iters = deque()
        sample = deque()
        i = 0
        while i < len(indexlist):
            # new iter for i, must have at least one item
            iit = iter(indexlist[i])
            try:
                iidx = next(iit)
            except StopIteration:
                return True
            # idx must be unique
            forceloop = False
            while forceloop or iidx in sample:
                try:
                    iidx = next(iit)
                    forceloop = False
                except StopIteration:
                    # no unique idx, continue with prev iter
                    try:
                        iit = iters.pop()
                    except IndexError:
                        return True
                    sample.pop()
                    i -= 1
                    forceloop = True
            iters.append(iit)
            sample.append(iidx)
            i += 1
        return False
        
    def join(self, cube):
        issubset = True
        issuperset = True
        diffs = []
        for pos in range(self.model.cell_count):
            if pos in self:
                selfv = set(self[pos])
                if pos in cube:
                    cubev = set(cube[pos])
                    issubset = issubset and selfv.issubset(cubev)
                    issuperset = issuperset and selfv.issuperset(cubev)
                    if selfv != cubev:
                        diffs.append(pos)
                else:
                    issuperset = False
                    diffs.append(pos)
            else:
                if pos in cube:
                    issubset = False
                    diffs.append(pos)
                else:
                    pass
        if issubset:
            return cube
        if issuperset:
            return self
        if len(diffs) == 1:
            res = self.copy()
            pos = diffs[0]
            res[pos].clear()
            res[pos].extend(list(set(self[pos] + cube[pos])))
            assert res[pos]
            return res
        return None
            
    def rotate_colors(self, rot):
        perm = self.model.rotated_position[rot]
        cube = PatternCube(self.model)
        for pos, cubie in self.items():
            assert cubie
            permpos = perm.index(pos)
            newcubie = PatternCubie(self.model, pos)
            for cidx, crot in cubie:
                ridx = perm.index(cidx)
                rrot = self.model.norm_symbol(rot + crot)
                newcubie.union(ridx, rrot)
            assert newcubie
            cube[pos] = newcubie
        return cube
        
        

