"""
Simple OpenStreetMap renderer.

Author: Sergey Vartanov (me@enzet.ru).
"""
import argparse

import numpy as np
import os
import svgwrite
import sys

from colour import Color
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.shapes import Rect
from typing import Any, Dict, List

from roentgen import ui
from roentgen.constructor import (
    Constructor, Figure, Building, Segment)
from roentgen.point import Point, Occupied
from roentgen.flinger import Flinger
from roentgen.grid import draw_all_icons
from roentgen.icon import Icon, IconExtractor
from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader, OverpassReader
from roentgen.scheme import Scheme
from roentgen.direction import DirectionSet, Sector
from roentgen.util import MinMax

ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml"
MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"

AUTHOR_MODE = "user-coloring"
CREATION_TIME_MODE = "time"


class Painter:
    """
    Map drawing.
    """

    def __init__(
            self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
            mode: str, draw_captions: str, map_: Map, flinger: Flinger,
            svg: svgwrite.Drawing, icon_extractor: IconExtractor,
            scheme: Scheme):

        self.show_missing_tags: bool = show_missing_tags
        self.overlap: int = overlap
        self.draw_nodes: bool = draw_nodes
        self.mode: str = mode
        self.draw_captions: str = draw_captions

        self.map_: Map = map_
        self.flinger: Flinger = flinger
        self.svg: svgwrite.Drawing = svg
        self.icon_extractor = icon_extractor
        self.scheme: Scheme = scheme

    def draw(self, constructor: Constructor):
        """
        Draw map.
        """
        ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
        ways_length: int = len(ways)
        for index, way in enumerate(ways):  # type: Figure
            ui.progress_bar(index, ways_length, step=10, text="Drawing ways")
            path_commands: str = way.get_path(self.flinger)
            if path_commands:
                path = Path(d=path_commands)
                path.update(way.line_style.style)
                self.svg.add(path)
        ui.progress_bar(-1, 0, text="Drawing ways")

        # Trees

        for node in constructor.nodes:
            if not (node.get_tag("natural") == "tree" and
                    ("diameter_crown" in node.tags or
                     "circumference" in node.tags)):
                continue
            if "circumference" in node.tags:
                if "diameter_crown" in node.tags:
                    opacity = 0.7
                    radius = float(node.tags["diameter_crown"]) / 2
                else:
                    opacity = 0.3
                    radius = 2
                self.svg.add(self.svg.circle(
                    node.point,
                    radius * self.flinger.get_scale(node.coordinates),
                    fill=self.scheme.get_color("evergreen_color"),
                    opacity=opacity))
                self.svg.add(self.svg.circle(
                    node.point,
                    float(node.tags["circumference"]) / 2 / np.pi *
                    self.flinger.get_scale(node.coordinates),
                    fill="#B89A74"))

        # Draw building shade.

        building_shade: Group = Group(opacity=0.1)
        length: float = self.flinger.get_scale()

        for way in constructor.buildings:  # type: Building
            shift = np.array((length * way.get_levels(), 0))
            for nodes11 in way.inners + way.outers:
                for i in range(len(nodes11) - 1):  # type: int
                    flung_1 = self.flinger.fling(nodes11[i].coordinates)
                    flung_2 = self.flinger.fling(nodes11[i + 1].coordinates)
                    building_shade.add(Path(
                        ("M", flung_1, "L", flung_2, np.add(flung_2, shift),
                         np.add(flung_1, shift), "Z"),
                        fill="#000000", stroke="#000000", stroke_width=1))

        self.svg.add(building_shade)

        # Draw buildings.

        previous_level: float = 0
        level_height: float = self.flinger.get_scale()
        level_count: int = len(constructor.levels)

        for index, level in enumerate(sorted(constructor.levels)):
            ui.progress_bar(
                index, level_count, step=1, text="Drawing buildings")
            fill: Color()
            for way in constructor.buildings:  # type: Building
                if way.get_levels() < level:
                    continue
                shift_1 = [0, -previous_level * level_height]
                shift_2 = [0, -level * level_height]
                for segment in way.parts:  # type: Segment
                    if level == 0.5:
                        fill = Color("#AAAAAA")
                    elif level == 1:
                        fill = Color("#C3C3C3")
                    else:
                        color_part: float = 0.8 + segment.angle * 0.2
                        fill = Color(rgb=(color_part, color_part, color_part))

                    self.svg.add(self.svg.path(
                        d=("M", segment.point_1 + shift_1, "L",
                           segment.point_2 + shift_1,
                           segment.point_2 + shift_2,
                           segment.point_1 + shift_2,
                           segment.point_1 + shift_1, "Z"),
                        fill=fill.hex, stroke=fill.hex, stroke_width=1,
                        stroke_linejoin="round"))

            # Draw building roofs.

            for way in constructor.buildings:  # type: Building
                if way.get_levels() == level:
                    shift = np.array([0, -way.get_levels() * level_height])
                    path_commands: str = way.get_path(self.flinger, shift)
                    path = Path(d=path_commands, opacity=1)
                    path.update(way.line_style.style)
                    path.update({"stroke-linejoin": "round"})
                    self.svg.add(path)

            previous_level = level

        ui.progress_bar(-1, level_count, step=1, text="Drawing buildings")

        # Directions

        for node in constructor.nodes:  # type: Point

            angle = None
            is_revert_gradient: bool = False

            if node.get_tag("man_made") == "surveillance":
                direction = node.get_tag("camera:direction")
                if "camera:angle" in node.tags:
                    angle = float(node.get_tag("camera:angle"))
                if "angle" in node.tags:
                    angle = float(node.get_tag("angle"))
                direction_radius: float = (
                    25 * self.flinger.get_scale(node.coordinates))
                direction_color: Color = (
                    self.scheme.get_color("direction_camera_color"))
            elif node.get_tag("traffic_sign") == "stop":
                direction = node.get_tag("direction")
                direction_radius: float = (
                    25 * self.flinger.get_scale(node.coordinates))
                direction_color: Color = Color("red")
            else:
                direction = node.get_tag("direction")
                direction_radius: float = (
                    50 * self.flinger.get_scale(node.coordinates))
                direction_color: Color = (
                    self.scheme.get_color("direction_view_color"))
                is_revert_gradient = True

            if not direction:
                continue

            point = (node.point.astype(int)).astype(float)

            if angle:
                paths = [Sector(direction, angle).draw(point, direction_radius)]
            else:
                paths = DirectionSet(direction).draw(point, direction_radius)

            for path in paths:
                gradient = self.svg.defs.add(self.svg.radialGradient(
                    center=point, r=direction_radius,
                    gradientUnits="userSpaceOnUse"))
                if is_revert_gradient:
                    gradient \
                        .add_stop_color(0, direction_color.hex, opacity=0) \
                        .add_stop_color(1, direction_color.hex, opacity=0.7)
                else:
                    gradient \
                        .add_stop_color(0, direction_color.hex, opacity=0.4) \
                        .add_stop_color(1, direction_color.hex, opacity=0)
                self.svg.add(self.svg.path(
                    d=["M", point] + path + ["L", point, "Z"],
                    fill=gradient.get_paint_server()))

        # All other points

        if self.overlap == 0:
            occupied = None
        else:
            occupied = Occupied(
                self.flinger.size[0], self.flinger.size[1], self.overlap)

        nodes = sorted(constructor.nodes, key=lambda x: -x.priority)
        for index, node in enumerate(nodes):  # type: int, Point
            if (node.get_tag("natural") == "tree" and
                    ("diameter_crown" in node.tags or
                     "circumference" in node.tags)):
                continue
            ui.progress_bar(index, len(nodes), step=10, text="Drawing nodes")
            node.draw_shapes(self.svg, occupied)
        ui.progress_bar(-1, len(nodes), step=10, text="Drawing nodes")

        if self.draw_captions == "no":
            return

        for node in nodes:  # type: Point
            if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
                node.draw_texts(
                    self.svg, self.scheme, occupied, self.draw_captions)


