

import random
from random import randint

from direct.interval.FunctionInterval import Func
from direct.interval.Interval import Interval
from direct.interval.LerpInterval import LerpPosInterval
from direct.interval.MetaInterval import Sequence
from direct.particles.ParticleEffect import ParticleEffect
from panda3d.core import NodePath, CollisionBox, Point3, CollisionNode, Vec3, Spotlight, VBase4, PerspectiveLens, \
    GeomNode, MaterialCollection

import helper
import sound_manager
from helper import base, BUILDING_MASK, pedestrian_spawnpoints, minor_spawnpoints, building_colliders, \
    BUILDING_CLICK_MASK, PARTICLES, STARS, STARS_BUILDING_DESTRUCTION

# region settings

MIN_BLOCK_SIZE = 8
MAX_BLOCK_SIZE = 19
MIN_BUILDING_SIZE = 2
MAX_BUILDING_SIZE = 6
MIN_BUILDING_HEIGHT = 2
MAX_BUILDING_HEIGHT = 6
ROAD_WIDTH = 4
MARGIN = 2
PLAZA_CHANCE = 0.1
MINOR_SPAWNPOINT_CHANCE = 0.2
WINDOW_LIGHT_CHANCE = 0.2
CITY_Z = -0.1
BUILDING_DESTRUCTION_DURATION = 5
# endregion

DEBUG = False

# region flags
UNDECIDED = -9
PLAZA = 0
ROAD_START = 1
ROAD_INTERSECTION = 2
ROAD_CONT = -1
BUILDING_UNDECIDED = -4
BUILDING_ORANGE = 4
BUILDING_GREEN = 5
BUILDING_BLUE = 6
BUILDING_PURPLE = 7
# endregion

# region directions
NORTH = 0
WEST = 1
SOUTH = 2
EAST = 3
# endregion

instance = None


