# Copyright (c) 2019, Battelle Memorial Institute
# All rights reserved.
#
# See LICENSE.txt and WARRANTY.txt for details.

import typing

from openlocationcode import openlocationcode

from buildingid.code_pattern import (
    FORMAT_STRING_,
    RE_GROUP_EAST_,
    RE_GROUP_NORTH_,
    RE_GROUP_OPENLOCATIONCODE_,
    RE_GROUP_SOUTH_,
    RE_GROUP_WEST_,
    RE_PATTERN_,
)
from buildingid.validators import (
    isValidCodeArea,
    isValidCodeLength,
    isValidLatitude,
    isValidLatitudeCenter,
    isValidLongitude,
    isValidLongitudeCenter,
)

Code = typing.NewType("Code", str)


class CodeArea(openlocationcode.CodeArea):
    def __init__(self, centroid: openlocationcode.CodeArea, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        self.centroid = centroid

    @property
    def area(self) -> float:
        height = float(self.latitudeHi - self.latitudeLo)
        width = float(self.longitudeHi - self.longitudeLo)

        return height * width

    def encode(self) -> Code:
        return encode(
            self.latitudeLo,
            self.longitudeLo,
            self.latitudeHi,
            self.longitudeHi,
            self.centroid.latitudeCenter,
            self.centroid.longitudeCenter,
            codeLength=self.codeLength,
        )

    def intersection(self, other: "CodeArea") -> tuple[float, float, float, float] | None:
        if other is None:
            return None

        latitudeLo = float(max(self.latitudeLo, other.latitudeLo))
        latitudeHi = float(min(self.latitudeHi, other.latitudeHi))
        longitudeLo = float(max(self.longitudeLo, other.longitudeLo))
        longitudeHi = float(min(self.longitudeHi, other.longitudeHi))

        if (latitudeLo > latitudeHi) or (longitudeLo > longitudeHi):
            return None

        return (
            longitudeLo,
            latitudeLo,
            longitudeHi,
            latitudeHi,
        )

    def jaccard(self, other: "CodeArea") -> float | None:
        bbox = self.intersection(other)

        if bbox is None:
            return None

        area = (bbox[3] - bbox[1]) * (bbox[2] - bbox[0])

        return area / (self.area + other.area - area)

    def resize(self) -> "CodeArea":
        halfHeight = float(self.centroid.latitudeHi - self.centroid.latitudeLo) / 2
        halfWidth = float(self.centroid.longitudeHi - self.centroid.longitudeLo) / 2

        return CodeArea(
            self.centroid,
            self.latitudeLo + halfHeight,
            self.longitudeLo + halfWidth,
            self.latitudeHi - halfHeight,
            self.longitudeHi - halfWidth,
            codeLength=self.codeLength,
        )


def decode(code: Code) -> CodeArea:
    match = isValid_(code)

    if match is None:
        raise ValueError("buildingid.code.decode - Invalid code")

    codeAreaCenter = openlocationcode.decode(match.group(RE_GROUP_OPENLOCATIONCODE_))
    assert isValidCodeArea(codeAreaCenter), "buildingid.code.decode - Invalid code area"

    codeAreaCenterHeight = codeAreaCenter.latitudeHi - codeAreaCenter.latitudeLo
    assert codeAreaCenterHeight >= 0, "buildingid.code.decode - Invalid code - Negative height"

    codeAreaCenterWidth = codeAreaCenter.longitudeHi - codeAreaCenter.longitudeLo
    assert codeAreaCenterWidth >= 0, "buildingid.code.decode - Invalid code - Negative width"

    latitudeLo = codeAreaCenter.latitudeLo - (int(match.group(RE_GROUP_SOUTH_)) * codeAreaCenterHeight)
    latitudeHi = codeAreaCenter.latitudeHi + (int(match.group(RE_GROUP_NORTH_)) * codeAreaCenterHeight)
    assert isValidLatitude(latitudeLo, latitudeHi), "buildingid.code.decode - Invalid code - Invalid latitude coordinates"

    longitudeLo = codeAreaCenter.longitudeLo - (int(match.group(RE_GROUP_WEST_)) * codeAreaCenterWidth)
    longitudeHi = codeAreaCenter.longitudeHi + (int(match.group(RE_GROUP_EAST_)) * codeAreaCenterWidth)
    assert isValidLongitude(longitudeLo, longitudeHi), "buildingid.code.decode - Invalid code - Invalid longitude coordinates"

    return CodeArea(codeAreaCenter, latitudeLo, longitudeLo, latitudeHi, longitudeHi, codeAreaCenter.codeLength)


def encode(
    latitudeLo: float,
    longitudeLo: float,
    latitudeHi: float,
    longitudeHi: float,
    latitudeCenter: float,
    longitudeCenter: float,
    codeLength: int = openlocationcode.PAIR_CODE_LENGTH_,
) -> Code:
    assert isValidCodeLength(codeLength), "buildingid.code.encode - Invalid code length"
    assert isValidLatitudeCenter(latitudeLo, latitudeHi, latitudeCenter), "buildingid.code.encode - Invalid latitude coordinates"
    assert isValidLongitudeCenter(longitudeLo, longitudeHi, longitudeCenter), "buildingid.code.encode - Invalid longitude coordinates"

    olcNortheast = openlocationcode.encode(latitudeHi, longitudeHi, codeLength=codeLength)
    codeAreaNortheast = openlocationcode.decode(olcNortheast)
    assert isValidCodeArea(codeAreaNortheast), "buildingid.code.encode - Invalid code area (northeast)"

    olcSouthwest = openlocationcode.encode(latitudeLo, longitudeLo, codeLength=codeLength)
    codeAreaSouthwest = openlocationcode.decode(olcSouthwest)
    assert isValidCodeArea(codeAreaSouthwest), "buildingid.code.encode - Invalid code area (southwest)"

    olcCenter = openlocationcode.encode(latitudeCenter, longitudeCenter, codeLength=codeLength)
    codeAreaCenter = openlocationcode.decode(olcCenter)
    assert isValidCodeArea(codeAreaCenter), "buildingid.code.encode - Invalid code area (center)"

    codeAreaCenterHeight = codeAreaCenter.latitudeHi - codeAreaCenter.latitudeLo
    assert codeAreaCenterHeight >= 0, "buildingid.code.encode - Invalid code - Negative height"

    codeAreaCenterWidth = codeAreaCenter.longitudeHi - codeAreaCenter.longitudeLo
    assert codeAreaCenterWidth >= 0, "buildingid.code.encode - Invalid code - Negative width"

    olcCountNorth = (codeAreaNortheast.latitudeHi - codeAreaCenter.latitudeHi) / codeAreaCenterHeight
    assert olcCountNorth >= 0, "buildingid.code.encode - Negative extent (north)"

    olcCountSouth = (codeAreaCenter.latitudeLo - codeAreaSouthwest.latitudeLo) / codeAreaCenterHeight
    assert olcCountSouth >= 0, "buildingid.code.encode - Negative extent (south)"

    olcCountEast = (codeAreaNortheast.longitudeHi - codeAreaCenter.longitudeHi) / codeAreaCenterWidth
    assert olcCountEast >= 0, "buildingid.code.encode - Negative extent (east)"

    olcCountWest = (codeAreaCenter.longitudeLo - codeAreaSouthwest.longitudeLo) / codeAreaCenterWidth
    assert olcCountWest >= 0, "buildingid.code.encode - Negative extent (west)"

    return Code(FORMAT_STRING_ % (olcCenter, olcCountNorth, olcCountEast, olcCountSouth, olcCountWest))


def isValid(code: Code) -> bool:
    return isValid_(code) is not None


def isValid_(code: Code) -> typing.Match[str] | None:
    if code is None:
        return None

    match = RE_PATTERN_.match(str(code))

    if (match is None) or not openlocationcode.isValid(match.group(RE_GROUP_OPENLOCATIONCODE_)):
        return None

    return match
