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

#  Copyright © 2009-2015  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 debug, DEBUG_MSG, DEBUG_RAND, DEBUG_ROTATE
from .moves import MoveQueuePacked, MoveData


# This class is currently used to store the initial state of the cube
# and to provide the cube to plugins.
class CubeState:
    rand = random.Random()
    if DEBUG_RAND:
        print('rand.seed(123)')
        rand.seed(123)
    
    def __init__(self, model):
        self.model = model
        self._blocksn = []      # list of (block-position, rotation-symbol)
        self._blocksr = []      # list of (block-index, rotation-symbol)
        # if   self._blocksn[idx] == (pos, rot)
        # then self._blocksr[pos] == (idx, rot)
        
    def copy(self):
        other = CubeState(self.model)
        other._blocksn = deepcopy(self._blocksn)
        other._blocksr = deepcopy(self._blocksr)
        return other
        
    @property
    def rotations(self):
        return [blockn[1] for blockn in self._blocksn]
        
    def set_solved(self):
        # '' symbolic rotation for identity
        self._blocksn = [(i, '') for i in range(self.model.cell_count)]
        self._blocksr = [(i, '') for i in range(self.model.cell_count)]
        
    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 blockpos, cells_visible_faces in enumerate(self.model.cells_visible_faces):
            for face in cells_visible_faces:
                color = self.get_colorsymbol(blockpos, face)
                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[rot][index]
            if pos != block:
                return False  # Cubie at wrong place
        return True
        
    def identify_rotation_blocks(self, maxis, mslice):
        if mslice == -1:
            for i, blockn in enumerate(self._blocksn):
                yield i, blockn
        else:
            for i, blockn in enumerate(self._blocksn):
                bslice = self.model.cell_indices[blockn[0]][maxis]
                if bslice == mslice:
                    yield i, blockn
                
    def _rotate_slice(self, axis, slice_, dir_):
        if DEBUG_ROTATE:
            print('rotate axis={} slice={} dir={!s:5}\n  blocks:'.format(axis, slice_, dir_), end=' ')
        for idx, (pos, rot) in self.identify_rotation_blocks(axis, slice_):
            if DEBUG_ROTATE:
                print('{}:{}{}'.format(idx, pos, rot), end='')
            pos, rot = self.model.rotate_symbolic(axis, dir_, pos, rot)
            if DEBUG_ROTATE:
                print('-{}{}'.format(pos, rot), end=' ')
            self._blocksn[idx] = pos, rot
            self._blocksr[pos] = idx, rot
        if DEBUG_ROTATE:
            print()
        
    def get_colorsymbol(self, blockpos, facesym):
        rot = self._blocksr[blockpos][1]
        return self.model.face_symbolic_to_face_color(facesym, rot)
        
    def get_colornum(self, blockpos, facesym):
        colorsym = self.get_colorsymbol(blockpos, facesym)
        return self.model.faces.index(colorsym)
        
    def format_block(self):
        # 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)
        
    __str__ = format_block
    
    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) != self.model.cell_count:
            raise ValueError('wrong block count: %s, expected: %s' % (len(blocks), self.model.cell_count))
        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
        self._blocksr = [None] * len(blocksn)
        for i, (pos, rot) in enumerate(blocksn):
            self._blocksr[pos] = (i, rot)
        
    def random(self, count=-1):
        sizes = self.model.sizes
        count_moves = sum(sizes)*2
        randrange = self.rand.randrange
        if count < 0:
            count = 10 * count_moves + randrange(count_moves)
        if count:
            # The solved cube should be randomly rotated (lp: 1411999)
            for sym in self.rand.choice(self.model.rotation_symbols):
                self.rotate_slice(self.model.sym_to_move(sym))
        packed = MoveQueuePacked()
        #FIXME: workaround: for random Prism5* the last slice should not be used
        prism5workaround = count and self.model.type.startswith('Prism5')
        for i in range(count):
            candidates = list(range(count_moves))
            while candidates:
                mslice = candidates.pop(randrange(len(candidates)))
                mdir = bool(mslice % 2)
                mslice //= 2
                for maxis, size in enumerate(sizes):
                    if mslice < size:
                        break
                    mslice -= size
                else:
                    assert False
                if prism5workaround and maxis != 1:
                    if mslice == size-1:
                        continue
                move = packed.pack(MoveData(maxis, mslice, mdir), self.model)
                if packed.length() > i:
                    if (prism5workaround and maxis != 1) or not bool(move.get_full_moves(self.model)):
                        self._rotate_slice(maxis, mslice, mdir)
                        break
                packed.pack(MoveData(maxis, mslice, not mdir), self.model)
            else:
                break
        if DEBUG_MSG:
            print(count, 'random moves:', packed.format(self.model)[0])
            
    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=' ')
            
        blockidx1, blockrot1 = self._blocksr[blockpos1]
        blockpos2, blockrot1r = self.model.rotate_symbolic(maxis, mdir, blockpos1, blockrot1)
        blockidx2, blockrot2 = self._blocksr[blockpos2]
        blockpos2r, blockrot2r = self.model.rotate_symbolic(maxis, not mdir, blockpos2, blockrot2)
        if DEBUG_ROTATE:
            print('{}:{}{}->{}{}\n    to: {}:{}{}->{}{}'.format(
                blockidx1, blockpos1, blockrot1, blockpos2, blockrot1r,
                blockidx2, blockpos2, blockrot2, blockpos2r, blockrot2r))
        assert blockpos2r == blockpos1
        self._blocksn[blockidx1] = blockpos2, blockrot1r
        self._blocksr[blockpos2] = blockidx1, blockrot1r
        self._blocksn[blockidx2] = blockpos2r, blockrot2r
        self._blocksr[blockpos2r] = blockidx2, blockrot2r
        
    def rotate_block(self, blockpos, rdir):
        blockidx, blockrot = self._blocksr[blockpos]
        try:
            rot = self.model.inplace_rotations[blockpos][-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
        self._blocksr[blockpos] = blockidx, blockrot2
        if DEBUG_ROTATE:
            sym1, colorsym1 = self.model.blockpos_to_blocksym(blockpos, blockrot)
            sym2, colorsym2 = self.model.blockpos_to_blocksym(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.blockpos_to_blocksym(blockpos, blockrot)
            if allsyms or blocksym != colorsym:
                print(' {}:{}'.format(blocksym, colorsym), end='')
        print('')
        
        
