"""Output rendering logic for GeoJSON."""
from datetime import datetime
from typing import Optional

import orjson
from django.contrib.gis.geos import GEOSGeometry
from django.db import models
from django.utils.timezone import utc
from gisserver.features import FeatureType

from .base import BytesBuffer, OutputRenderer

WRITE_BUFFER_SIZE = 4096


class GeoJsonRenderer(OutputRenderer):
    """Fast GeoJSON renderer, using a stream response.

    The complex encoding bits are handled by the C-library "orjson"
    and the geojson property of GEOSGeometry.

    NOTE: While Django has a GeoJSON serializer
    (see https://docs.djangoproject.com/en/3.0/ref/contrib/gis/serializers/),
    it does not offer streaming response handling.
    """

    content_type = "application/json; charset=utf-8"

    def render_stream(self):
        output = BytesBuffer()

        # Generate the header from a Python dict,
        # but replace the last "}" into a comma, to allow writing more
        header = self.get_header()
        header_bytes = orjson.dumps(header)[:-1]

        # Have a temporary buffer that is written
        output.write(header_bytes)
        output.write(b',\n  "features": [\n')

        # number_returned is None for this data, as the SQL COUNT is avoided.
        number_returned = 0

        # Flatten the results, they are not grouped in a second FeatureCollection
        is_first_collection = True
        for sub_collection in self.collection.results:
            if is_first_collection:
                is_first_collection = False
            else:
                output.write(b",\n")

            is_first = True
            for instance in sub_collection:
                if is_first:
                    is_first = False
                else:
                    output.write(b",\n")

                # The "properties" object is generated by orjson.dumps(),
                # while the "geometry" object built-in using GEOSGeometry.geojson
                output.write(self.render_feature(sub_collection.feature_type, instance))
                number_returned += 1

                # Only perform a 'yield' every once in a while,
                # as it goes back-and-forth for writing it to the client.
                if output.is_full():
                    yield output.getvalue()
                    output.clear()

        # Instead of performing an expensive .count() on the start of the page,
        # write this as a last field at the end of the response.
        # This still honors the WFS 30 DRAFT without sacrificing performance.
        output.write(b"\n  ],\n")
        output.write(b'  "numberReturned": %d' % number_returned)
        output.write(b"}\n")
        yield output.getvalue()

    def render_feature(
        self, feature_type: FeatureType, instance: models.Model
    ) -> bytes:
        """Render the output of a single feature"""

        # Get geo value
        geo_value = getattr(instance, feature_type.geometry_field_name)

        # Get all instance attributes:
        properties = self.get_properties(feature_type, instance)

        return (
            b"    {"
            b'"type": "Feature", '
            b'"id":%b, '
            b'"geometry_name":%b,'
            b'"geometry":%b,'
            b'"properties":%b'
            b"}"
        ) % (
            orjson.dumps(f"{feature_type.name}.{instance.pk}"),
            orjson.dumps(str(instance)),
            self.render_geometry(geo_value),
            orjson.dumps(properties),
        )

    def _format_geojson_value(self, value):
        if isinstance(value, datetime):
            return value.astimezone(utc)
        else:
            return value

    def render_geometry(self, geometry: Optional[GEOSGeometry]) -> bytes:
        """Generate the proper GeoJSON notation for a geometry"""
        if geometry is None:
            return b"null"

        self.output_crs.apply_to(geometry)
        return geometry.json.encode()

    def get_header(self) -> dict:
        """Generate the header fields.

        The format is based on the WFS 3.0 DRAFT which defines the extra
        pagination headers and numberMatched/numberReturned headers. The
        'numberReturned' is not included here, as it's written by the main
        render method.
        """
        return {
            "type": "FeatureCollection",
            "links": self.get_links(),
            "timeStamp": self._format_geojson_value(self.collection.timestamp),
            "numberMatched": self.collection.number_matched,
            # "numberReturned": is written at the end for better query performance.
            "crs": {"type": "name", "properties": {"name": str(self.output_crs)}},
        }

    def get_links(self) -> list:
        """Generate the pagination links"""
        links = []
        if self.collection.next:
            links.append(
                {
                    "href": self.collection.next,
                    "rel": "next",
                    "type": "application/geo+json",
                    "title": "next page",
                }
            )
        if self.collection.previous:
            links.append(
                {
                    "href": self.collection.previous,
                    "rel": "previous",
                    "type": "application/geo+json",
                    "title": "previous page",
                }
            )
        return links

    def get_properties(self, feature_type: FeatureType, instance: models.Model) -> dict:
        """Collect the data for the 'properties' field"""
        props = {}
        for name in feature_type.fields:
            if name not in feature_type.geometry_field_names:
                try:
                    value = getattr(instance, name)
                except AttributeError:
                    # E.g. Django foreign keys that point to a non-existing member.
                    props[name] = None
                else:
                    props[name] = self._format_geojson_value(value)

        return props
