# 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/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
#                           level1.py
#                     --------------------
#       date created : Fri Aug 10 2012
#          copyright : (C) 2012 Niels G. W. Serup
#      maintained by : Niels G. W. Serup <ns@metanohi.name>

"""
The fourth level.
"""

from __future__ import print_function

import os
import pygame
import random
import re
import itertools

import level
import player
import tile
import block
import lever
from mirror import Mirror
from laser import Laser
import misc
import worldobject

import logic.lasermirror as lm
from logic.direction import *

class Level4(level.Level):
    def __init__(self, game, graphics_dir, paused=False):
        level.Level.__init__(self, game, graphics_dir,
                             size=(64 * 16, 48 * 20), paused=paused)

        self.dimensions = 16, 16

        for i in range(self.dimensions[0]):
            for j in range(self.dimensions[1]):
                self.tiles.append(
                    tile.Tile(self, i*64, (j+4)*48,
                              self.imgs['indoor%d' % random.randint(1, 6)]))

        self.draw_background()

        self.playfield = lm.generate_simple_playfield(16)

        self.target_blocks = []
        for (x, y), t in self.playfield.items():
            x1, y1 = 64 * x, 48 * (y + 4)
            if isinstance(t, lm.Source):
                self.objects.append(
                    block.LaserSource(self, x1, y1, t.direction))
                continue
            def mir(b, x1, y1):
                def f(x, y):
                    def g(setting):
                        self.playfield[(x, y)] = lm.MirrorLeft \
                            if self.playfield[(x, y)] is lm.MirrorRight \
                            else lm.MirrorRight
                        self.generate_lasers()
                    return g
                return Mirror(self, x, y, b, links=[f(x, y)])
            def targ():
                b = block.LaserTarget(self, x, y)
                self.target_blocks.append(b)
                return b
            self.objects.append({
                lm.MirrorLeft: lambda: mir(True, x1, y1),
                lm.MirrorRight: lambda: mir(False, x1, y1),
                lm.Lever: lambda: lever.Lever(
                        self, x1, y1, [lambda setting: self.generate_lasers],
                        toggling=True,
                        anim='lever_leftright' if x in (0, 15)
                        else 'lever_updown'),
                lm.Target: targ,
                lm.Blocker: lambda: block.Block(self, x1, y1,
                                                self.imgs['block1'],
                                                movable=False)
            }[t]())
        mirrors = list(filter(lambda obj: isinstance(obj, Mirror),
                              self.objects))
        levers = list(filter(lambda obj: isinstance(obj, lever.Lever),
                             self.objects))
        random.shuffle(levers)
        for l in levers:
            m = min(mirrors, key=lambda m: misc.manhattan_dist(
                    (m.x, m.y), (l.x, l.y)))
            mirrors.remove(m)
            l.links.insert(0, (lambda m: lambda setting: m.rotate())(m))

        top = self.size[0] / 64
        for i in range(top):
            if not i % 3:
                self.objects.append(block.Block(
                        self, i * 64, 48 * 3,
                        self.imgs['wall'],
                        width=1 if i == top - 1 else 3,
                        blit_area=(0, 0, 96, 192) if i == top - 1 else None))
            self.objects.append(block.InvisBlock(self, i * 64,
                                                 self.size[1]))
        for i in range(self.size[1] / 48):
            self.objects.append(block.InvisBlock(self, - 64,
                                                 i * 48))
            self.objects.append(block.InvisBlock(self, self.size[0],
                                                 i * 48))

        self.generate_lasers()

        self.player.set_pos(64 * 7, 48 * 5)
        self.player.set_init_pos()

    def on_completion(self):
        # End game here.
        print('Well done.')
        
    def restart(self):
        for obj in self.objects:
            obj.reset_pos()

    def generate_lasers(self):
        lasers = lm.generate_lasers(self.playfield)
        self.lasers_orig = list(itertools.chain(*lasers))
        self.lasers = []
        for laser in lasers:
            laser = iter(laser)
            self.lasers.append(Laser(self, next(laser), first_laser=True))
            self.lasers.extend(Laser(self, line) for line in laser)
        for b in self.target_blocks:
            b.new_playfield_update()
        if all(b.glows for b in self.target_blocks):
            self.on_completion()

    def update(self, e, t, dt):
        level.Level.update(self, e, t, dt)
        for laser in self.lasers:
            laser.update(e, t, dt)
            
    def draw(self, window):
        self._blit_background(window)

        objs = self._sorted_objs(self.objects + self.lasers)
        objs = self._after_sort(itertools.groupby(
                objs, lambda obj: obj.y + obj.z))
        for obj in objs:
            obj.draw(window)

        self.darkness.draw(window)

    def _after_sort(self, objss):
        n_objs = []
        for c, objs in objss:
            n_objs.extend(self._after_sort_line(list(objs)))
        return n_objs

    def _after_sort_line(self, objs):
        is_special = lambda obj: type(obj) in \
            (Mirror, Laser, block.LaserSource, block.LaserTarget)
        specials, nonspecials = (filter(is_special, objs),
                                 filter(lambda obj: not is_special(obj), objs))
        return nonspecials + self._sort_line_specials(specials)

    def _sort_line_specials(self, objs):
        mirrors = filter(lambda obj: isinstance(obj, Mirror), objs)
        lasers = filter(lambda obj: isinstance(obj, Laser), objs)
        sources = filter(lambda obj: isinstance(obj, block.LaserSource), objs)
        targets = filter(lambda obj: isinstance(obj, block.LaserTarget), objs)

        lasers_back = set(lasers)
        sep_ords = []
        for obj in itertools.chain(mirrors, targets):
            before, after = [], []
            for laser in _hit_lasers(obj, lasers):
                lasers_back.discard(laser)
                if _obj_is_behind_laser(obj, laser):
                    after.append(laser)
                else:
                    before.append(laser)
            sep_ords.append(before + [obj] + after)
        points = set((obj.x0, obj.y0) for obj in itertools.chain(mirrors, targets))
        for laser in filter(lambda laser: (laser.x0, laser.y0) in points
                            and (laser.x1, laser.y1) in points, lasers):
            # print(laser)
            xs, ys = filter(lambda sep_ord: laser in sep_ord, sep_ords)
            sep_ords.remove(xs)
            sep_ords.remove(ys)

            xs, ys, nobjs = iter(xs), iter(ys), []
            while True:
                x = next(xs)
                if x is laser:
                    break
                nobjs.append(x)
            while True:
                x = next(ys)
                if x is laser:
                    break
                nobjs.append(x)

            nobjs.append(laser)
            nobjs.extend(xs)
            nobjs.extend(ys)
            sep_ords.append(nobjs)

        objs = list(itertools.chain(*sep_ords)) + list(lasers_back) \
            + sources
        return objs

def _hit_lasers(obj, lasers):
    p = (obj.x0, obj.y0)
    return filter(lambda laser: (laser.x0, laser.y0) == p
                  or (laser.x1, laser.y1) == p, lasers)

def _obj_is_behind_laser(obj, laser):
    return (_mirror_is_behind_laser
            if isinstance(obj, Mirror)
            else _target_is_behind_laser)(obj, laser)

def _mirror_is_behind_laser(mirror, laser):
    return \
        laser.y0 == laser.y1 \
        and (
        (mirror.left_up
         and (
                    (laser.x0 == mirror.x0 and laser.x1 > mirror.x0)
                    or
                    (laser.x1 == mirror.x0 and laser.x0 > mirror.x0)
                    )
         ) \
            or \
        (not mirror.left_up
         and (
                    (laser.x0 == mirror.x0 and laser.x1 < mirror.x0)
                    or
                    (laser.x1 == mirror.x0 and laser.x0 < mirror.x0)
                    )
         )
        )

def _target_is_behind_laser(target, laser):
    return laser.x0 != laser.x1
