#-*- 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/>.


import random
from copy import deepcopy

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


# This class is currently used to store the initial state of the cube
# and to provide the cube to plugins.
class CubeState (object):
    rand = random.Random()
    if DEBUG_RAND:
        print('rand.seed(123)')
        rand.seed(123)
    
    def __init__(self, model):
        self.model = model
        self._blocksn = None
        
    def copy(self):
        other = CubeState(self.model)
        other._blocksn = deepcopy(self._blocksn)
        return other
        
    @property
    def type(self):
        return self.model.type
    @property
    def size(self):
        return self.model.size
    @property
    def blocks(self):
        assert self._blocksn is not None
        blocks = [self.model.rotation_symbolic_to_matrix(*blockn)
                        for blockn in self._blocksn]
        return blocks
    
    def set_model(self, model):
        assert self.model.type == model.type
        assert self.model.sizes == model.sizes
        self.model = model
        
    def set_solved(self):
        # '' symbolic rotation for identity
        self._blocksn = [(i, '') for i, unused_block in enumerate(self.model.blocks)]
        
    def is_solved(self):
        '''Check whether the cube is solved.
        
        Only test the colors of the visible faces.
        Cubes with rotated center faces are treated as solved.
        '''
        face_colors = {}
        for index, (unused_pos, rot) in enumerate(self._blocksn):
            for face in self.model.blocks[index].visible_faces:
                color = self.get_colorsymbol(index, face.lower())
                try:
                    color_ref = face_colors[face]
                except KeyError:
                    face_colors[face] = color
                else:
                    if color != color_ref:
                        return False
        return True
        
    def is_solved_strict(self):
        '''Check whether the cube is solved.
        
        Test the rotation and position of the blocks.
        Cubes with rotated center faces are treated as not solved.
        '''
        #FIXME: currently unused function, should be used for faces with images
        allrot = self._blocksn[0][1]
        for index, (pos, rot) in enumerate(self._blocksn):
            if rot != allrot:
                return False  # Cubie rotated
            block = self.model.rotated_position[index, rot]
            if pos != block:
                return False  # Cubie at wrong place
        return True
        
    def identify_rotation_blocks(self, maxis, mslice):
        assert self._blocksn
        if mslice == -1:
            for i in range(len(self._blocksn)):
                yield i, True
        else:
            for i, blockn in enumerate(self._blocksn):
                bslice = self.model.blocks[blockn[0]].axis_to_slice(maxis)
                yield i, bslice == mslice
                
    def _rotate_slice(self, axis, slice_, dir_):
        assert self._blocksn
        
        if DEBUG_ROTATE:
            print('rotate axis={} slice={} dir={!s:5}\n  blocks:'.format(axis, slice_, dir_), end=' ')
        for block_id, selected in self.identify_rotation_blocks(axis, slice_):
            if  selected:
                if DEBUG_ROTATE:
                    print('{}:{}{}'.format(block_id, *self._blocksn[block_id]), end='')
                block = self.model.rotate_symbolic(axis, dir_, *self._blocksn[block_id])
                if DEBUG_ROTATE:
                    print('-{}{}'.format(*block), end=' ')
                self._blocksn[block_id] = block
        if DEBUG_ROTATE:
            print()
        
    def get_colorsymbol(self, blocknum, facesym):
        for pos, rot in self._blocksn:
            if pos == blocknum:
                return self.model.face_symbolic_to_face_color(facesym, rot)
        assert False, 'Should not be reached'
        
    def get_colornum(self, blocknum, facesym):
        #FIXME: face and color symbols should be upper
        colorsym = self.get_colorsymbol(blocknum, facesym.lower())
        return self.model.faces.index(colorsym.upper())
        
    def format_block(self):
        assert self._blocksn is not None
        # every block is stored as pos-sym, where sym is a symbolic rotation
        blocks = ['{}-{}'.format(pos, sym) for pos, sym in self._blocksn]
        return 'idx-rot: ' + ' '.join(blocks)
        
    def parse_block(self, blocks):
        if blocks == 'solved':
            return self.set_solved()
        bformat, blocks = blocks.split(':', 1)
        if bformat != 'idx-rot':
            raise ValueError('unknown block format:', bformat)
        blocks = blocks.strip().split(' ')
        if len(blocks) != len(self.model.blocks):
            raise ValueError('wrong block count: %s, expected: %s' % (len(blocks), len(self.model.blocks)))
        blocksn = []
        for block in blocks:
            # every block is stored as idx-rot, where idx: index to blocklist, rot: symbolic rotation
            block = block.strip().split('-', 1)
            index, rot = block
            index = int(index)
            rot = self.model.norm_symbol(rot)
            blocksn.append((index, rot))
        # test whether block indices is a permutation,
        # in fact thats not enough, e.g. swap a corner cubie with an edge,
        # also cubie rotation may be invalid, it can be possible that a
        # label is rotated inside the cube.
        for i1, i2 in enumerate(sorted(i for i, r in blocksn)):
            if i1 != i2:
                raise ValueError('block list is not a permutation')
        self._blocksn = blocksn
        
    def randomize(self, count=-1):
        if not sum(self.model.sizes):
            return
        if count >= 0:
            midslice = -1
            count_ = count
        else:
            midslice = [s // 2 for s in self.model.sizes]
            count_ = 20 * sum(self.model.sizes)
        cubemoves = []
        debug('random moves:', count_)
        lastaxis = None
        lastslices = []
        for unused_i in range(count_):
            while True:
                maxis = self.rand.randrange(3)
                mdir = bool(self.rand.randrange(2))
                msign = -1 if mdir else 1
                mslice = self.rand.randrange(self.model.sizes[maxis])
                if maxis != lastaxis:
                    lastaxis = maxis
                    lastslices = [0] * self.model.sizes[maxis]
                    lastslices[mslice] = msign
                    break # subsequent different axes are accepted
                nslices = lastslices[mslice] + msign
                if abs(nslices) < abs(lastslices[mslice]):
                    continue # moves that revert a previous one are rejected
                if abs(nslices * self.model.symmetry[maxis]) > 180.:
                    continue # moves that rotate a slice more than 180° are rejected
                lastslices[mslice] += msign
                if (sum(1 for ls in lastslices if ls > 0 or ls * self.model.symmetry[maxis] == -180.)
                        > self.model.sizes[maxis] // 2):
                    lastslices[mslice] -= msign
                    continue
                if (sum(1 for ls in lastslices if ls < 0 or ls * self.model.symmetry[maxis] == 180.)
                        > self.model.sizes[maxis] // 2):
                    lastslices[mslice] -= msign
                    continue
                break
            if mslice == midslice:
                cubemoves.append((maxis, mdir))
            self._rotate_slice(maxis, mslice, mdir)
        for maxis, mdir in reversed(cubemoves):
            self._rotate_slice(maxis, -1, not mdir)
            
    def rotate_slice(self, move_data):
        self._rotate_slice(*move_data)
        
    def swap_block(self, blockpos1, maxis, mslice, mdir):
        if DEBUG_ROTATE:
            print('rotate axis={} slice={} dir={!s:5}\n  from:'.format(maxis, mslice, mdir), end=' ')
            
        for blockidx1, (pos, blockrot1) in enumerate(self._blocksn):
            if pos == blockpos1:
                break
        else:
            blockidx1 = blockrot1 = None
            assert False
        blockpos1r, blockrot1r = self.model.rotate_symbolic(maxis, mdir, blockpos1, blockrot1)
        if DEBUG_ROTATE:
            print('{}:{}{}->{}{}\n    to:'.format(blockidx1, blockpos1, blockrot1,
                                                 blockpos1r, blockrot1r), end=' ')
        for blockidx2, unused_selected in self.identify_rotation_blocks(maxis, mslice):
            blockpos2, blockrot2 = self._blocksn[blockidx2]
            if blockpos1r == blockpos2:
                self._blocksn[blockidx1] = blockpos1r, blockrot1r
                blockpos2r, blockrot2r = self.model.rotate_symbolic(maxis, not mdir, blockpos2, blockrot2)
                if DEBUG_ROTATE:
                    print('{}:{}{}->{}{}'.format(blockidx2, blockpos2, blockrot2,
                                                blockpos2r, blockrot2r), end=' ')
                self._blocksn[blockidx2] = blockpos2r, blockrot2r
                break
        if DEBUG_ROTATE:
            print()
        
    def rotate_block(self, blockpos, rdir):
        for blockidx, (pos, blockrot) in enumerate(self._blocksn):
            if pos == blockpos:
                break
        else:
            blockidx = blockrot = None
            assert False
        block = self.model.blocks[blockpos]
        try:
            rot = block.inplace_rotations[-1 if rdir else 0]
        except IndexError:
            # not every block can be rotated inline: e.g. edges and center faces on the 4×4×4-Cube,
            # edges and corners on towers and bricks
            #TODO: swap edge at pos n with the one at pos (size-1 - n),
            #      rotate all center faces on the same ring
            return
        blockrot2 = self.model.norm_symbol(blockrot + rot)
        self._blocksn[blockidx] = blockpos, blockrot2
        if DEBUG_ROTATE:
            sym1, colorsym1 = self.model.block_index_to_block_symbolic(blockpos, blockrot)
            sym2, colorsym2 = self.model.block_index_to_block_symbolic(blockpos, blockrot2)
            print('{}:{}{}->{}{} ({}:{}->{}:{})'.format(blockidx, blockpos, blockrot, blockpos, blockrot2,
                                                      sym1, colorsym1, sym2, colorsym2))
            self.debug_blocksymbols(allsyms=False)
            
    def debug_blocksymbols(self, allsyms):
        for blockpos, blockrot in self._blocksn:
            blocksym, colorsym = self.model.block_index_to_block_symbolic(blockpos, blockrot)
            if allsyms or blocksym != colorsym:
                print(' {}:{}'.format(blocksym, colorsym), end='')
        print('')
        
        
