#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of ROBOTGAME
#
# ROBOTGAME 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.
#
# ROBOTGAME 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
# ROBOTGAME.  If not, see <http://www.gnu.org/licenses/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
#                         rollingstone.py
#                     --------------------
#       date created : Tue Aug 7 2012
#          copyright : (C) 2012 Niels G. W. Serup
#      maintained by : Niels G. W. Serup <ns@metanohi.name>

"""
Logic for a rolling stone on a playfield of movement-stopping stones and
direction-changing turns. Also has a pseudo-random playfield generator.
"""

from __future__ import print_function
import math
import random
import itertools

from robotgame.logic.direction import *
import robotgame.misc as misc

class Blocker(object):
    pass

def step(playfield, width, height, old_pos, direc):
    """
    Return a new (position, direction) tuple based on the location on the
    playfield.
    """
    pos = direc.next_pos(old_pos)
    x, y = pos
    if playfield.get(pos) is Blocker or x < 0 or x >= width \
            or y < 0 or y >= height:
        pos = old_pos
    elif isDirection(playfield.get(pos)):
        direc = playfield[pos]
    return pos, direc

def reaches_goal(playfield, width, height, max_steps, start_pos, goal_pos):
    """
    Determine if the rolling stone reaches the goal within max_steps steps.

    playfield[start_pos] must contain either a Turn(Down) or a Turn(Right)
    object, or the rolling stone will not roll.
    """
    pos = start_pos
    direc = playfield[pos]
    for _ in range(max_steps):
        new_pos, new_direc = step(playfield, width, height, pos, direc)
        if new_pos == goal_pos:
            return True
        if new_pos == pos:
            return False
        pos, direc = new_pos, new_direc
    return False


def generate_simple_playfield(width, height, nturns, nstones,
                              do_transpose=None, start_inside=True):
    """
    Generate a completable playfield where:
      * the starting position is in the upper left corner
      * the goal is in the lower right corner
      * the playfield is completable in nturns or less
      * the playfield has at most nstones stones

    Return (playfield : {(x, y): Direction | Blocker},
            steps : int)
    where (x, y) : (int, int)

    The returned playfield contains Direction objects which can be used with
    the step function to move towards the goal. The solution denoted by the
    Direction objects is not necessarily the only solution.

    'steps' is the number of steps used by the generated solution. It is not
    necessarily the lowest number of steps the playfield can be completed in.

    This generator favours increasing the turn density the closer to the goal
    it gets.
    """

    min_width, min_height = _min_play_size(nturns)
    if width < min_width or height < min_height:
        nturns = min(2 * (width - 1), 2 * (height - 1) - 1)
        min_width, min_height = _min_play_size(nturns)

    if do_transpose is None:
        do_transpose = random.choice((True, False))
    if do_transpose:
        width, height = height, width
        min_width, min_height = min_height, min_width

    turns = [((0, 0), None)]
    stones = []
    x, y = (0, 0)
    not_allowed_y = []
    offset_x = 0
    while True:
        missing = nturns - len(turns) + 1
        if missing == 1:
            turns[-1] = (turns[-1][0], Down)
            turns.append(((x, height - 1), Right))
            break
        elif missing == 0:
            turns[-1] = ((width - 1, turns[-1][0][1]), Down)
            break
        else:
            allowed = set(range(0, height)) - set(not_allowed_y)
            if missing <= 3:
                allowed -= set((height - 1,))
            if missing == nturns:
                allowed -= set((0,))
            y1 = random.choice(list(allowed))
            turns[-1] = (turns[-1][0], Down if y1 > y else Up)
            not_allowed_y.append(y1)
            if len(not_allowed_y) == 3:
                del not_allowed_y[0]
            turns.append(((x, y1), Right))
            x1p = random.randint(0, width - min_width - offset_x)
            offset_x += x1p
            x1 = x + x1p + 1
            turns.append(((x1, y1), None))
            x, y = x1, y1
    turns.append(((width - 1, height - 1), None))
    if not start_inside:
        del turns[0]

    if do_transpose:
        turns[:] = [((y, x), {
                    Down:  Right,
                    Right: Down,
                    Up:    Left,
                    }.get(d)) for ((x, y), d) in turns]
        width, height = height, width
        min_width, min_height = min_height, min_width

    used_fields = _fields_from_turns(turns)
    playfield = {}
    del turns[-1]
    for p, d in turns:
        playfield[p] = d

    for pos in misc.pick_random_elements(
        list(set(itertools.product(range(width), range(height)))
             - set(used_fields)), nstones):
        playfield[pos] = Blocker
    return playfield, len(used_fields) - 1

def generate_simple_unsolved_solvable_playfield(*args, **kwds):
    """
    Return a tuple of a playfield without direction objects, its number of
    steps, and a list of the direction objects.
    """
    playfield, steps = generate_simple_playfield(*args, **kwds)
    new_playfield, directions = {}, []
    for pos, val in playfield.items():
        if val is Blocker:
            new_playfield[pos] = val
        else:
            directions.append(val)
    return new_playfield, steps, directions

def generate_simple_unsolved_solvable_extra(*args, **kwds):
    """
    Do the same as generate_simple_unsolved_solvable, but throw in some copies
    of the direction object not returned by that function. You probably want to
    use this in your game.
    """
    playfield, steps, directions = generate_simple_unsolved_solvable_playfield(
        *args, **kwds)
    missing_dir = list(set(all_directions) - set(directions))[0]
    return playfield, steps, directions + \
        [missing_dir] * (len(directions) / 3) + [Right]

def print_playfield(playfield, width, height, hide_directions):
    text = [['·' for _ in range(width)] for _ in range(height)]
    for (x, y), val in playfield.items():
        if isDirection(val) and hide_directions:
            continue
        text[y][x] = '%' if val is Blocker else repr(val).rsplit('.', 1)[1][0] \
            if isDirection(val) else 'G'
    print('\n'.join(''.join(line) for line in text))

def _cells_upto(fields, start, direc, end):
    (x0, y0), (x2, y2) = start, end
    if direc in (Up, Down):
        t = -1 if direc == Up else 1
        for y in range(y0 + t, y2 + t, t):
            fields.append((x0, y))
    else:
        t = -1 if direc == Left else 1
        for x in range(x0 + t, x2 + t, t):
            fields.append((x, y0))

def _fields_from_turns(turns):
    fields = [(0, 0)]
    prev_pos, prev_direc = turns[0]
    for (pos, direc) in turns:
        _cells_upto(fields, prev_pos, prev_direc, pos)
        prev_pos, prev_direc = pos, direc
    return fields

def _min_play_size(nturns):
    return (int(math.ceil(nturns / 2.0)) + 1,
            int(math.ceil((nturns + 1) / 2.0)) + 1)