class City:
    def __init__(self, size=64):
        global instance
        instance = self
        # generate city
        # build a 64x64 grid
        self.size = size
        self.grid=[[UNDECIDED for _ in range(size)] for _ in range(size)]
        self.road_model = NodePath('city roads')
        self.road_model.reparent_to(base.render)

        # set aside borders for buildings and roads
        borders = (0, 1, -1, -2)
        for x in range(size):
            for b in borders:
                self.grid[x][b] = BUILDING_UNDECIDED
                self.grid[b][x] = BUILDING_UNDECIDED

        # draw N roads in each direction
        block_starts = [[], []]
        block_widths = [[], []]
        roads = [[2, -6], [2, -6]]
        for axis in range(2):
            x = ROAD_WIDTH + MARGIN
            max_x = self.size - 6
            while x < max_x - MAX_BLOCK_SIZE - MIN_BLOCK_SIZE - ROAD_WIDTH:
                block_widths[axis].append(randint(MIN_BLOCK_SIZE, MAX_BLOCK_SIZE))
                block_starts[axis].append(x)
                x += block_widths[axis][-1]
                roads[axis].append(x)
                x += ROAD_WIDTH
            space_left = max_x - x
            if space_left <= MAX_BLOCK_SIZE:
                block_widths[axis].append(space_left)
                block_starts[axis].append(x)
            else:
                if DEBUG:
                    print("space left = ", space_left)
                block_widths[axis].append(randint(
                    MIN_BLOCK_SIZE,
                    min(space_left - MIN_BLOCK_SIZE - ROAD_WIDTH, MAX_BLOCK_SIZE)
                ))
                block_starts[axis].append(x)
                x += block_widths[axis][-1]
                roads[axis].append(x)
                x += ROAD_WIDTH
                block_widths[axis].append(max_x - x)
                block_starts[axis].append(x)

            if DEBUG:
                print("ROADS:", roads)

            _intersection_fallout = 0
            for r in roads[axis]:
                for x in range(MARGIN, self.size - MARGIN):
                    if _intersection_fallout > 0:
                        _intersection_fallout -= 1
                        continue
                    elif axis == 1 and self.grid[r][x] in (ROAD_START, ROAD_CONT):
                        self.grid[r][x] = ROAD_INTERSECTION
                        pedestrian_spawnpoints.append(Vec3(r+2, x+2, 2))
                        _intersection_fallout = ROAD_WIDTH - 1
                    else:
                        if axis == 0:
                            self.grid[x][r] = ROAD_START
                            if random.random() < MINOR_SPAWNPOINT_CHANCE:
                                minor_spawnpoints.append(Vec3(x, r, 0))
                        else:
                            self.grid[r][x] = ROAD_START
                    for i in range(1, ROAD_WIDTH):
                        if axis == 0:
                            self.grid[x][r+i] = ROAD_CONT
                        else:
                            self.grid[r+i][x] = ROAD_CONT

        if DEBUG:
            for row in self.grid:
                print(row)

        for x in range(self.size):
            for y in range(self.size):
                h = 0
                lamp = False
                if self.grid[x][y] == ROAD_START:
                    path = 'city/road_straight'
                    if (x + y) % 4 == 0:
                        lamp = True
                    if self.grid[x][y+1] == ROAD_CONT:
                        h = 90
                elif self.grid[x][y] == ROAD_INTERSECTION:
                    path = 'city/road_intersect'
                elif self.grid[x][y] in (UNDECIDED, BUILDING_UNDECIDED):
                    path = 'city/plaza'
                else:
                    if self.grid[x][y] != ROAD_CONT:
                        print("hit else!", self.grid[x][y])
                    continue
                road = base.loader.load_model(path)
                road.reparent_to(self.road_model)
                road.set_pos(x, y, 0)
                road.set_h(h)
                if lamp:
                    l = base.loader.load_model('city/lamp_post')
                    l.reparent_to(self.road_model)
                    l.set_pos(x, y, 0)
                    l.set_h(h-90)

        self.road_model.flatten_strong()
        self.road_model.set_z(CITY_Z)

        print("starts", block_starts)
        print("widths", block_widths)
        print("roads", roads)
        self.blocks = []
        for xi in range(len(block_starts[0])):
            for yi in range(len(block_starts[1])):
                block = self.Block(
                    y=block_starts[0][xi],
                    x=block_starts[1][yi],
                    height=block_widths[0][xi],
                    width=block_widths[1][yi],
                    parent=self.road_model
                )
                self.blocks.append(block)
        self.blocks += [
            self.Block(x=0, y=0, width=self.size, height=MARGIN, parent=self.road_model),
            self.Block(x=MARGIN, y=self.size-MARGIN, width=self.size-(MARGIN*2), height=MARGIN, parent=self.road_model),
            self.Block(x=0, y=MARGIN, width=MARGIN, height=self.size-(MARGIN*2), parent=self.road_model),
            self.Block(x=self.size-MARGIN, y=0, width=MARGIN, height=self.size, parent=self.road_model),
        ]

        total_buildings = self.Building.counter[0]
        best_color = ''
        best_count = 9999
        for color, count in self.Building.color_counts.items():
            if best_count > count >= total_buildings/10:
                best_color = color
                best_count = count
        helper.COLOR_TO_WIN = best_color
        helper.BUILDINGS_TO_DESTROY = best_count

    def on_road(self, x, y):
        x = int(round(x))
        y = int(round(y))
        try:
            return self.grid[x][y] in (ROAD_CONT, ROAD_START, ROAD_INTERSECTION)
        except IndexError:
            return False

    class Block:
        def __init__(self, x, y, width, height, parent):
            if DEBUG:
                print("new block")
                wall = base.loader.load_model('city/wall')
                wall.reparent_to(base.render)
                wall.set_pos(x + (width/2), y + (height/2), 0)
                wall.set_scale(width, height, 0.5)
            self.x = x
            self.y = y
            self.width = width
            self.height = height
            self.parent = parent

            grid = [[UNDECIDED for _ in range(width)] for _ in range(height)]

            buildings = []
            # move along each edge and fit in whatever buildings we can
            for edge in (SOUTH, WEST, NORTH, EAST):
                d_edge, d_depth = 0, 0
                edge_row = []
                depth = 0
                if edge == SOUTH:
                    edge_row = [row[0] for row in grid]
                    depth = len(grid[0])
                elif edge == NORTH:
                    edge_row = [row[-1] for row in grid]
                    depth = len(grid[0])
                elif edge == EAST:
                    edge_row = grid[-1]
                    depth = len(grid)
                elif edge == WEST:
                    edge_row = grid[0]
                    depth = len(grid)

                edge_len = len(edge_row)
                while d_edge < edge_len:
                    # generate building rect that fits

                    while d_edge < edge_len and edge_row[d_edge] != UNDECIDED:
                        d_edge += 1
                    if d_edge == edge_len:
                        continue

                    # lambda to generate minx, maxx, miny, maxy
                    if edge == SOUTH:
                        xy = lambda b_width, b_depth: (d_edge, d_edge + b_width, 0, b_depth)
                    elif edge == NORTH:
                        xy = lambda b_width, b_depth: (d_edge, d_edge + b_width, depth - b_depth, depth)
                    elif edge == WEST:
                        xy = lambda b_width, b_depth: (0, b_depth, d_edge, d_edge + b_width)
                    elif edge == EAST:
                        xy = lambda b_width, b_depth: (depth - b_depth, depth, d_edge, d_edge + b_width)

                    available_widths = [x for x in range(MIN_BUILDING_SIZE, MAX_BUILDING_SIZE+1)]
                    available_depths = list(available_widths)

                    try:
                        while True:
                            building_depth = MIN_BUILDING_SIZE
                            building_width = available_widths.pop(randint(0, len(available_widths)-1))
                            if edge_len - d_edge - building_width < MIN_BUILDING_SIZE:
                                if edge_len != d_edge + building_width:
                                    continue
                            minx, maxx, miny, maxy = xy(building_width, building_depth)
                            grid_squares = [cell for row in grid[minx:maxx] for cell in row[miny:maxy]]
                            if all(cell == UNDECIDED for cell in grid_squares):
                                break
                    except ValueError as ex:
                        print(ex)
                        break

                    try:
                        while True:
                            building_depth = available_depths.pop(randint(0, len(available_depths)-1))
                            minx, maxx, miny, maxy = xy(building_width, building_depth)
                            grid_squares = [cell for row in grid[minx:maxx] for cell in row[miny:maxy]]
                            if any((cell != UNDECIDED for cell in grid_squares)):
                                continue
                            if depth - d_depth - building_depth >= MIN_BUILDING_SIZE:
                                break
                            if depth == d_depth + building_depth:
                                break
                        plaza = random.random() < PLAZA_CHANCE
                        minx, maxx, miny, maxy = xy(building_width, building_depth)
                        if DEBUG:
                            print("edge", edge)
                            print("bwidth/bdepth", building_width, building_depth)
                            print("grid dimensions", edge_len, depth)
                            print("xy:", minx, maxx, miny, maxy)
                    except ValueError as ex:
                        print(ex)
                        break

                    for row in grid[minx: maxx]:
                        for ddy in range(miny, maxy):
                            row[ddy] = PLAZA if plaza else BUILDING_UNDECIDED
                    if not plaza:
                        buildings.append(
                            (self.x + miny, self.y + minx, maxy-miny, maxx-minx)
                        )
                    if plaza:
                        pedestrian_spawnpoints.append(Vec3(self.x+miny, self.y+minx, min(maxy-miny, maxx-minx)))
                    d_edge += building_width
                    if DEBUG:
                        for row in grid:
                            print(row)
                        print('\n\n\n\n\n\n')

            self.buildings = []
            for building_args in buildings:

                free_sides = []
                y, x, length, width = building_args
                x -= self.y
                y -= self.x
                if x == 0:
                    free_sides.append(WEST)
                else:
                    adjacent_row = grid[x-1]
                    adjacent_squares = adjacent_row[y:y+length]
                    if all((s != BUILDING_UNDECIDED for s in adjacent_squares)):
                        free_sides.append(WEST)
                if x+width == len(grid):
                    free_sides.append(EAST)
                else:
                    adjacent_row = grid[x+width]
                    adjacent_squares = adjacent_row[y:y+length]
                    if all((s != BUILDING_UNDECIDED for s in adjacent_squares)):
                        free_sides.append(EAST)
                if y == 0:
                    free_sides.append(SOUTH)
                else:
                    adjacent_squares = [row[y-1] for row in grid[x: x+width]]
                    if all((s != BUILDING_UNDECIDED for s in adjacent_squares)):
                        free_sides.append(SOUTH)
                if y+length == len(grid[0]):
                    free_sides.append(NORTH)
                else:
                    adjacent_squares = [row[y+length] for row in grid[x: x+width]]
                    if all((s != BUILDING_UNDECIDED for s in adjacent_squares)):
                        free_sides.append(NORTH)

                self.buildings.append(
                    City.Building(*building_args, self.parent, free_sides)
                )

    class Building:
        counter = [0]
        color_counts = {
            'green': 0,
            'orange': 0,
            'purple': 0,
            'blue': 0,
        }

        def __init__(self, x, y, width, length, parent, free_sides):
            self.id = self.counter[0]
            self.counter[0] += 1
            self.name = "building {}".format(self.id)

            self.lit_windows = 0

            self.x = x
            self.y = y
            self.HP = 15
            self.width = width
            assert MIN_BUILDING_SIZE <= width <= MAX_BUILDING_SIZE
            self.length = length
            assert MIN_BUILDING_SIZE <= length <= MAX_BUILDING_SIZE
            self.parent = parent
            self.color = random.choice(['orange', 'green', 'blue', 'purple'])
            self.color_counts[self.color] += 1
            self.height = random.randint(MIN_BUILDING_HEIGHT, MAX_BUILDING_HEIGHT)
            self.model = parent.attach_new_node(self.color + ' building')
            self.model.set_pos(x, y, 0)

            colbox = CollisionBox(Point3(-0.75, -0.75, -10), Point3(width-0.25, length-0.25, self.height))
            self.collider = self.model.attach_new_node(CollisionNode(self.name))
            self.collider.node().add_solid(colbox)
            self.collider.node().set_tag('building', 't')
            self.collider.set_collide_mask(BUILDING_MASK)
            building_colliders[self.collider] = self

            colbox = CollisionBox(Point3(0, 0, 0), Point3(width-0.5, length-0.5, self.height))
            self.click_collider = self.model.attach_new_node(CollisionNode(self.color + ' building clicker'))
            self.click_collider.node().add_solid(colbox)
            self.click_collider.set_collide_mask(BUILDING_CLICK_MASK)
            building_colliders[self.click_collider] = self

            for facade in [NORTH, WEST, EAST, SOUTH]:
                if facade == NORTH:
                    p = lambda x, z: Point3(x, length-1, z)
                    h = 0
                elif facade == WEST:
                    p = lambda x, z: Point3(0, x, z)
                    h = 90
                elif facade == EAST:
                    p = lambda x, z: Point3(width-1, x, z)
                    h = 270
                else:  # SOUTH
                    p = lambda x, z: Point3(x, 0, z)
                    h = 180

                local_width = width if facade in (NORTH, SOUTH) else length
                for x in range(local_width):
                    self.wall(p(x, 0), h)
                    #if facade in free_sides:  # todo: fix this
                    if True:
                        for z in range(1, self.height):
                            if random.random() < WINDOW_LIGHT_CHANCE:
                                lit = 'light'
                                self.lit_windows += 1
                            else:
                                lit = 'dark'
                            if x == 0:
                                corner = 'corner_window_' + ('right' if facade in (NORTH, WEST) else 'left')
                            elif x == local_width - 1:
                                corner = 'corner_window_' + ('left' if facade in (NORTH, WEST) else 'right')
                            else:
                                corner = 'window'
                            window = base.loader.load_model('city/colored/{}_{}_{}'.format(corner, self.color, lit))
                            window.reparent_to(self.model)
                            window.set_pos(p(x, z))
                            window.set_h(h)
                            # for mat in window.findAllMaterials('*window*'):
                            #     helper.tweak_material(mat, '{} {}'.format(lit, self.color))
                            # window.set_color(1, 1, 1, 1)

            for x in range(1, width-1):
                for y in range(1, length-1):
                    self.wall(Point3(x, y, self.height-1), 0)

            self.model.flatten_strong()

        def __del__(self):
            try:
                del building_colliders[self.click_collider]
            except KeyError:
                pass
            try:
                del building_colliders[self.collider]
            except KeyError:
                pass
            self.model.remove_node()

        def damage(self):
            self.HP -= 1
            if self.HP == 0:
                STARS[1] += STARS_BUILDING_DESTRUCTION
                if self.color == helper.COLOR_TO_WIN:
                    helper.BUILDINGS_DESTROYED += 1
                sound_manager.crumble(self.center, self.id)
                from people.player import instance
                if instance.ally_target == self:
                    instance.ally_target = None
                if PARTICLES:
                    smoke_parent = base.render.attach_new_node('smoke parent')
                    smoke_parent.set_pos(self.center)
                    smoke = ParticleEffect()
                    smoke.loadConfig('particle/smoke.ptf')
                    smoke.set_scale(self.width+2, self.length+2, 1)
                    smoke.start(parent=smoke_parent, renderParent=smoke_parent)
                Sequence(
                    LerpPosInterval(self.model, BUILDING_DESTRUCTION_DURATION, (0, 0, -self.height)),
                    Func(sound_manager.uncrumble, self.id),
                    Func(self.__del__),
                    Interval('', BUILDING_DESTRUCTION_DURATION*4),
                    Func(smoke.cleanup) if PARTICLES else Interval('', 0),
                    name="building destruction"
                ).start()

        @property
        def center(self):
            return Point3(
                self.x + self.width/2,
                self.y + self.length/2,
                0
            )

        def wall(self, pos, h):
            wall = base.loader.load_model('city/wall')
            wall.reparent_to(self.model)
            wall.set_pos(pos)
            wall.set_h(h)






