# -*- coding: utf-8 -*-
import random
import pickle

from .utils import Coord, Movement, Rotation, T_Spin
from .tetromino import Tetromino, T_Tetrimino, I_Tetrimino
from .consts import (
    NB_LINES,
    NB_COLS,
    NB_NEXT,
    LOCK_DELAY,
    FALL_DELAY,
    AUTOREPEAT_DELAY,
    AUTOREPEAT_PERIOD,
    CURRENT_COORD,
    NEXT_COORDS,
    HELD_COORD,
)


LINES_CLEAR_NAME = "LINES_CLEAR_NAME"
CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343


class State:

    STARTING = "STARTING"
    PLAYING = "PLAYING"
    PAUSED = "PAUSED"
    OVER = "OVER"


class PieceContainer:

    def __init__(self):
        self.piece = None


class HoldQueue(PieceContainer):
    pass


class Matrix(list, PieceContainer):

    def __init__(self, *args, **kargs):
        list.__init__(self, *args, **kargs)
        PieceContainer.__init__(self)

    def cell_is_free(self, coord):
        return 0 <= coord.x < NB_COLS and 0 <= coord.y and not self[coord.y][coord.x]


class NextQueue(PieceContainer):

    def __init__(self):
        super().__init__()
        self.pieces = []


class TetrisLogic:

    NB_LINES = NB_LINES
    NB_COLS = NB_COLS
    NB_NEXT = NB_NEXT
    LOCK_DELAY = LOCK_DELAY
    FALL_DELAY = FALL_DELAY
    AUTOREPEAT_DELAY = AUTOREPEAT_DELAY
    AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD
    CURRENT_COORD = CURRENT_COORD
    NEXT_COORDS = NEXT_COORDS
    HELD_COORD = HELD_COORD
    random_bag = []

    def __init__(self):
        self.load_high_score()
        self.state = State.STARTING
        self.held = HoldQueue()
        self.matrix = Matrix()
        self.matrix.ghost = None
        self.next = NextQueue()
        self.time = 0
        self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop)
        self.pressed_actions = []
        self._score = 0

    def get_score(self):
        return self._score

    def set_score(self, new_score):
        self._score = new_score
        if self._score > self.high_score:
            self.high_score = self._score

    score = property(get_score, set_score)

    def new_game(self):
        self.level = 0
        self.score = 0
        self.nb_lines_cleared = 0
        self.goal = 0
        self.time = 0

        self.pressed_actions = []
        self.auto_repeat = False

        self.lock_delay = self.LOCK_DELAY
        self.fall_delay = self.FALL_DELAY

        self.matrix.clear()
        for y in range(self.NB_LINES + 3):
            self.append_new_line_to_matrix()
        self.next.pieces = [self.new_tetromino() for n in range(self.NB_NEXT)]
        self.held.piece = None
        self.state = State.PLAYING
        self.start(self.update_time, 1)

        self.new_level()

    def new_tetromino(self):
        if not self.random_bag:
            self.random_bag = list(Tetromino.shapes)
            random.shuffle(self.random_bag)
        return self.random_bag.pop()()

    def append_new_line_to_matrix(self):
        self.matrix.append([None for x in range(self.NB_COLS)])

    def new_level(self):
        self.level += 1
        self.goal += 5 * self.level
        if self.level <= 20:
            self.fall_delay = pow(0.8 - ((self.level - 1) * 0.007), self.level - 1)
        if self.level > 15:
            self.lock_delay = 0.5 * pow(0.9, self.level - 15)
        self.show_text("LEVEL\n{:n}".format(self.level))
        self.restart(self.fall, self.fall_delay)
        self.new_matrix_piece()

    def new_matrix_piece(self):
        self.matrix.piece = self.next.pieces.pop(0)
        self.matrix.piece.coord = self.CURRENT_COORD
        self.matrix.ghost = self.matrix.piece.ghost()
        self.move_ghost()
        self.next.pieces.append(self.new_tetromino())
        self.next.pieces[-1].coord = self.NEXT_COORDS[-1]
        for tetromino, coord in zip(self.next.pieces, self.NEXT_COORDS):
            tetromino.coord = coord

        if not self.can_move(self.matrix.piece.coord, (mino.coord for mino in self.matrix.piece)):
            self.game_over()

    def move_left(self):
        self.move(Movement.LEFT)

    def move_right(self):
        self.move(Movement.RIGHT)

    def rotate_clockwise(self):
        self.rotate(Rotation.CLOCKWISE)

    def rotate_counter(self):
        self.rotate(Rotation.COUNTER)

    def move_ghost(self):
        self.matrix.ghost.coord = self.matrix.piece.coord
        for ghost_mino, current_mino in zip(self.matrix.ghost, self.matrix.piece):
            ghost_mino.coord = current_mino.coord
        while self.can_move(self.matrix.ghost.coord + Movement.DOWN, (mino.coord for mino in self.matrix.ghost)):
            self.matrix.ghost.coord += Movement.DOWN

    def soft_drop(self):
        moved = self.move(Movement.DOWN)
        if moved:
            self.score += 1
        return moved

    def hard_drop(self):
        while self.move(Movement.DOWN, prelock=False):
            self.score += 2
        self.lock()

    def fall(self):
        self.move(Movement.DOWN)

    def move(self, movement, prelock=True):
        potential_coord = self.matrix.piece.coord + movement
        if self.can_move(potential_coord, (mino.coord for mino in self.matrix.piece)):
            if self.matrix.piece.prelocked:
                self.restart(self.lock, self.lock_delay)
            self.matrix.piece.coord = potential_coord
            if not movement == Movement.DOWN:
                self.matrix.piece.last_rotation_point = None
            self.move_ghost()
            return True
        else:
            if prelock and not self.matrix.piece.prelocked and movement == Movement.DOWN:
                self.matrix.piece.prelocked = True
                self.start(self.lock, self.lock_delay)
            return False

    def rotate(self, rotation):
        rotated_coords = tuple(Coord(rotation * mino.coord.y, -rotation * mino.coord.x) for mino in self.matrix.piece)
        for rotation_point, liberty_degree in enumerate(self.matrix.piece.SRS[rotation][self.matrix.piece.orientation], start=1):
            potential_coord = self.matrix.piece.coord + liberty_degree
            if self.can_move(potential_coord, rotated_coords):
                if self.matrix.piece.prelocked:
                    self.restart(self.lock, self.lock_delay)
                self.matrix.piece.coord = potential_coord
                for mino, coord in zip(self.matrix.piece, rotated_coords):
                    mino.coord = coord
                self.matrix.piece.orientation = (self.matrix.piece.orientation + rotation) % 4
                self.matrix.piece.last_rotation_point = rotation_point
                self.move_ghost()
                return True
        else:
            return False

    SCORES = (
        {LINES_CLEAR_NAME: "", T_Spin.NONE: 0, T_Spin.MINI: 1, T_Spin.T_SPIN: 4},
        {LINES_CLEAR_NAME: "SINGLE", T_Spin.NONE: 1, T_Spin.MINI: 2, T_Spin.T_SPIN: 8},
        {LINES_CLEAR_NAME: "DOUBLE", T_Spin.NONE: 3, T_Spin.T_SPIN: 12},
        {LINES_CLEAR_NAME: "TRIPLE", T_Spin.NONE: 5, T_Spin.T_SPIN: 16},
        {LINES_CLEAR_NAME: "TETRIS", T_Spin.NONE: 8},
    )

    def lock(self):
        self.matrix.piece.prelocked = False
        self.stop(self.lock)

        # Piece unlocked
        if self.can_move(self.matrix.piece.coord + Movement.DOWN, (mino.coord for mino in self.matrix.piece)):
            return

        # Game over
        if all((mino.coord + self.matrix.piece.coord).y >= self.NB_LINES for mino in self.matrix.piece):
            self.game_over()
            return

        if self.pressed_actions:
            self.auto_repeat = False
            self.restart(self.repeat_action, self.AUTOREPEAT_DELAY)

        # T-Spin
        if type(self.matrix.piece) == T_Tetrimino and self.matrix.piece.last_rotation_point is not None:
            a = self.is_t_slot(0)
            b = self.is_t_slot(1)
            c = self.is_t_slot(3)
            d = self.is_t_slot(2)
            if self.matrix.piece.last_rotation_point == 5 or (a and b and (c or d)):
                t_spin = T_Spin.T_SPIN
            elif c and d and (a or b):
                t_spin = T_Spin.MINI
            else:
                t_spin = T_Spin.NONE
        else:
            t_spin = T_Spin.NONE

        for mino in self.matrix.piece:
            coord = mino.coord + self.matrix.piece.coord
            del mino.coord
            if coord.y <= self.NB_LINES + 3:
                self.matrix[coord.y][coord.x] = mino

        # Clear complete lines
        nb_lines_cleared = 0
        for y, line in reversed(list(enumerate(self.matrix))):
            if all(mino for mino in line):
                nb_lines_cleared += 1
                self.matrix.pop(y)
                self.append_new_line_to_matrix()
        if nb_lines_cleared:
            self.nb_lines_cleared += nb_lines_cleared

        # Scoring
        lock_strings = []
        lock_score = 0

        if t_spin:
            lock_strings.append(t_spin)
        if nb_lines_cleared:
            lock_strings.append(self.SCORES[nb_lines_cleared][LINES_CLEAR_NAME])
            self.combo += 1
        else:
            self.combo = -1

        if nb_lines_cleared or t_spin:
            ds = self.SCORES[nb_lines_cleared][t_spin]
            self.goal -= ds
            ds *= 100 * self.level
            lock_score += ds
            lock_strings.append(str(ds))
            self.show_text("\n".join(lock_strings))

        if self.combo >= 1:
            ds = (20 if nb_lines_cleared == 1 else 50) * self.combo * self.level
            lock_score += ds
            self.show_text("COMBO x{:n}\n{:n}".format(self.combo, ds))

        self.score += lock_score

        if self.goal <= 0:
            self.new_level()
        else:
            self.new_matrix_piece()

    def can_move(self, potential_coord, minoes_coords):
        return all(self.matrix.cell_is_free(potential_coord + mino_coord) for mino_coord in minoes_coords)

    T_SLOT_COORDS = (Coord(-1, 1), Coord(1, 1), Coord(-1, 1), Coord(-1, -1))

    def is_t_slot(self, n):
        t_slot_coord = self.matrix.piece.coord + self.T_SLOT_COORDS[(self.matrix.piece.orientation + n) % 4]
        return not self.matrix.cell_is_free(t_slot_coord)

    def swap(self):
        if self.matrix.piece.hold_enabled:
            self.matrix.piece.hold_enabled = False
            self.matrix.piece.prelocked = False
            self.stop(self.lock)
            self.matrix.piece, self.held.piece = self.held.piece, self.matrix.piece
            self.held.piece.coord = self.HELD_COORD
            if type(self.held.piece) == I_Tetrimino:
                self.held.piece.coord += Movement.LEFT
            for mino, coord in zip(self.held.piece, self.held.piece.MINOES_COORDS):
                mino.coord = coord

            if self.matrix.piece:
                self.matrix.piece.coord = self.CURRENT_COORD
                self.matrix.ghost = self.matrix.piece.ghost()
                self.move_ghost()
            else:
                self.new_matrix_piece()

    def pause(self):
        self.state = State.PAUSED
        self.stop_all()
        self.pressed_actions = []
        self.auto_repeat = False
        self.stop(self.repeat_action)

    def resume(self):
        self.state = State.PLAYING
        self.start(self.fall, self.fall_delay)
        if self.matrix.piece.prelocked:
            self.start(self.lock, self.lock_delay)
        self.start(self.update_time, 1)

    def game_over(self):
        self.state = State.OVER
        self.stop_all()
        self.save_high_score()

    def stop_all(self):
        self.stop(self.fall)
        self.stop(self.lock)
        self.stop(self.update_time)

    def update_time(self):
        self.time += 1

    def do_action(self, action):
        action()
        if action in self.autorepeatable_actions:
            self.auto_repeat = False
            self.pressed_actions.append(action)
            if action == self.soft_drop:
                delay = self.fall_delay / 20
            else:
                delay = self.AUTOREPEAT_DELAY
            self.restart(self.repeat_action, delay)

    def repeat_action(self):
        if self.pressed_actions:
            self.pressed_actions[-1]()
            if not self.auto_repeat:
                self.auto_repeat = True
                self.restart(self.repeat_action, self.AUTOREPEAT_PERIOD)
        else:
            self.auto_repeat = False
            self.stop(self.repeat_action)

    def remove_action(self, action):
        if action in self.autorepeatable_actions:
            try:
                self.pressed_actions.remove(action)
            except ValueError:
                pass

    def show_text(self, text):
        print(text)
        raise Warning("TetrisLogic.show_text not implemented.")

    def load_high_score(self, crypted_high_score=None):
        if crypted_high_score:
            crypted_high_score = int(pickle.loads(crypted_high_score))
            self.high_score = crypted_high_score ^ CRYPT_KEY
        else:
            raise Warning(
                """TetrisLogic.load_high_score not implemented.
High score is set to 0"""
            )
            self.high_score = 0

    def save_high_score(self):
        crypted_high_score = self.high_score ^ CRYPT_KEY
        crypted_high_score = pickle.dumps(crypted_high_score)
        return crypted_high_score

    def start(task, period):
        raise Warning("TetrisLogic.start is not implemented.")

    def stop(self, task):
        raise Warning("TetrisLogic.stop is not implemented.")

    def restart(self, task, period):
        self.stop(task)
        self.start(task, period)