def check_level_number(tags: Dict[str, Any], level: float):
    """
    Check if element described by tags is no the specified level.
    """
    if "level" in tags:
        levels = map(float, tags["level"].replace(",", ".").split(";"))
        if level not in levels:
            return False
    else:
        return False
    return True


def check_level_overground(tags: Dict[str, Any]) -> bool:
    """
    Check if element described by tags is overground.
    """
    if "level" in tags:
        try:
            levels = map(float, tags["level"].replace(",", ".").split(";"))
            for level in levels:
                if level <= 0:
                    return False
        except ValueError:
            pass
    if "layer" in tags:
        try:
            levels = map(float, tags["layer"].replace(",", ".").split(";"))
            for level in levels:
                if level <= 0:
                    return False
        except ValueError:
            pass
    if "parking" in tags and tags["parking"] == "underground":
        return False
    return True


def main(argv) -> None:
    """
    Röntgen entry point.

    :param argv: command-line arguments
    """
    if len(argv) == 2:
        if argv[1] == "grid":
            draw_all_icons("icon_grid.svg")
        return

    options: argparse.Namespace = ui.parse_options(argv)

    if not options:
        sys.exit(1)

    background_color: Color = Color("#EEEEEE")
    if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
        background_color: Color = Color("#111111")

    if options.input_file_name:
        input_file_name = options.input_file_name
    else:
        content = get_osm(options.boundary_box)
        if not content:
            ui.error("cannot download OSM data")
        input_file_name = [os.path.join("map", options.boundary_box + ".osm")]

    scheme: Scheme = Scheme(TAGS_FILE_NAME)

    if input_file_name[0].endswith(".json"):
        reader: OverpassReader = OverpassReader()
        reader.parse_json_file(input_file_name[0])
        map_ = reader.map_
        min1 = np.array((map_.boundary_box[0].min_, map_.boundary_box[1].min_))
        max1 = np.array((map_.boundary_box[0].max_, map_.boundary_box[1].max_))
    else:

        boundary_box = list(map(
            lambda x: float(x.replace('m', '-')), options.boundary_box.split(',')))

        full = False  # Full keys getting

        if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
            full = True

        osm_reader = OSMReader()

        for file_name in input_file_name:
            if not os.path.isfile(file_name):
                print("Fatal: no such file: " + file_name + ".")
                sys.exit(1)

            osm_reader.parse_osm_file(
                file_name, parse_ways=options.draw_ways,
                parse_relations=options.draw_ways, full=full)

        map_: Map = osm_reader.map_

        min1: np.array = np.array((boundary_box[1], boundary_box[0]))
        max1: np.array = np.array((boundary_box[3], boundary_box[2]))

    flinger: Flinger = Flinger(MinMax(min1, max1), options.scale)
    size: np.array = flinger.size

    svg: svgwrite.Drawing = (
        svgwrite.Drawing(options.output_file_name, size=size))
    svg.add(Rect((0, 0), size, fill=background_color))

    icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)

    def check_level(x) -> bool:
        """ Draw objects on all levels. """
        return True

    if options.level:
        if options.level == "overground":
            check_level = check_level_overground
        elif options.level == "underground":
            def check_level(x) -> bool:
                """ Draw underground objects. """
                return not check_level_overground(x)
        else:
            def check_level(x) -> bool:
                """ Draw objects on the specified level. """
                return not check_level_number(x, float(options.level))

    constructor: Constructor = Constructor(
        check_level, options.mode, options.seed, map_, flinger, scheme,
        icon_extractor)
    if options.draw_ways:
        constructor.construct_ways()
        constructor.construct_relations()
    if options.mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
        constructor.construct_nodes()

    painter: Painter = Painter(
        show_missing_tags=options.show_missing_tags, overlap=options.overlap,
        draw_nodes=options.draw_nodes, mode=options.mode,
        draw_captions=options.draw_captions,
        map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor,
        scheme=scheme)

    painter.draw(constructor)

    print("Writing output SVG...")
    svg.write(open(options.output_file_name, "w"))
    print("Done.")
