# -*- coding: utf-8 -*-
"""
***************************************************************************
    mgrs.py
    ---------------------
    Date                 : August 2016, October 2019
    Author               : Alex Bruy, Planet Federal
    Copyright            : (C) 2016 Boundless, http://boundlessgeo.com
                         : (C) 2019 Planet Inc, https://planet.com
***************************************************************************
*                                                                         *
*   This program 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 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""

from builtins import str
from builtins import range

__author__ = "Planet Federal"
__date__ = "October 2019"
__copyright__ = "(C) 2019 Planet Inc, https://planet.com"

# This will get replaced with a git SHA1 when you do a git archive
__revision__ = "$Format:%H$"

import os
import sys
import re
import math
import itertools
import logging

HAVE_OSR = False
# Force using proj for transformations by setting MGRSPY_USE_PROJ env var
if os.environ.get("MGRSPY_USE_PROJ", None) is None:
    try:
        from osgeo import osr

        HAVE_OSR = True
    except ImportError:
        pass

PYPROJ_VER = 0
if not HAVE_OSR:
    try:
        from pyproj import Transformer, CRS, __version__ as pyproj_ver

        PYPROJ_VER = 2
        if float(pyproj_ver[:3]) < 2.2:
            raise Exception("Unsupported pyproj version (need >= 2.2)")
    except ImportError:
        from pyproj import Proj, transform, __version__ as pyproj_ver

        if float(pyproj_ver[:3]) < 1.9 or int(pyproj_ver[4]) < 5:
            raise Exception("Unsupported pyproj version (need >= 1.9.5)")
        PYPROJ_VER = 1

LOG_LEVEL = os.environ.get("PYTHON_LOG_LEVEL", "WARNING").upper()
FORMAT = "%(levelname)s [%(name)s:%(lineno)s  %(funcName)s()] %(message)s"
logging.basicConfig(level=LOG_LEVEL, format=FORMAT)
log = logging.getLogger(__name__)

BADLY_FORMED = "An MGRS string error: string too long, too short, or badly formed"

# Whether to add the extra half-multiplier to UTM coords per precision,
# added in geotrans3.8
GEOTRANS_HALFMULTI = False

ALPHABET = {l: c for c, l in enumerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")}

ONEHT = 100000.0
TWOMIL = 2000000.0

MAX_PRECISION = 5  # Maximum precision of easting & northing
MIN_EAST_NORTH = 0
MAX_EAST_NORTH = 4000000

# letter,
# 2nd letter range - low,
# 2nd letter range - high,
# 3rd letter range - high (UPS),
# false easting based on 2nd letter,
# false northing based on 3rd letter
UPS_CONSTANTS = {
    0: (ALPHABET["A"], ALPHABET["J"], ALPHABET["Z"], ALPHABET["Z"], 800000.0, 800000.0),
    1: (
        ALPHABET["B"],
        ALPHABET["A"],
        ALPHABET["R"],
        ALPHABET["Z"],
        2000000.0,
        800000.0,
    ),
    2: (
        ALPHABET["Y"],
        ALPHABET["J"],
        ALPHABET["Z"],
        ALPHABET["P"],
        800000.0,
        1300000.0,
    ),
    3: (
        ALPHABET["Z"],
        ALPHABET["A"],
        ALPHABET["J"],
        ALPHABET["P"],
        2000000.0,
        1300000.0,
    ),
}

# letter, minimum northing, upper latitude, lower latitude, northing offset
LATITUDE_BANDS = [
    (ALPHABET["C"], 1100000.0, -72.0, -80.5, 0.0),
    (ALPHABET["D"], 2000000.0, -64.0, -72.0, 2000000.0),
    (ALPHABET["E"], 2800000.0, -56.0, -64.0, 2000000.0),
    (ALPHABET["F"], 3700000.0, -48.0, -56.0, 2000000.0),
    (ALPHABET["G"], 4600000.0, -40.0, -48.0, 4000000.0),
    (ALPHABET["H"], 5500000.0, -32.0, -40.0, 4000000.0),
    (ALPHABET["J"], 6400000.0, -24.0, -32.0, 6000000.0),
    (ALPHABET["K"], 7300000.0, -16.0, -24.0, 6000000.0),
    (ALPHABET["L"], 8200000.0, -8.0, -16.0, 8000000.0),
    (ALPHABET["M"], 9100000.0, 0.0, -8.0, 8000000.0),
    (ALPHABET["N"], 0.0, 8.0, 0.0, 0.0),
    (ALPHABET["P"], 800000.0, 16.0, 8.0, 0.0),
    (ALPHABET["Q"], 1700000.0, 24.0, 16.0, 0.0),
    (ALPHABET["R"], 2600000.0, 32.0, 24.0, 2000000.0),
    (ALPHABET["S"], 3500000.0, 40.0, 32.0, 2000000.0),
    (ALPHABET["T"], 4400000.0, 48.0, 40.0, 4000000.0),
    (ALPHABET["U"], 5300000.0, 56.0, 48.0, 4000000.0),
    (ALPHABET["V"], 6200000.0, 64.0, 56.0, 6000000.0),
    (ALPHABET["W"], 7000000.0, 72.0, 64.0, 6000000.0),
    (ALPHABET["X"], 7900000.0, 84.5, 72.0, 6000000.0),
]


class MgrsException(Exception):
    pass


def _log_proj_crs(proj_crs, proj_desc="", espg=""):
    if proj_desc:
        proj_desc = "{0} ".format(str(proj_desc))
    if espg:
        espg = "espg:{0} ".format(str(espg))
    definition = ""
    if HAVE_OSR:
        definition = proj_crs.ExportToPrettyWkt()
    if PYPROJ_VER == 1:
        definition = proj_crs.definition_string()
    elif PYPROJ_VER == 2:
        definition = proj_crs.to_wkt(pretty=True)
    log.debug("{0}proj: {1}{2}{3}".format(proj_desc, espg, os.linesep, definition))


def _transform_proj(x1, y1, epsg_src, epsg_dst, polar=False):
    if PYPROJ_VER == 1:
        proj_src = Proj(init="epsg:{0}".format(epsg_src))
        _log_proj_crs(proj_src, proj_desc="src", espg=epsg_src)
        proj_dst = Proj(init="epsg:{0}".format(epsg_dst))
        _log_proj_crs(proj_dst, proj_desc="dst", espg=epsg_dst)
        x2, y2 = transform(proj_src, proj_dst, x1, y1)
    elif PYPROJ_VER == 2:
        # With PROJ 6+ input axis ordering needs honored per projection, even
        #   though always_xy should fix it (doesn't seem to work for UPS)
        crs_src = CRS.from_epsg(epsg_src)
        _log_proj_crs(crs_src, proj_desc="src", espg=epsg_src)
        crs_dst = CRS.from_epsg(epsg_dst)
        _log_proj_crs(crs_dst, proj_desc="dst", espg=epsg_dst)
        ct = Transformer.from_crs(crs_src, crs_dst, always_xy=(not polar))
        if polar:
            y2, x2 = ct.transform(y1, x1)
        else:
            x2, y2 = ct.transform(x1, y1)
    else:
        raise MgrsException("pyproj version unsupported")

    return x2, y2


def _transform_osr(x1, y1, epsg_src, epsg_dst, polar=False):
    src = osr.SpatialReference()
    # Check if we are using osgeo.osr linked against PROJ 6+
    # If so, input axis ordering needs honored per projection, even though
    #   OAMS_TRADITIONAL_GIS_ORDER should fix it (doesn't seem to work for UPS)
    # See GDAL/OGR migration guide for 2.4 to 3.0
    # https://github.com/OSGeo/gdal/blob/master/gdal/MIGRATION_GUIDE.TXT and
    # https://trac.osgeo.org/gdal/wiki/rfc73_proj6_wkt2_srsbarn#Axisorderissues
    osr_proj6 = hasattr(src, "SetAxisMappingStrategy")
    if not polar and osr_proj6:
        src.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
    src.ImportFromEPSG(epsg_src)
    _log_proj_crs(src, proj_desc="src", espg=epsg_src)
    dst = osr.SpatialReference()
    if not polar and osr_proj6:
        dst.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
    dst.ImportFromEPSG(epsg_dst)
    _log_proj_crs(dst, proj_desc="dst", espg=epsg_dst)
    ct = osr.CoordinateTransformation(src, dst)
    if polar and osr_proj6:
        # only supported with osgeo.osr v3.0.0+
        y2, x2, _ = ct.TransformPoint(y1, x1)
    else:
        x2, y2, _ = ct.TransformPoint(x1, y1)

    return x2, y2


def _transform(x1, y1, epsg_src, epsg_dst, polar=False):
    if HAVE_OSR:
        return _transform_osr(x1, y1, epsg_src, epsg_dst, polar=polar)
    else:
        return _transform_proj(x1, y1, epsg_src, epsg_dst, polar=polar)


def toMgrs(latitude, longitude, precision=5):
    """Converts geodetic (latitude and longitude) coordinates to an MGRS
    coordinate string, according to the current ellipsoid parameters.

    @param latitude - latitude value
    @param longitude - longitude value
    @param precision - precision level of MGRS string
    @returns - MGRS coordinate string
    """

    # To avoid precision issues, which appear when using more than
    # 9 decimal places
    # This may no longer be an issue for Python >= 3
    if sys.version_info.major < 3:
        latitude = round(latitude, 9)
        longitude = round(longitude, 9)

    if math.fabs(latitude) > 90:
        raise MgrsException("Latitude outside of valid range (-90 to 90 degrees).")

    if (longitude < -180) or (longitude > 360):
        raise MgrsException("Longitude outside of valid range (-180 to 360 degrees).")

    if (precision < 0) or (precision > MAX_PRECISION):
        raise MgrsException("The precision must be between 0 and 5 inclusive.")

    hemisphere, zone, epsg = _epsgForWgs(latitude, longitude)

    x, y = _transform(longitude, latitude, 4326, epsg, polar=(zone == 61))

    if (latitude < -80) or (latitude > 84):
        # Convert to UPS
        mgrs = _upsToMgrs(hemisphere, x, y, precision)
    else:
        # Convert to UTM
        mgrs = _utmToMgrs(zone, hemisphere, latitude, longitude, x, y, precision)

    return mgrs


def toWgs(mgrs):
    """Converts an MGRS coordinate string to geodetic (latitude and longitude)
    coordinates

    @param mgrs - MGRS coordinate string
    @returns - tuple containning latitude and longitude values
    """
    mgrs = _clean_mgrs_str(mgrs)
    log.debug("in: {0}".format(mgrs))

    utm = _checkZone(mgrs)
    if utm:
        zone, hemisphere, easting, northing = _mgrsToUtm(mgrs)
    else:
        zone, hemisphere, easting, northing = _mgrsToUps(mgrs)

    log.debug("e: {0}, n: {1}".format(easting, northing))

    epsg = _epsgForUtm(zone, hemisphere)

    longitude, latitude = _transform(easting, northing, epsg, 4326, polar=(not utm))

    # Note y, x axis order for output
    log.debug("lat: {0}, lon: {1}".format(latitude, longitude))

    return latitude, longitude


def _upsToMgrs(hemisphere, easting, northing, precision):
    """Converts UPS (hemisphere, easting, and northing) coordinates
    to an MGRS coordinate string.

    @param hemisphere - hemisphere either 'N' or 'S'
    @param easting - easting/X in meters
    @param northing - northing/Y in meters
    @param precision - precision level of MGRS string
    @returns - MGRS coordinate string
    """
    if hemisphere not in ["N", "S"]:
        raise MgrsException('Invalid hemisphere ("N" or "S").')

    if (easting < MIN_EAST_NORTH) or (easting > MAX_EAST_NORTH):
        raise MgrsException(
            "Easting outside of valid range (100,000 to 900,000 meters "
            "for UTM, 0 to 4,000,000 meters for UPS)."
        )

    if (northing < MIN_EAST_NORTH) or (northing > MAX_EAST_NORTH):
        raise MgrsException(
            "Northing outside of valid range (0 to 10,000,000 meters for UTM, "
            "0 to 4,000,000 meters for UPS)."
        )

    if (precision < 0) or (precision > MAX_PRECISION):
        raise MgrsException("The precision must be between 0 and 5 inclusive.")

    letters = [None, None, None]
    if hemisphere == "N":
        if easting >= TWOMIL:
            letters[0] = ALPHABET["Z"]
        else:
            letters[0] = ALPHABET["Y"]

        idx = letters[0] - 22
        ltr2LowValue = UPS_CONSTANTS[idx][1]
        falseEasting = UPS_CONSTANTS[idx][4]
        falseNorthing = UPS_CONSTANTS[idx][5]
    else:
        if easting >= TWOMIL:
            letters[0] = ALPHABET["B"]
        else:
            letters[0] = ALPHABET["A"]

        ltr2LowValue = UPS_CONSTANTS[letters[0]][1]
        falseEasting = UPS_CONSTANTS[letters[0]][4]
        falseNorthing = UPS_CONSTANTS[letters[0]][5]

    gridNorthing = northing
    gridNorthing = gridNorthing - falseNorthing

    letters[2] = int(gridNorthing / ONEHT)

    if letters[2] > ALPHABET["H"]:
        letters[2] = letters[2] + 1

    if letters[2] > ALPHABET["N"]:
        letters[2] = letters[2] + 1

    gridEasting = easting
    gridEasting = gridEasting - falseEasting
    letters[1] = ltr2LowValue + int(gridEasting / ONEHT)

    if easting < TWOMIL:
        if letters[1] > ALPHABET["L"]:
            letters[1] = letters[1] + 3

        if letters[1] > ALPHABET["U"]:
            letters[1] = letters[1] + 2
    else:
        if letters[1] > ALPHABET["C"]:
            letters[1] = letters[1] + 2

        if letters[1] > ALPHABET["H"]:
            letters[1] = letters[1] + 1

        if letters[1] > ALPHABET["L"]:
            letters[1] = letters[1] + 3

    return _mgrsString(0, letters, easting, northing, precision)


def _mgrsToUps(mgrs):
    """Converts an MGRS coordinate string to UTM projection (zone, hemisphere,
    easting and northing) coordinates

    @param mgrs - MGRS coordinate string
    @returns - tuple containing UTM zone, hemisphere, easting and northing
    """
    zone, letters, easting, northing, precision = _breakMgrsString(mgrs)

    if zone != 0:
        raise MgrsException(BADLY_FORMED)

    if letters[0] >= ALPHABET["Y"]:
        hemisphere = "N"

        idx = letters[0] - 22
        ltr2LowValue = UPS_CONSTANTS[idx][1]
        ltr2HighValue = UPS_CONSTANTS[idx][2]
        ltr3HighValue = UPS_CONSTANTS[idx][3]
        falseEasting = UPS_CONSTANTS[idx][4]
        falseNorthing = UPS_CONSTANTS[idx][5]
    else:
        hemisphere = "S"

        ltr2LowValue = UPS_CONSTANTS[letters[0]][1]
        ltr2HighValue = UPS_CONSTANTS[letters[0]][2]
        ltr3HighValue = UPS_CONSTANTS[letters[0]][3]
        falseEasting = UPS_CONSTANTS[letters[0]][4]
        falseNorthing = UPS_CONSTANTS[letters[0]][5]

    # Check that the second letter of the MGRS string is within the range
    # of valid second letter values. Also check that the third letter is valid
    invalid = [
        ALPHABET["D"],
        ALPHABET["E"],
        ALPHABET["M"],
        ALPHABET["N"],
        ALPHABET["V"],
        ALPHABET["W"],
    ]
    if (
        (letters[1] < ltr2LowValue)
        or (letters[1] > ltr2HighValue)
        or (letters[1] in [invalid])
        or (letters[2] > ltr3HighValue)
    ):
        raise MgrsException(BADLY_FORMED)

    gridNorthing = float(letters[2] * ONEHT + falseNorthing)
    if letters[2] > ALPHABET["I"]:
        gridNorthing = gridNorthing - ONEHT

    if letters[2] > ALPHABET["O"]:
        gridNorthing = gridNorthing - ONEHT

    gridEasting = float((letters[1] - ltr2LowValue) * ONEHT + falseEasting)
    if ltr2LowValue != ALPHABET["A"]:
        if letters[1] > ALPHABET["L"]:
            gridEasting = gridEasting - 300000.0

        if letters[1] > ALPHABET["U"]:
            gridEasting = gridEasting - 200000.0
    else:
        if letters[1] > ALPHABET["C"]:
            gridEasting = gridEasting - 200000.0

        if letters[1] > ALPHABET["I"]:
            gridEasting = gridEasting - ONEHT

        if letters[1] > ALPHABET["L"]:
            gridEasting = gridEasting - 300000.0

    easting += gridEasting
    northing += gridNorthing

    return zone, hemisphere, easting, northing


def _utmToMgrs(zone, hemisphere, latitude, longitude, easting, northing, precision):
    """Calculates an MGRS coordinate string based on the UTM zone, latitude,
    easting and northing values.

    @param zone - UTM zone number
    @param hemisphere - hemisphere either 'N' or 'S'
    @param latitude - latitude value
    @param longitude - longitude value
    @param easting - easting/X in meters
    @param northing - northing/Y in meters
    @param precision - precision level of MGRS string
    @returns - MGRS coordinate string
    """
    # FIXME: do we really need this?
    # Special check for rounding to (truncated) eastern edge of zone 31V
    # if (zone == 31) \
    #         and (((latitude >= 56.0) and (latitude < 64.0))
    #              and ((longitude >= 3.0) or (easting >= 500000.0))):
    #    # Reconvert to UTM zone 32
    #    override = 32
    #    lat = int(latitude)
    #    lon = int(longitude)
    #    if zone == 1 and override == 60:
    #        zone = override
    #    elif zone == 60 and override == 1:
    #        zone = override
    #    elif (lat > 71) and (lon > -1) and (lon < 42):
    #        if (zone - 2 <= override) and (override <= zone + 2):
    #            zone = override
    #        else:
    #            raise MgrsException('Zone outside of valid range (1 to 60) '
    #                                'and within 1 of "natural" zone')
    #    elif (zone - 1 <= override) and (override <= zone + 1):
    #        zone = override
    #    else:
    #        raise MgrsException('Zone outside of valid range (1 to 60) and '
    #                            'within 1 of "natural" zone')
    #
    #    epsg = _epsgForUtm(zone, hemisphere)
    #
    #    src = osr.SpatialReference()
    #    src.ImportFromEPSG(4326)
    #    dst = osr.SpatialReference()
    #    dst.ImportFromEPSG(epsg)
    #    ct = osr.CoordinateTransformation(src, dst)
    #    x, y, z = ct.TransformPoint(longitude, latitude)

    if latitude <= 0.0 and northing == 1.0e7:
        latitude = 0
        northing = 0

    ltr2LowValue, ltr2HighValue, patternOffset = _gridValues(zone)

    letters = [_latitudeLetter(latitude), None, None]

    while northing >= TWOMIL:
        northing = northing - TWOMIL

    northing += patternOffset
    if northing >= TWOMIL:
        northing = northing - TWOMIL

    letters[2] = int(northing / ONEHT)
    if letters[2] > ALPHABET["H"]:
        letters[2] += 1

    if letters[2] > ALPHABET["N"]:
        letters[2] += 1

    if ((letters[0] == ALPHABET["V"]) and (zone == 31)) and (easting == 500000.0):
        easting = easting - 1.0  # Substract 1 meter

    letters[1] = ltr2LowValue + int((easting / ONEHT) - 1)
    if ltr2LowValue == ALPHABET["J"] and letters[1] > ALPHABET["N"]:
        letters[1] += 1

    return _mgrsString(zone, letters, easting, northing, precision)


def _mgrsToUtm(mgrs):
    """Converts an MGRS coordinate string to UTM projection (zone, hemisphere,
    easting and northing) coordinates.

    @param mgrs - MGRS coordinate string
    @returns - tuple containing UTM zone, hemisphere, easting, northing
    """
    zone, letters, easting, northing, precision = _breakMgrsString(mgrs)
    if zone == 0:
        raise MgrsException(BADLY_FORMED)

    if letters == ALPHABET["X"] and zone in [32, 34, 36]:
        raise MgrsException(BADLY_FORMED)

    if letters[0] < ALPHABET["N"]:
        hemisphere = "S"
    else:
        hemisphere = "N"

    ltr2LowValue, ltr2HighValue, patternOffset = _gridValues(zone)

    # Check that the second letter of the MGRS string is within the range
    # of valid second letter values. Also check that the third letter is valid
    if (
        (letters[1] < ltr2LowValue)
        or (letters[1] > ltr2HighValue)
        or (letters[2] > ALPHABET["V"])
    ):
        raise MgrsException(BADLY_FORMED)

    rowLetterNorthing = float(letters[2] * ONEHT)
    gridEasting = float((letters[1] - ltr2LowValue + 1) * ONEHT)
    if ltr2LowValue == ALPHABET["J"] and letters[1] > ALPHABET["O"]:
        gridEasting = gridEasting - ONEHT

    if letters[2] > ALPHABET["O"]:
        rowLetterNorthing = rowLetterNorthing - ONEHT

    if letters[2] > ALPHABET["I"]:
        rowLetterNorthing = rowLetterNorthing - ONEHT

    if rowLetterNorthing >= TWOMIL:
        rowLetterNorthing = rowLetterNorthing - TWOMIL

    minNorthing, northingOffset = _latitudeBandMinNorthing(letters[0])

    gridNorthing = rowLetterNorthing - patternOffset
    if gridNorthing < 0:
        gridNorthing += TWOMIL

    gridNorthing += northingOffset

    if gridNorthing < minNorthing:
        gridNorthing += TWOMIL

    easting += gridEasting
    northing += gridNorthing

    return zone, hemisphere, easting, northing


def _mgrsString(zone, letters, easting, northing, precision):
    """Constructs an MGRS string from its component parts
    @param zone - UTM zone
    @param letters - MGRS coordinate string letters
    @param easting - easting value
    @param northing - northing value
    @param precision - precision level of MGRS string
    @returns - MGRS coordinate string
    """
    if zone:
        tmp = str(zone)
        mgrs = tmp.zfill(3 - len(tmp))
    else:
        mgrs = "  "

    for i in range(3):
        mgrs += list(ALPHABET.keys())[list(ALPHABET.values()).index(letters[i])]

    easting = math.fmod(easting + 1e-8, 100000.0)
    if easting >= 99999.5:
        easting = 99999.0
    mgrs += str(int(easting)).rjust(5, "0")[:precision]

    northing = math.fmod(northing + 1e-8, 100000.0)
    if northing >= 99999.5:
        northing = 99999.0
    mgrs += str(int(northing)).rjust(5, "0")[:precision]

    log.debug("mgrs: {0}".format(mgrs))

    return mgrs


def _epsgForWgs(latitude, longitude):
    """Returns corresponding UTM or UPS EPSG code from WGS84 coordinates
    @param latitude - latitude value
    @param longitude - longitude value
    @returns - tuple containing hemisphere, UTM zone and EPSG code
    """

    if math.fabs(latitude) > 90:
        raise MgrsException("Latitude outside of valid range (-90 to 90 degrees).")

    if longitude < -180 or longitude > 360:
        return MgrsException("Longitude outside of valid range (-180 to 360 degrees).")

    # hemisphere
    if latitude < 0:
        hemisphere = "S"
    else:
        hemisphere = "N"

    # UTM zone
    if latitude <= -80 or latitude >= 84:
        # Coordinates falls under UPS system
        zone = 61
    else:
        # Coordinates falls under UTM system
        if longitude < 180:
            zone = int(31 + (longitude / 6.0))
        else:
            zone = int((longitude / 6) - 29)

        if zone > 60:
            zone = 1

        # Handle UTM special cases
        if 56.0 <= latitude < 64.0 and 3.0 <= longitude < 12.0:
            zone = 32

        if 72.0 <= latitude < 84.0:
            if 0.0 <= longitude < 9.0:
                zone = 31
            elif 9.0 <= longitude < 21.0:
                zone = 33
            elif 21.0 <= longitude < 33.0:
                zone = 35
            elif 33.0 <= longitude < 42.0:
                zone = 37

    # North or South hemisphere
    if latitude >= 0:
        ns = 600
    else:
        ns = 700

    return hemisphere, zone, 32000 + ns + zone


def _epsgForUtm(zone, hemisphere):
    """Returen EPSG code for given UTM zone and hemisphere

    @param zone - UTM zone
    @param hemisphere - hemisphere either 'N' or 'S'
    @returns - corresponding EPSG code
    """
    if hemisphere not in ["N", "S"]:
        raise MgrsException('Invalid hemisphere ("N" or "S").')

    if zone < 0 or zone > 60:
        raise MgrsException("UTM zone ouside valid range.")

    if hemisphere == "N":
        ns = 600
    else:
        ns = 700

    if zone == 0:
        zone = 61

    return 32000 + ns + zone


def _gridValues(zone):
    """Sets the letter range used for the 2nd letter in the MGRS coordinate
    string, based on the set number of the UTM zone. It also sets the pattern
    offset using a value of A for the second letter of the grid square, based
    on the grid pattern and set number of the UTM zone.

    @param zone - UTM zone number
    @returns - tuple containing 2nd letter low number, 2nd letter high number
    and pattern offset
    """
    setNumber = zone % 6

    if not setNumber:
        setNumber = 6

    ltr2LowValue = None
    ltr2HighValue = None

    if setNumber in [1, 4]:
        ltr2LowValue = ALPHABET["A"]
        ltr2HighValue = ALPHABET["H"]
    elif setNumber in [2, 5]:
        ltr2LowValue = ALPHABET["J"]
        ltr2HighValue = ALPHABET["R"]
    elif setNumber in [3, 6]:
        ltr2LowValue = ALPHABET["S"]
        ltr2HighValue = ALPHABET["Z"]

    if ltr2LowValue is None or ltr2HighValue is None:
        raise MgrsException(BADLY_FORMED)

    if setNumber % 2:
        patternOffset = 0.0
    else:
        patternOffset = 500000.0

    return ltr2LowValue, ltr2HighValue, patternOffset


def _latitudeLetter(latitude):
    """Returns the latitude band letter for given latitude

    @param latitude - latitude value
    @returns - latitude band letter
    """
    if 72 <= latitude < 84.5:
        return ALPHABET["X"]
    elif -80.5 < latitude < 72:
        idx = int(((latitude + 80.0) / 8.0) + 1.0e-12)
        return LATITUDE_BANDS[idx][0]


def _checkZone(mgrs):
    """Checks if MGRS coordinate string contains UTM zone definition

    @param mgrs - MGRS coordinate string
    @returns - True if zone is given, False otherwise (or for UPS)
    """
    mgrs = _clean_mgrs_str(mgrs)  # should always set two zone digits, even UPS
    count = sum(1 for _ in itertools.takewhile(str.isdigit, mgrs))
    zone = int(mgrs[:count])
    if zone == 0:
        return False
    if count <= 2:
        return count > 0
    else:
        raise MgrsException(BADLY_FORMED)


def _breakMgrsString(mgrs):
    """Breaks down an MGRS coordinate string into its component parts.

    @param mgrs - MGRS coordinate string
    @returns - tuple containing MGRS string componets: UTM zone,
    MGRS coordinate string letters, easting, northing and precision
    """
    mgrs = _clean_mgrs_str(mgrs)  # should always set two zone digits, even UPS
    # Number of zone digits
    count = sum(1 for _ in itertools.takewhile(str.isdigit, mgrs))
    if count == 2:
        zone = int(mgrs[:2])
        if zone < 0 or zone > 60:
            raise MgrsException(BADLY_FORMED)
    else:
        raise MgrsException(BADLY_FORMED)

    idx = count
    # MGRS letters
    count = sum(
        1 for _ in itertools.takewhile(str.isalpha, itertools.islice(mgrs, idx, None))
    )
    if count == 3:
        a = ord("A")
        invalid = [ALPHABET["I"], ALPHABET["O"]]

        letters = []
        ch = ord(mgrs[idx : idx + 1].upper()) - a
        if ch in invalid:
            raise MgrsException(BADLY_FORMED)
        idx += 1
        letters.append(ch)

        ch = ord(mgrs[idx : idx + 1].upper()) - a
        if ch in invalid:
            raise MgrsException(BADLY_FORMED)
        idx += 1
        letters.append(ch)

        ch = ord(mgrs[idx : idx + 1].upper()) - a
        if ch in invalid:
            raise MgrsException(BADLY_FORMED)
        idx += 1
        letters.append(ch)
    else:
        raise MgrsException(BADLY_FORMED)

    # Easting and Northing
    count = sum(
        1 for _ in itertools.takewhile(str.isdigit, itertools.islice(mgrs, idx, None))
    )
    if count <= 10 and count % 2 == 0:
        precision = int(count / 2)
        if precision > 0:
            easting = float(mgrs[idx : idx + precision])
            northing = float(mgrs[idx + precision :])

            multiplier = _computeScale(precision)
            easting = easting * multiplier
            northing = northing * multiplier

            if GEOTRANS_HALFMULTI:
                half_multi = multiplier * 0.5  # added in geotrans3.8
                easting += half_multi
                northing += half_multi
        else:
            easting = 0.0
            northing = 0.0
    else:
        raise MgrsException(BADLY_FORMED)

    log.debug(
        'zone: "{0}", letters: "{1}", '
        'easting: "{2}": northing "{3}", precision: "{4}" '.format(
            zone, letters, easting, northing, precision
        )
    )

    return zone, letters, easting, northing, precision


def _latitudeBandMinNorthing(letter):
    """Determines the minimum northing and northing offset
    for given latitude band letter.

    @param letter - latitude band letter
    @returns - tuple containing minimum northing and northing offset
    for that letter
    """
    if ALPHABET["C"] <= letter <= ALPHABET["H"]:
        minNorthing = LATITUDE_BANDS[letter - 2][1]
        northingOffset = LATITUDE_BANDS[letter - 2][4]
    elif ALPHABET["J"] <= letter <= ALPHABET["N"]:
        minNorthing = LATITUDE_BANDS[letter - 3][1]
        northingOffset = LATITUDE_BANDS[letter - 3][4]
    elif ALPHABET["P"] <= letter <= ALPHABET["X"]:
        minNorthing = LATITUDE_BANDS[letter - 4][1]
        northingOffset = LATITUDE_BANDS[letter - 4][4]
    else:
        raise MgrsException(BADLY_FORMED)

    return minNorthing, northingOffset


def _computeScale(precision):
    if precision == 0:
        return 1.0e5
    elif precision == 1:
        return 1.0e4
    elif precision == 2:
        return 1.0e3
    elif precision == 3:
        return 1.0e2
    elif precision == 4:
        return 1.0e1
    elif precision == 5:
        return 1.0e0
    return 1.0e5


def _clean_mgrs_str(s):
    """
    Clean up MGRS user-input string.
    :param s: MGRS input string
    :return: Cleaned and stripped string as Unicode
    """
    log.debug("in: {0}".format(s))
    if str(type(s)) not in [
        "<class 'str'>",
        "<class 'bytes'>",
        "<type 'str'>",
        "<type 'unicode'>",
    ]:
        raise MgrsException(BADLY_FORMED)

    # convert to unicode, so str.isdigit, etc work in Py2
    if str(type(s)) == "<class 'bytes'>":  # Py 3
        s = s.decode()  # <class 'str'> as UTF-8
    elif str(type(s)) == "<type 'str'>":  # Py 2
        s = str(s, encoding="UTF-8")  # <type 'unicode'>

    # strip whitespace
    s = re.sub(r"\s+", "", s)

    # prepend 0 to input of single-digit zone
    count = sum(1 for _ in itertools.takewhile(str.isdigit, s))
    if count == 0:
        s = "00" + s
    elif count == 1:
        s = "0" + s
    elif count > 2:
        raise MgrsException(BADLY_FORMED)
    log.debug("out: {0}".format(s))
    return s


################
# Added by Vgrid
################
def get_mgrs_resolution_and_cell_size(mgrs_id):
    """Get the resolution and cell size of the MGRS code based on its length.

    @param mgrs: MGRS coordinate string (no spaces)
    @returns: tuple containing (resolution in meters, grid size in meters)
    """
    # Clean the MGRS string (if necessary)
    mgrs_id = _clean_mgrs_str(mgrs_id)

    # Validate the MGRS code length
    length = len(mgrs_id)

    # Initialize precision and grid size variables
    resolution = 0
    grid_size = 100000

    # Determine precision and grid size based on MGRS length
    if length == 5:
        resolution = 0
        grid_size = 100000  # 100 km
    elif length == 7:
        resolution = 1
        grid_size = 10000  # 10 km
    elif length == 9:
        resolution = 2
        grid_size = 1000  # 1 km
    elif length == 11:
        resolution = 3
        grid_size = 100  # 100 m
    elif length == 13:
        resolution = 4
        grid_size = 10  # 10 m
    elif length == 15:
        resolution = 5
        grid_size = 1  # 1 m
    else:
        raise ValueError("MGRS code does not match expected lengths for precision.")

    return resolution, grid_size


def mgrscell(mgrs_id):
    resolution, grid_size = get_mgrs_resolution_and_cell_size(mgrs_id)
    zone, hemisphere, easting, northing = _mgrsToUtm(mgrs_id)

    min_x, min_y = easting, northing
    max_x, max_y = (
        min_x + grid_size,
        min_y + grid_size,
    )  # Correct max_x, max_y calculation

    # Determine UTM EPSG code
    if hemisphere == "N":
        epsg_code = 32600 + int(zone)
    else:
        epsg_code = 32700 + int(zone)

    utm_crs = CRS.from_epsg(epsg_code)
    wgs84_crs = CRS.from_epsg(4326)

    # Create transformer from UTM to WGS84
    transformer = Transformer.from_crs(utm_crs, wgs84_crs, always_xy=True)

    # Convert all four corners
    min_lon, min_lat = transformer.transform(min_x, min_y)
    max_lon, max_lat = transformer.transform(max_x, max_y)

    return min_lat, min_lon, max_lat, max_lon, resolution
