#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ******************************************************************************
#
#  Project:  GDAL
#  Purpose:  Validate JPEG2000 file structure
#  Author:   Even Rouault, <even dot rouault at spatialys dot com>
#
# ******************************************************************************
#  Copyright (c) 2015, European Union (European Environment Agency)
#
# SPDX-License-Identifier: MIT
# ******************************************************************************

import os
import sys

from osgeo import gdal, osr


def Usage():
    print(
        "Usage: validate_jp2 [-expected_gmljp2] [-inspire_tg] [-datatype imagery|non_imagery]"
    )
    print(
        "                    [-oidoc in.xml] [-ogc_schemas_location path|disabled] test.jp2"
    )
    print("")
    print("Options:")
    print("-expected_gmljp2: hint to indicate that a GMLJP2 box should be present.")
    print("-inspire_tg: Validate using Inspire Orthoimagery technical guidelines.")
    print(
        "-datatype imagery|non_imagery: To specify the nature of the data. Defaults is imagery."
    )
    print("                               Only used by -inspire_tg")
    print(
        "-oidoc: XML document conforming with Inspire Orthoimagery GML application schema."
    )
    print(
        "-ogc_schemas_location: Path to directory with OGC schemas. Needed for GMLJP2 validation."
    )
    return 2


XML_TYPE_IDX = 0
XML_VALUE_IDX = 1
XML_FIRST_CHILD_IDX = 2


def find_xml_node(ar, element_name, only_attributes=False):
    # type = ar[XML_TYPE_IDX]
    value = ar[XML_VALUE_IDX]
    if value == element_name:
        return ar
    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        if only_attributes and child[XML_TYPE_IDX] != gdal.CXT_Attribute:
            continue
        found = find_xml_node(child, element_name)
        if found is not None:
            return found
    return None


def get_attribute_val(ar, attr_name):
    node = find_xml_node(ar, attr_name, True)
    if node is None or node[XML_TYPE_IDX] != gdal.CXT_Attribute:
        return None
    if (
        len(ar) > XML_FIRST_CHILD_IDX
        and node[XML_FIRST_CHILD_IDX][XML_TYPE_IDX] == gdal.CXT_Text
    ):
        return node[XML_FIRST_CHILD_IDX][XML_VALUE_IDX]
    return None


def find_message(ar):
    msg = get_attribute_val(ar, "message")
    if msg is None:
        return "unknown"
    return msg


def find_element_with_name(ar, element_name, name, attribute_name="name"):
    typ = ar[XML_TYPE_IDX]
    value = ar[XML_VALUE_IDX]
    if (
        typ == gdal.CXT_Element
        and value == element_name
        and get_attribute_val(ar, attribute_name) == name
    ):
        return ar
    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        found = find_element_with_name(child, element_name, name, attribute_name)
        if found:
            return found
    return None


def find_jp2box(ar, jp2box_name):
    return find_element_with_name(ar, "JP2Box", jp2box_name)


def find_marker(ar, marker_name):
    return find_element_with_name(ar, "Marker", marker_name)


def get_count_and_indices_of_jp2boxes(ar):
    the_dic = {}
    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        if child[XML_TYPE_IDX] == gdal.CXT_Element and child[XML_VALUE_IDX] == "JP2Box":
            jp2box_name = get_attribute_val(child, "name")
            if jp2box_name in the_dic:
                the_dic[jp2box_name] = (
                    the_dic[jp2box_name][0] + 1,
                    the_dic[jp2box_name][1],
                )
            else:
                the_dic[jp2box_name] = (1, child_idx)

    return the_dic


def get_count_of_uuidboxes(ar):
    the_dic = {}
    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        if child[XML_TYPE_IDX] == gdal.CXT_Element and child[XML_VALUE_IDX] == "JP2Box":
            jp2box_name = get_attribute_val(child, "name")
            if jp2box_name == "uuid":
                uuid = get_element_val(find_xml_node(child, "UUID"))
                if uuid in the_dic:
                    the_dic[uuid] += 1
                else:
                    the_dic[uuid] = 1

    return the_dic


def find_field(ar, field_name):
    return find_element_with_name(ar, "Field", field_name)


def get_element_val(node):
    if node is None:
        return None
    for child_idx in range(XML_FIRST_CHILD_IDX, len(node)):
        child = node[child_idx]
        if child[XML_TYPE_IDX] == gdal.CXT_Text:
            return child[XML_VALUE_IDX]
    return None


def get_field_val(ar, field_name):
    return get_element_val(find_field(ar, field_name))


def gdalOpenWithOpenJPEGDriverPreferably(filename):
    drivers = []
    jp2openjpeg_drv = gdal.GetDriverByName("JP2OpenJPEG")
    if jp2openjpeg_drv:
        # Deregister all drivers except JP2OpenJPEG if it exists
        for drvname in ["JP2KAK", "JP2ECW", "JP2OpenJPEG", "JP2MrSID", "JPEG2000"]:
            drv = gdal.GetDriverByName(drvname)
            if drvname != "JP2OpenJPEG" and drv is not None:
                drv.Deregister()
            drivers.append(drv)

    ds = gdal.Open(filename)

    # Re-register drivers
    if jp2openjpeg_drv:
        jp2openjpeg_drv.Deregister()
        for drv in drivers:
            if drv is not None:
                drv.Register()

    return ds


def get_gmljp2(filename):
    ds = gdalOpenWithOpenJPEGDriverPreferably(filename)
    if ds is None:
        return None
    mdd = ds.GetMetadata("xml:gml.root-instance")
    if mdd is None:
        return None
    return mdd[0]


class ErrorReport:
    def __init__(self, collect_internally=False):
        self.error_count = 0
        self.warning_count = 0
        self.collect_internally = collect_internally
        self.error_array = []
        self.warning_array = []

    def EmitError(self, category, msg, requirement=None, conformance_class=None):
        self.error_count += 1

        if category == "PROFILE_1" and conformance_class is None:
            conformance_class = "A.8.14"

        if requirement is not None and conformance_class is not None:
            full_msg = "ERROR[%s, Requirement %d, Conformance class %s]: %s" % (
                category,
                requirement,
                conformance_class,
                msg,
            )
        elif requirement is not None:
            full_msg = "ERROR[%s, Requirement %d]: %s" % (category, requirement, msg)
        elif conformance_class is not None:
            full_msg = "ERROR[%s, Conformance class %s]: %s" % (
                category,
                conformance_class,
                msg,
            )
        else:
            full_msg = "ERROR[%s]: %s" % (category, msg)
        if self.collect_internally:
            self.error_array.append(full_msg)
        else:
            print(full_msg)

    def EmitWarning(self, category, msg, recommendation=None):
        self.warning_count += 1
        if recommendation is not None:
            full_msg = "WARNING[%s, Recommendation %d]: %s" % (
                category,
                recommendation,
                msg,
            )
        else:
            full_msg = "WARNING[%s]: %s" % (category, msg)
        if self.collect_internally:
            self.warning_array.append(full_msg)
        else:
            print(full_msg)


# Report JP2 boxes errors


def find_remaining_bytes(error_report, ar, parent_node_name=None):
    typ = ar[XML_TYPE_IDX]
    value = ar[XML_VALUE_IDX]
    if typ == gdal.CXT_Element and value == "JP2Box":
        parent_node_name = get_attribute_val(ar, "name")
    if typ == gdal.CXT_Element and value == "RemainingBytes":
        error_report.EmitError(
            "GENERAL",
            "Remaining bytes in JP2 box %s: %s"
            % (parent_node_name, get_element_val(ar)),
        )

    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        find_remaining_bytes(error_report, child, parent_node_name)


# Report codestream errors
def find_errors(error_report, ar, parent_node=None):
    typ = ar[XML_TYPE_IDX]
    value = ar[XML_VALUE_IDX]
    if typ == gdal.CXT_Element and value == "Error":
        parent_node_name = ""
        if parent_node is not None:
            parent_node_name = get_attribute_val(parent_node, "name")
            if parent_node_name is None:
                parent_node_name = parent_node[XML_VALUE_IDX]
        error_report.EmitError(
            "GENERAL",
            "Codestream error found on element %s: %s"
            % (parent_node_name, find_message(ar)),
        )

    for child_idx in range(XML_FIRST_CHILD_IDX, len(ar)):
        child = ar[child_idx]
        find_errors(error_report, child, ar)


def validate_bitsize(error_report, inspire_tg, val_ori, field_name, datatype):
    val = val_ori
    signedness = "unsigned"
    nbits = 0
    if val is not None:
        if val >= 128:
            signedness = "signed"
            val -= 128
        val += 1
        nbits = val
    if inspire_tg and val != 1 and val != 8 and val != 16 and val != 32:
        error_report.EmitError(
            "INSPIRE_TG",
            "%s=%s (%s %d bits), which is not allowed"
            % (field_name, str(val_ori), signedness, nbits),
            requirement=24,
            conformance_class="A.8.9",
        )
    elif (
        inspire_tg
        and datatype == "imagery"
        and ((val != 1 and val != 8 and val != 16) or val_ori >= 128)
    ):
        error_report.EmitError(
            "INSPIRE_TG",
            "%s=%s (%s %d bits), which is not allowed for Orthoimagery (but OK for other data)"
            % (field_name, str(val_ori), signedness, nbits),
            requirement=27,
            conformance_class="A.8.9",
        )
    elif val is None or val > 37:
        error_report.EmitError(
            "GENERAL",
            "%s=%s (%s %d bits), which is not allowed"
            % (field_name, str(val_ori), signedness, nbits),
        )


def int_or_none(val):
    if val is None:
        return None
    return int(val)


def check_geojp2_gmljp2_consistency(filename, error_report):
    gdal.SetConfigOption("GDAL_USE_GEOJP2", "YES")
    gdal.SetConfigOption("GDAL_USE_GMLJP2", "NO")
    ds = gdalOpenWithOpenJPEGDriverPreferably(filename)
    if ds is None:
        error_report.EmitError(
            "GENERAL", "Cannot open %s with a JPEG2000 compatible driver" % filename
        )
        return
    geojp2_gt = ds.GetGeoTransform()
    geojp2_wkt = ds.GetProjectionRef()
    geojp2_gcps = ds.GetGCPCount()
    ds = None

    gdal.SetConfigOption("GDAL_USE_GEOJP2", "NO")
    gdal.SetConfigOption("GDAL_USE_GMLJP2", "YES")
    ds = gdalOpenWithOpenJPEGDriverPreferably(filename)
    gmljp2_gt = ds.GetGeoTransform()
    gmljp2_wkt = ds.GetProjectionRef()
    ds = None

    gdal.SetConfigOption("GDAL_USE_GEOJP2", None)
    gdal.SetConfigOption("GDAL_USE_GMLJP2", None)

    if geojp2_gcps == 0:
        diff = False
        for i in range(6):
            if abs(geojp2_gt[i] - gmljp2_gt[i] > 1e-8):
                diff = True
        if diff:
            error_report.EmitError(
                "GENERAL",
                "Inconsistent geotransform between GeoJP2 (%s) and GMLJP2 (%s)"
                % (str(geojp2_gt), str(gmljp2_gt)),
            )

    geojp2_sr = osr.SpatialReference()
    geojp2_sr.ImportFromWkt(geojp2_wkt)
    geojp2_epsg_code = geojp2_sr.GetAuthorityCode(None)
    gmljp2_sr = osr.SpatialReference()
    gmljp2_sr.ImportFromWkt(gmljp2_wkt)
    gmljp2_epsg_code = gmljp2_sr.GetAuthorityCode(None)
    if geojp2_sr.IsSame(gmljp2_sr) == 0 and geojp2_epsg_code != gmljp2_epsg_code:
        geojp2_proj4 = geojp2_sr.ExportToProj4()
        gmljp2_proj4 = gmljp2_sr.ExportToProj4()
        if geojp2_proj4 != gmljp2_proj4:
            error_report.EmitError(
                "GENERAL",
                "Inconsistent SRS between GeoJP2 (wkt=%s, proj4=%s) and GMLJP2 (wkt=%s, proj4=%s)"
                % (geojp2_wkt, geojp2_proj4, gmljp2_wkt, gmljp2_proj4),
            )


# Check consistency of georeferencing of OrthoimageCoverage with the one embedded in the JPEG2000 file
def check_oi_rg_consistency(filename, serialized_oi_rg, error_report):
    if gdal.GetDriverByName("JP2OpenJPEG") is None:
        return

    ds = gdalOpenWithOpenJPEGDriverPreferably(filename)
    if ds is None:
        error_report.EmitError(
            "GENERAL", "Cannot open %s with a JPEG2000 compatible driver" % filename
        )
        return "fail"
    gt = ds.GetGeoTransform()
    wkt = ds.GetProjectionRef()
    gcps = ds.GetGCPCount()
    ds = None

    gmljp2_from_oi = (
        """<gml:FeatureCollection xmlns:gml="http://www.opengis.net/gml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/gml http://schemas.opengis.net/gml/3.1.1/profiles/gmlJP2Profile/1.0.0/gmlJP2Profile.xsd">
        <gml:boundedBy>
        <gml:Null>withheld</gml:Null>
        </gml:boundedBy>
        <gml:featureMember>
        <gml:FeatureCollection>
            <gml:featureMember>
            <gml:RectifiedGridCoverage dimension="2" gml:id="RGC0001">
                <gml:rectifiedGridDomain>
                    %s
                </gml:rectifiedGridDomain>
                <gml:rangeSet>
                <gml:File>
                    <gml:rangeParameters />
                    <gml:fileName>gmljp2://codestream/0</gml:fileName>
                    <gml:fileStructure>Record Interleaved</gml:fileStructure>
                </gml:File>
                </gml:rangeSet>
            </gml:RectifiedGridCoverage>
            </gml:featureMember>
        </gml:FeatureCollection>
        </gml:featureMember>
    </gml:FeatureCollection>"""
        % serialized_oi_rg
    )
    gdal.SetConfigOption("GMLJP2OVERRIDE", "/vsimem/override.gml")
    gdal.FileFromMemBuffer("/vsimem/override.gml", gmljp2_from_oi)
    fake_in_ds = gdal.GetDriverByName("MEM").Create("", 10, 10, 1)
    fake_in_ds.SetGeoTransform([0, 60, 0, 0, 0, -60])
    gdal.GetDriverByName("JP2OpenJPEG").CreateCopy(
        "/vsimem/temp.jp2", fake_in_ds, options=["GeoJP2=NO"]
    )
    gdal.SetConfigOption("GMLJP2OVERRIDE", None)
    gdal.Unlink("/vsimem/override.gml")

    ds = gdalOpenWithOpenJPEGDriverPreferably("/vsimem/temp.jp2")
    oi_gt = ds.GetGeoTransform()
    oi_wkt = ds.GetProjectionRef()
    ds = None
    gdal.Unlink("/vsimem/temp.jp2")

    if gcps == 0:
        diff = False
        for i in range(6):
            if abs(oi_gt[i] - gt[i] > 1e-8):
                diff = True
        if diff:
            error_report.EmitError(
                "INSPIRE_TG",
                "Inconsistent geotransform between OrthoImagery (%s) and GMLJP2/GeoJP2 (%s)"
                % (str(oi_gt), str(gt)),
                conformance_class="A.8.8",
            )

    sr = osr.SpatialReference()
    sr.ImportFromWkt(wkt)
    epsg_code = sr.GetAuthorityCode(None)
    oi_sr = osr.SpatialReference()
    oi_sr.ImportFromWkt(oi_wkt)
    oi_epsg_code = oi_sr.GetAuthorityCode(None)
    if sr.IsSame(oi_sr) == 0 and epsg_code != oi_epsg_code:
        proj4 = sr.ExportToProj4()
        oi_proj4 = oi_sr.ExportToProj4()
        if proj4 != oi_proj4:
            error_report.EmitError(
                "INSPIRE_TG",
                "Inconsistent SRS between OrthoImagery (wkt=%s, proj4=%s) and GMLJP2/GeoJP2 (wkt=%s, proj4=%s)"
                % (wkt, proj4, oi_wkt, oi_proj4),
                conformance_class="A.8.8",
            )


def validate(
    filename,
    oidoc,
    inspire_tg,
    expected_gmljp2,
    ogc_schemas_location,
    datatype="imagery",
    error_report=None,
    expected_ftyp_branding=None,
):

    if error_report is None:
        error_report = ErrorReport()

    ar = gdal.GetJPEG2000Structure(filename, ["ALL=YES"])
    if ar is None:
        error_report.error_count = 1
        return error_report

    find_remaining_bytes(error_report, ar)
    find_errors(error_report, ar)

    if inspire_tg and ar[XML_VALUE_IDX] != "JP2File":
        error_report.EmitError(
            "INSPIRE_TG",
            "The file contains only a JPEG2000 codestream, instead of being a JP2 file",
        )
        return error_report

    ihdr = None
    ihdr_nc = 0
    pclr = None
    bpc_vals = []

    if ar[XML_VALUE_IDX] == "JP2File":

        # Check "jP " box
        if not find_jp2box(ar, "jP  "):
            error_report.EmitError("GENERAL", '"jp  " box not found')

        # Detect GMLJP2 and validate it if possible
        gmljp2 = get_gmljp2(filename)
        gmljp2_found = gmljp2 is not None
        if expected_gmljp2 and not gmljp2_found:
            error_report.EmitError(
                "GMLJP2", "No GMLJP2 box found whereas it was expected"
            )
        if gmljp2_found and inspire_tg and "gmljp2:GMLJP2CoverageCollection" in gmljp2:
            error_report.EmitError(
                "INSPIRE_TG",
                "GMLJP2 v2 box found, but Inspire TG require GMLJP2 v1",
                conformance_class="A.8.6",
            )
        if gmljp2_found and ogc_schemas_location != "disabled":
            if ogc_schemas_location is not None:
                sys.path.append(
                    os.path.dirname(os.path.realpath(__file__))
                    + "/../../../../autotest/pymod"
                )
                try:
                    import xmlvalidate

                    xml_validate_found = True
                except ImportError:
                    error_report.EmitWarning(
                        "GMLJP2", "xmlvalidate not found or not runnable"
                    )
                    xml_validate_found = False
                if xml_validate_found:
                    if not xmlvalidate.validate(
                        gmljp2, ogc_schemas_location=ogc_schemas_location
                    ):
                        error_report.EmitError(
                            "GMLJP2",
                            "Validation of GMLJP2 document failed",
                            conformance_class="A.8.6",
                        )
            else:
                error_report.EmitWarning(
                    "GMLJP2", "-ogc_schemas_location not specified"
                )

        # Check that there's only one GeoTIFF box
        count_uuidboxes = get_count_of_uuidboxes(ar)
        if "B14BF8BD083D4B43A5AE8CD7D5A6CE03" in count_uuidboxes:
            count_geotiff_boxes = count_uuidboxes["B14BF8BD083D4B43A5AE8CD7D5A6CE03"]
            if count_geotiff_boxes > 1:
                error_report.EmitError(
                    "GeoJP2", "%d GeoTIFF UUID box found" % count_geotiff_boxes
                )

        # Check the content of a GeoTIFF UUID box
        geotiff_found = (
            find_element_with_name(ar, "UUID", "GeoTIFF", attribute_name="description")
            is not None
        )
        decoded_geotiff = find_xml_node(ar, "DecodedGeoTIFF")
        if geotiff_found:
            if not decoded_geotiff:
                error_report.EmitError(
                    "GeoJP2", "GeoTIFF UUID box found, but content is not valid GeoTIFF"
                )
            else:
                vrtdataset = find_xml_node(ar, "VRTDataset")
                w = get_attribute_val(vrtdataset, "rasterXSize")
                if w != "1":
                    error_report.EmitError(
                        "GeoJP2",
                        "GeoTIFF should have width of 1 pixel, not %s" % str(w),
                    )
                h = get_attribute_val(vrtdataset, "rasterXSize")
                if h != "1":
                    error_report.EmitError(
                        "GeoJP2",
                        "GeoTIFF should have height of 1 pixel, not %s" % str(h),
                    )

        # Check that information of GeoJP2 and GMLJP2 are consistent
        if geotiff_found and gmljp2_found:
            check_geojp2_gmljp2_consistency(filename, error_report)

        # Check "ftyp" box
        ftyp = find_jp2box(ar, "ftyp")
        if ftyp:
            JP2CLFound = False
            JPXCLFound = False

            if expected_ftyp_branding is None:
                if gmljp2_found and "gmljp2:GMLJP2CoverageCollection" in gmljp2:
                    expected_ftyp_branding = "jpx "
                elif gmljp2_found:
                    # GMLJP2 v1 uses asoc/lbl boxes from JPEG2000 Part II;
                    # "jpx " branding is valid per section 8.1 of 05-047r3
                    actual_br = get_field_val(ftyp, "BR")
                    if actual_br in ("jp2 ", "jpx "):
                        expected_ftyp_branding = actual_br
                    else:
                        expected_ftyp_branding = "jp2 "
                else:
                    expected_ftyp_branding = "jp2 "

            if get_field_val(ftyp, "BR") != expected_ftyp_branding:
                error_report.EmitError(
                    "GENERAL",
                    'ftyp.BR = "%s" instead of "%s"'
                    % (get_field_val(ftyp, "BR"), expected_ftyp_branding),
                )

            if get_field_val(ftyp, "MinV") != "0":
                error_report.EmitError(
                    "GENERAL",
                    'ftyp.MinV = "%s" instead of 0' % get_field_val(ftyp, "MinV"),
                )

            for i in range(10):
                val = get_field_val(ftyp, "CL%d" % i)
                if val is None:
                    break
                if val == "jp2 ":
                    JP2CLFound = True
                if val == "jpx ":
                    JPXCLFound = True
            if not JP2CLFound:
                error_report.EmitError(
                    "GENERAL", '"jp2 " not found in compatibility list of ftyp'
                )
            if gmljp2_found and not JPXCLFound:
                if inspire_tg:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        '"jpx " not found in compatibility list of ftyp, but GMLJP2 box present',
                    )
                else:
                    error_report.EmitWarning(
                        "GENERAL",
                        '"jpx " not found in compatibility list of ftyp, but GMLJP2 box present',
                    )
        else:
            error_report.EmitError("GENERAL", '"ftyp" box not found')

        # Check "rreq" box
        rreq = find_jp2box(ar, "rreq")
        if inspire_tg and gmljp2_found and not rreq:
            error_report.EmitError(
                "INSPIRE_TG", '"rreq" box not found, but GMLJP2 box present'
            )
        elif rreq:
            NSF = get_field_val(rreq, "NSF")
            if NSF is None:
                error_report.EmitError("GENERAL", "rreq.NSF not found")
                NSF = 0
            NSF = int(NSF)
            SF67Found = False
            NSF_found = 0
            for i in range(1000):
                val = get_field_val(rreq, "SF%d" % i)
                if val is None:
                    break
                if val == "67":
                    SF67Found = True
                val = get_field_val(rreq, "SM%d" % i)
                if val is None:
                    error_report.EmitError("GENERAL", "rreq.SM[%d] not found" % i)
                    break
                NSF_found += 1
            if NSF != NSF_found:
                error_report.EmitError(
                    "GENERAL", "rreq.NSF (=%d) != NSF_found (=%d)" % (NSF, NSF_found)
                )
            if gmljp2_found and not SF67Found:
                if inspire_tg:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        '"rreq" box does not advertise standard flag 67 whereas GMLJP2 box is present',
                    )
                else:
                    error_report.EmitWarning(
                        "GENERAL",
                        '"rreq" box does not advertise standard flag 67 whereas GMLJP2 box is present',
                    )

            NVF = get_field_val(rreq, "NVF")
            if NVF is None:
                error_report.EmitError("GENERAL", "rreq.NVF not found")
                NVF = 0
            NVF = int(NVF)
            NVF_found = 0
            for i in range(1000):
                val = get_field_val(rreq, "VF%d" % i)
                if val is None:
                    break
                val = get_field_val(rreq, "VM%d" % i)
                if val is None:
                    error_report.EmitError("GENERAL", "rreq.VM[%d] not found" % i)
                    break
                NVF_found += 1
            if NVF != NVF_found:
                error_report.EmitError(
                    "GENERAL", "rreq.NVF (=%d) != NVF_found (=%d)" % (NVF, NVF_found)
                )

        # Check "jp2h" box
        jp2h = find_jp2box(ar, "jp2h")
        if not jp2h:
            error_report.EmitError("GENERAL", '"jp2h" box not found')
        else:
            # Check "ihdr" subbox
            ihdr = find_jp2box(jp2h, "ihdr")
            if not ihdr:
                error_report.EmitError("GENERAL", '"ihdr" box not found')
            else:
                ihdr_height = int_or_none(get_field_val(ihdr, "HEIGHT"))
                if ihdr_height is None:
                    error_report.EmitError(
                        "GENERAL", "invalid value ihdr.HEIGHT = %s" % str(ihdr_height)
                    )
                    ihdr_height = 0
                elif inspire_tg and ihdr_height > 2**31:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        "ihdr.height = %d, whereas only 31 bits are allowed for Profile 1"
                        % ihdr_height,
                    )

                ihdr_width = int_or_none(get_field_val(ihdr, "WIDTH"))
                if ihdr_width is None:
                    error_report.EmitError(
                        "GENERAL", "invalid value ihdr.WIDTH = %s" % str(ihdr_width)
                    )
                    ihdr_width = 0
                elif inspire_tg and ihdr_width > 2**31:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        "ihdr.width = %d, whereas only 31 bits are allowed for Profile 1"
                        % ihdr_width,
                    )

                ihdr_nc = int_or_none(get_field_val(ihdr, "NC"))
                if ihdr_nc is None or ihdr_nc > 16384:
                    error_report.EmitError(
                        "GENERAL", "invalid value ihdr.NC = %s" % str(ihdr_nc)
                    )
                    ihdr_nc = 0

                ihdr_bpcc = int_or_none(get_field_val(ihdr, "BPC"))
                if ihdr_bpcc != 255:
                    validate_bitsize(
                        error_report, inspire_tg, ihdr_bpcc, "ihdr.bpcc", datatype
                    )

                ihdr_c = int_or_none(get_field_val(ihdr, "C"))
                if ihdr_c != 7:
                    error_report.EmitError(
                        "GENERAL", "ihdr.C = %s instead of 7" % str(ihdr_c)
                    )

                ihdr_unkc = int_or_none(get_field_val(ihdr, "UnkC"))
                if ihdr_unkc is None or ihdr_unkc > 1:
                    error_report.EmitError(
                        "GENERAL", "ihdr.UnkC = %s instead of 0 or 1" % str(ihdr_unkc)
                    )

                ihdr_ipr = int_or_none(get_field_val(ihdr, "IPR"))
                if ihdr_ipr is None or ihdr_ipr > 1:
                    error_report.EmitError(
                        "GENERAL", "ihdr.IPR = %s instead of 0 or 1" % str(ihdr_ipr)
                    )

            # Check optional "bpcc" subbox
            bpcc = find_jp2box(jp2h, "bpcc")
            if ihdr and ihdr_bpcc == 255:
                if not bpcc:
                    error_report.EmitError(
                        "GENERAL", '"bpcc" box not found whereas ihdr.bpcc requires it'
                    )
            elif ihdr and bpcc and ihdr_bpcc != 255:
                error_report.EmitWarning(
                    "GENERAL", '"bpcc" box found whereas ihdr.bpcc does not require it'
                )
            if ihdr and bpcc:
                for i in range(16384):
                    val = get_field_val(bpcc, "BPC%d" % i)
                    if val is None:
                        break
                    val = int(val)
                    bpc_vals.append(val)

                    validate_bitsize(
                        error_report, inspire_tg, val, "bpcc.BPC[%d]" % i, datatype
                    )

                if len(bpc_vals) != ihdr_nc:
                    error_report.EmitWarning(
                        "GENERAL",
                        '"bpcc" box has %d elements whereas ihdr.nc = %d'
                        % (len(bpc_vals), ihdr_nc),
                    )

            if ihdr and not bpcc:
                bpc_vals = [ihdr_bpcc for i in range(ihdr_nc)]

            # Check "colr" subbox
            colr = find_jp2box(jp2h, "colr")
            if not colr:
                error_report.EmitError("GENERAL", '"colr" box not found')
            else:
                meth = int_or_none(get_field_val(colr, "METH"))
                if meth != 1 and meth != 2:
                    error_report.EmitWarning(
                        "GENERAL", "Unknown value %s for colr.METH" % str(meth)
                    )

                prec = int_or_none(get_field_val(colr, "PREC"))
                if prec is None or (inspire_tg and prec != 0):
                    error_report.EmitWarning(
                        "GENERAL", "Unknown value %s for colr.PREC" % str(prec)
                    )

                approx = int_or_none(get_field_val(colr, "APPROX"))
                if approx or (inspire_tg and approx != 0):
                    error_report.EmitWarning(
                        "GENERAL", "Unknown value %s for colr.APPROX" % str(approx)
                    )

                if meth == 1:
                    enum_cs = int_or_none(get_field_val(colr, "EnumCS"))
                    if enum_cs is None or (
                        inspire_tg and enum_cs != 16 and enum_cs != 17 and enum_cs != 18
                    ):
                        error_report.EmitWarning(
                            "GENERAL", "Unknown value %s for colr.EnumCS" % str(enum_cs)
                        )
                else:
                    enum_cs = None

            # Check optional "pclr" subbox
            pclr = find_jp2box(jp2h, "pclr")
            if ihdr and pclr:
                if inspire_tg and ihdr_nc != 1:
                    error_report.EmitError(
                        "INSPIRE_TG", "pclr box found but ihdr.nc = %d" % (ihdr_nc)
                    )
                pclr_NE = int_or_none(get_field_val(pclr, "NE"))
                if pclr_NE is None:
                    error_report.EmitWarning(
                        "GENERAL", "Invalid value %s for pclr.NE" % str(pclr_NE)
                    )
                    pclr_NE = 0

                pclr_NPC = int_or_none(get_field_val(pclr, "NPC"))
                if pclr_NPC is None:
                    error_report.EmitWarning(
                        "GENERAL", "Invalid value %s for pclr.NPC" % str(pclr_NPC)
                    )
                    pclr_NPC = 0
                if inspire_tg and pclr_NPC != 3:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        "pclr.NPC(=%d) != 3 (for color table)" % (pclr_NPC),
                        conformance_class="A.8.6",
                    )

                if ihdr_bpcc == 7 and pclr_NE > 256:
                    error_report.EmitError(
                        "GENERAL", "%d entries in pclr box, but 8 bit depth" % (pclr_NE)
                    )
                for i in range(pclr_NPC):
                    val = get_field_val(pclr, "B%d" % i)
                    if val is None:
                        error_report.EmitError("GENERAL", "pclr.B%d not found" % (i))
                        break
                    val = int(val)
                    validate_bitsize(
                        error_report, inspire_tg, val, "pclr.B[%d]" % i, datatype
                    )
                if pclr_NE > 0 and pclr_NPC > 0:
                    val = get_field_val(pclr, "C_%d_%d" % (pclr_NE - 1, pclr_NPC - 1))
                    if val is None:
                        error_report.EmitError(
                            "GENERAL",
                            "pclr.C_%d_%d not found" % (pclr_NE - 1, pclr_NPC - 1),
                        )

            # Check optional "cmap" subbox
            cmap = find_jp2box(jp2h, "cmap")
            if cmap:
                if pclr is None:
                    error_report.EmitError("GENERAL", "cmap box found but no pclr box")
                else:
                    cmap_count = 0
                    PCOL_mapping = {}
                    for i in range(16384):
                        CMP = get_field_val(cmap, "CMP%d" % i)
                        if CMP is None:
                            break
                        CMP = int(CMP)
                        if CMP >= ihdr_nc:
                            error_report.EmitError(
                                "GENERAL", "cmap.CMP[%d] = %d is invalid" % (i, CMP)
                            )

                        MTYP = get_field_val(cmap, "MTYP%d" % i)
                        if MTYP is None:
                            error_report.EmitError(
                                "GENERAL", "cmap.MTYP[%d] missing" % i
                            )
                            break
                        MTYP = int(MTYP)
                        if inspire_tg and MTYP != 1:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "cmap.MTYP[%d] = %d is invalid" % (i, MTYP),
                            )

                        PCOL = get_field_val(cmap, "PCOL%d" % i)
                        if PCOL is None:
                            error_report.EmitError(
                                "GENERAL", "cmap.PCOL[%d] missing" % i
                            )
                            break
                        PCOL = int(PCOL)
                        if ihdr_nc == 1 and PCOL >= pclr_NPC:
                            error_report.EmitError(
                                "GENERAL", "cmap.PCOL[%d] = %d is invalid" % (i, PCOL)
                            )
                        if MTYP == 1:
                            if PCOL in PCOL_mapping:
                                error_report.EmitError(
                                    "GENERAL",
                                    "cmap.PCOL[%d] = %d is invalid since already used"
                                    % (i, PCOL),
                                )
                            PCOL_mapping[PCOL] = True

                        cmap_count += 1

                    if ihdr_nc == 1 and cmap_count != pclr_NPC:
                        error_report.EmitError(
                            "GENERAL",
                            "cmap box contains %d channel definitions but pclr.NPC = %d"
                            % (cmap_count, pclr_NPC),
                        )

            else:
                if pclr:
                    error_report.EmitError(
                        "GENERAL", "cmap box not found but pclr exists"
                    )

            # Check optional "cdef" subbox
            cdef = find_jp2box(jp2h, "cdef")
            transparent_index = -1
            if cdef and pclr:
                error_report.EmitWarning(
                    "GENERAL",
                    "cdef found and pclr also. Ignoring cdef as it is unusual",
                )
            elif ihdr and cdef:
                cdef_N = int(get_field_val(cdef, "N"))
                if cdef_N != ihdr_nc:
                    error_report.EmitError(
                        "GENERAL",
                        "cdef.N = %d whereas ihdr.nc = %d" % (cdef_N, ihdr_nc),
                    )
                cdef_count = 0
                cn_mapping = {}
                typ_alpha_used = False
                asoc_mapping = {}
                asoc_whole_used = False
                for i in range(16384):
                    cn = get_field_val(cdef, "Cn%d" % i)
                    if cn is None:
                        break
                    cn = int(cn)
                    if cn < 0 or cn >= ihdr_nc:
                        error_report.EmitError(
                            "GENERAL", "cdef.cn[%d] = %d is invalid" % (i, cn)
                        )
                    elif cn in cn_mapping:
                        error_report.EmitError(
                            "GENERAL",
                            "cdef.cn[%d] = %d is invalid since already used" % (i, cn),
                        )
                    cn_mapping[cn] = True

                    typ = get_field_val(cdef, "Typ%d" % i)
                    if typ is None:
                        error_report.EmitError("GENERAL", "cdef.typ[%d] missing" % i)
                        break
                    typ = int(typ)
                    if typ != 0 and typ != 1 and typ != 2 and typ != 65535:
                        error_report.EmitError(
                            "GENERAL", "cdef.typ[%d] = %d is invalid" % (i, typ)
                        )
                    if typ == 1 or typ == 2:
                        if inspire_tg and cn < len(bpc_vals) and bpc_vals[cn] != 0:
                            error_report.EmitWarning(
                                "INSPIRE_TG",
                                "Bit depth of alpha channel should be 1 (BPCC 0), but its BPCC is %d"
                                % bpc_vals[cn],
                                recommendation=38,
                            )
                        if typ_alpha_used and inspire_tg:
                            error_report.EmitError(
                                "GENERAL",
                                "cdef.typ[%d] = %d is invalid since another alpha channel has already been defined"
                                % (i, typ),
                            )
                        transparent_index = cn
                        typ_alpha_used = True

                    asoc = get_field_val(cdef, "Asoc%d" % i)
                    if asoc is None:
                        error_report.EmitError("GENERAL", "cdef.asoc[%d] missing" % i)
                        break
                    asoc = int(asoc)
                    if inspire_tg and asoc == 0:
                        if not (typ == 1 or typ == 2):
                            error_report.EmitError(
                                "GENERAL",
                                "cdef.asoc[%d] = %d whereas cdef.typ[%d] = %d"
                                % (i, asoc, i, typ),
                            )
                        if asoc_whole_used:
                            error_report.EmitError(
                                "GENERAL",
                                "cdef.asoc[%d] = %d is invalid since another band has already been associated to whole image"
                                % (i, asoc),
                            )
                        asoc_whole_used = True
                    elif colr and asoc > 0 and asoc < 65535:
                        if asoc > ihdr_nc:
                            error_report.EmitError(
                                "GENERAL", "cdef.asoc[%d] = %d is invalid" % (i, asoc)
                            )
                        elif asoc in asoc_mapping:
                            error_report.EmitError(
                                "GENERAL",
                                "cdef.asoc[%d] = %d is invalid since already used"
                                % asoc,
                            )
                        asoc_mapping[asoc] = True

                    cdef_count += 1

                if cdef_count != cdef_N:
                    error_report.EmitError(
                        "GENERAL",
                        "cdef.N = %d whereas there are %d channel definitions"
                        % (cdef_N, cdef_count),
                    )

            # Check that all bands have the same bit-depth, except the alpha band than can (should) be 1-bit
            if inspire_tg:
                for i, bpc_val in enumerate(bpc_vals):
                    if i == transparent_index:
                        if bpc_val != bpc_vals[0] and bpc_val != 0:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "Band %d has bpc=%d, which is different from first band whose value is %d"
                                % (i, bpc_val, bpc_vals[0]),
                                requirement=25,
                            )
                    elif bpc_val != bpc_vals[0]:
                        error_report.EmitError(
                            "INSPIRE_TG",
                            "Band %d has bpc=%d, which is different from first band whose value is %d"
                            % (i, bpc_val, bpc_vals[0]),
                            requirement=25,
                        )

            # Check optional "res " subbox
            res = find_jp2box(jp2h, "res ")
            if res:
                count_boxes = get_count_and_indices_of_jp2boxes(res)
                for key in count_boxes:
                    val, _ = count_boxes[key]
                    if val > 1:
                        error_report.EmitError(
                            "GENERAL",
                            '"%s" box expected to be found zero or one time, but present %d times'
                            % (key, val),
                        )
                    if key not in ("resd", "resc"):
                        error_report.EmitWarning(
                            "GENERAL", '"%s" box not expected' % key
                        )

            # Check number of sub-boxes
            count_boxes = get_count_and_indices_of_jp2boxes(jp2h)
            for key in count_boxes:
                val, _ = count_boxes[key]
                if val > 1:
                    error_report.EmitError(
                        "GENERAL",
                        '"%s" box expected to be found zero or one time, but present %d times'
                        % (key, val),
                    )
                if key not in ("ihdr", "bpcc", "colr", "pclr", "cmap", "cdef", "res "):
                    error_report.EmitWarning("GENERAL", '"%s" box not expected' % key)

            # Check order of boxes
            last_idx = -1
            for box_name in ["ihdr", "bpcc", "colr", "pclr", "cmap", "cdef", "res "]:
                if box_name in count_boxes:
                    _, idx = count_boxes[box_name]
                    if idx < last_idx:
                        error_report.EmitWarning(
                            "GENERAL", '"%s" box not at expected index' % box_name
                        )
                    last_idx = idx

        # Check "jp2c" box
        jp2c = find_jp2box(ar, "jp2c")
        if not jp2c:
            error_report.EmitError("GENERAL", '"jp2c" box not found')

        if ihdr and ihdr_ipr == 1 and not find_jp2box(ar, "jp2i"):
            error_report.EmitWarning("GENERAL", "ihdr.ipr = 1 but no jp2i box found")
        elif ihdr and ihdr_ipr == 0 and find_jp2box(ar, "jp2i"):
            error_report.EmitWarning("GENERAL", "ihdr.ipr = 0 but jp2i box found")

        # Check number of boxes
        count_boxes = get_count_and_indices_of_jp2boxes(ar)
        for key in count_boxes:
            val, _ = count_boxes[key]
            if key in ("jP  ", "ftyp", "rreq", "jp2h", "jp2c"):
                if key == "jp2c":
                    if inspire_tg and val > 1:
                        error_report.EmitError(
                            "INSPIRE_TG",
                            '"%s" box expected to be found one time, but present %d times'
                            % (key, val),
                            requirement=23,
                            conformance_class="A.8.15",
                        )
                elif val > 1:
                    error_report.EmitError(
                        "GENERAL",
                        '"%s" box expected to be found zero or one time, but present %d times'
                        % (key, val),
                    )
            elif key not in ("jp2i", "asoc", "xml ", "uuid", "uinf"):
                error_report.EmitWarning("GENERAL", '"%s" box not expected' % key)

        # Check order of boxes
        last_idx = -1
        for box_name in ["jP  ", "ftyp", "rreq", "jp2h", "jp2c", "jp2i"]:
            if box_name in count_boxes:
                _, idx = count_boxes[box_name]
                if idx < last_idx:
                    error_report.EmitWarning(
                        "GENERAL", '"%s" box not at expected index' % box_name
                    )
                last_idx = idx
        if inspire_tg:
            for box_name in ["asoc", "xml ", "uuid", "uinf"]:
                if box_name in count_boxes:
                    _, idx = count_boxes[box_name]
                    if idx < last_idx:
                        error_report.EmitWarning(
                            "INSPIRE_TG", '"%s" box not at expected index' % box_name
                        )
                    last_idx = idx

    cs = find_xml_node(ar, "JP2KCodeStream")
    if cs is None:
        return error_report

    soc = find_marker(cs, "SOC")
    if not soc:
        error_report.EmitError("GENERAL", "No SOC marker found")

    # Validate content of SIZ marker
    siz = find_marker(cs, "SIZ")
    Csiz = 0
    Xsiz = 0
    Ysiz = 0
    XOsiz = 0
    YOsiz = 0
    Rsiz = 0
    tab_Ssiz = []
    if not siz:
        error_report.EmitError("GENERAL", "No SIZ marker found")
    else:
        while True:
            Csiz = get_field_val(siz, "Csiz")
            if Csiz is None:
                error_report.EmitError("GENERAL", "SIZ.Csiz not found")
                break
            Csiz = int(Csiz)

            Rsiz = int(get_field_val(siz, "Rsiz"))
            if inspire_tg and Rsiz != 2:
                error_report.EmitError(
                    "INSPIRE_TG",
                    "SIZ.Rsiz=%d found but 2 (Profile 1) expected" % Rsiz,
                    requirement=21,
                )

            Xsiz = int(get_field_val(siz, "Xsiz"))
            Ysiz = int(get_field_val(siz, "Ysiz"))
            XOsiz = int(get_field_val(siz, "XOsiz"))
            YOsiz = int(get_field_val(siz, "YOsiz"))
            XTsiz = int(get_field_val(siz, "XTsiz"))
            YTsiz = int(get_field_val(siz, "YTsiz"))
            XTOSiz = int(get_field_val(siz, "XTOSiz"))
            YTOSiz = int(get_field_val(siz, "YTOSiz"))

            if (inspire_tg or Rsiz == 2) and Xsiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "Xsiz = %d, whereas only 31 bits are allowed for Profile 1" % Xsiz,
                )
            if (inspire_tg or Rsiz == 2) and Ysiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "Ysiz = %d, whereas only 31 bits are allowed for Profile 1" % Ysiz,
                )
            if (inspire_tg or Rsiz == 2) and XOsiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "XOsiz = %d, whereas only 31 bits are allowed for Profile 1"
                    % XOsiz,
                )
            if (inspire_tg or Rsiz == 2) and YOsiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "YOsiz = %d, whereas only 31 bits are allowed for Profile 1"
                    % YOsiz,
                )
            if (inspire_tg or Rsiz == 2) and XTOSiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "XTOSiz = %d, whereas only 31 bits are allowed for Profile 1"
                    % XTOSiz,
                )
            if (inspire_tg or Rsiz == 2) and YTOSiz >= 2**31:
                error_report.EmitError(
                    "PROFILE_1",
                    "YTOSiz = %d, whereas only 31 bits are allowed for Profile 1"
                    % YTOSiz,
                )
            if ihdr and ihdr_width != Xsiz - XOsiz:
                error_report.EmitError(
                    "GENERAL",
                    "ihdr_width(=%d) != Xsiz (=%d)- XOsiz(=%d)"
                    % (ihdr_width, Xsiz, XOsiz),
                )
            if ihdr and ihdr_height != Ysiz - YOsiz:
                error_report.EmitError(
                    "GENERAL",
                    "ihdr_height(=%d) != Ysiz(=%d) - YOsiz(=%d)"
                    % (ihdr_height, Ysiz, YOsiz),
                )
            if ihdr and ihdr_nc != Csiz:
                error_report.EmitError(
                    "GENERAL", "ihdr_nc(=%d) != Csiz (=%d)" % (ihdr_nc, Csiz)
                )

            min_XYRSiz = 255
            for i in range(Csiz):
                Ssiz = get_field_val(siz, "Ssiz%d" % i)
                if Ssiz is None:
                    error_report.EmitError("GENERAL", "SIZ.Ssiz[%d] not found" % i)
                    break
                Ssiz = int(Ssiz)
                validate_bitsize(
                    error_report, inspire_tg, Ssiz, "SIZ.Ssiz[%d]" % i, datatype
                )
                tab_Ssiz.append(Ssiz)

                if bpc_vals and i < len(bpc_vals) and bpc_vals[i] != Ssiz:
                    error_report.EmitError(
                        "GENERAL",
                        "SIZ.Ssiz[%d]=%s, whereas bpcc[%d]=%s"
                        % (i, str(Ssiz), i, str(bpc_vals[i])),
                    )

                XRsiz = get_field_val(siz, "XRsiz%d" % i)
                if XRsiz is None:
                    error_report.EmitError("GENERAL", "SIZ.XRsiz[%d] not found" % i)
                    break
                XRsiz = int(XRsiz)
                if XRsiz == 0:
                    error_report.EmitError(
                        "GENERAL", "XRsiz[%d] = %d is invalid" % (i, XRsiz)
                    )
                elif XRsiz < min_XYRSiz:
                    min_XYRSiz = XRsiz

                YRsiz = get_field_val(siz, "YRsiz%d" % i)
                if YRsiz is None:
                    error_report.EmitError("GENERAL", "SIZ.YRsiz[%d] not found" % i)
                    break
                YRsiz = int(YRsiz)
                if YRsiz == 0:
                    error_report.EmitError(
                        "GENERAL", "YRsiz[%d] = %d is invalid" % (i, YRsiz)
                    )
                elif YRsiz < min_XYRSiz:
                    min_XYRSiz = YRsiz

            tiled = not (XTsiz + XTOSiz >= Xsiz and YTsiz + YTOSiz >= Ysiz)
            if (inspire_tg or Rsiz == 2) and XTsiz / min_XYRSiz > 1024 and tiled:
                error_report.EmitError(
                    "PROFILE_1",
                    "XTsiz / min_XYRSiz = %f > 1024" % (1.0 * XTsiz / min_XYRSiz),
                )
            if (inspire_tg or Rsiz == 2) and XTsiz != YTsiz and tiled:
                error_report.EmitError(
                    "PROFILE_1", "XTsiz (=%d) != YTsiz (=%d)" % (XTsiz, YTsiz)
                )

            break

    # Check GMLJP2 RectifiedGrid envelope against codestream dimensions
    if gmljp2_found:
        gmljp2_node = gdal.ParseXMLString(gmljp2)
        rg = find_xml_node(gmljp2_node, "gml:RectifiedGrid")
        if rg is None:
            rg = find_xml_node(gmljp2_node, "RectifiedGrid")
        if rg is None:
            error_report.EmitError("GMLJP2", "Cannot find RectifiedGrid in GMLJP2")
        else:
            low = find_xml_node(rg, "low")
            if low is None:
                low = find_xml_node(rg, "gml:low")
            high = find_xml_node(rg, "high")
            if high is None:
                high = find_xml_node(rg, "gml:high")
            if low and high:
                low = get_element_val(low)
                low_x, low_y = low.split(" ")[0:2]
                low_x = int(low_x)
                low_y = int(low_y)

                if low_x != XOsiz:
                    error_report.EmitError(
                        "GMLJP2", "RectifiedGrid.limits.GridEnvelope.low[x] != XOsiz"
                    )
                if low_y != YOsiz:
                    error_report.EmitError(
                        "GMLJP2", "RectifiedGrid.limits.GridEnvelope.low[y] != YOsiz"
                    )

                high = get_element_val(high)
                high_x, high_y = high.split(" ")[0:2]
                high_x = int(high_x)
                high_y = int(high_y)

                if high_x != Xsiz - 1:
                    error_report.EmitError(
                        "GMLJP2",
                        "RectifiedGrid.limits.GridEnvelope.high[x] != Xsiz - 1",
                    )
                if high_y != Ysiz - 1:
                    error_report.EmitError(
                        "GMLJP2",
                        "RectifiedGrid.limits.GridEnvelope.high[y] != Ysiz - 1",
                    )
            else:
                error_report.EmitError(
                    "GMLJP2", "Cannot find low/high node in RectifiedGrid"
                )

    # Check against Orthoimagery document
    if oidoc:
        oidoc_content = open(oidoc).read()
        oidoc_node = gdal.ParseXMLString(oidoc_content)
        if oidoc_node is None:
            error_report.EmitError("GENERAL", "Cannot parse %s" % oidoc)
        else:
            oic = find_xml_node(oidoc_node, "OrthoimageCoverage")
            if oic is None:
                oic = find_xml_node(oidoc_node, "oi:OrthoimageCoverage")
            if oic is None:
                error_report.EmitError(
                    "GENERAL", "Cannot find OrthoimageCoverage in %s" % oidoc
                )
            else:
                # Check RectifiedGrid envelope against codestream dimensions
                rg = find_xml_node(oic, "gml:RectifiedGrid")
                if rg is None:
                    rg = find_xml_node(oic, "RectifiedGrid")
                if rg is None:
                    if expected_gmljp2:
                        error_report.EmitError(
                            "INSPIRE_TG",
                            "Cannot find RectifiedGrid in OrthoImageryCoverage",
                        )
                else:
                    low = find_xml_node(rg, "low")
                    if low is None:
                        low = find_xml_node(rg, "gml:low")
                    high = find_xml_node(rg, "high")
                    if high is None:
                        high = find_xml_node(rg, "gml:high")
                    if low and high:
                        low = get_element_val(low)
                        low_x, low_y = low.split(" ")[0:2]
                        low_x = int(low_x)
                        low_y = int(low_y)

                        if low_x != XOsiz:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "RectifiedGrid.limits.GridEnvelope.low[x](=%d) != XOsiz(=%d)"
                                % (low_x, XOsiz),
                                conformance_class="A.8.6",
                            )
                        if low_y != YOsiz:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "RectifiedGrid.limits.GridEnvelope.low[y](=%d) != YOsiz(=%d)"
                                % (low_y, YOsiz),
                                conformance_class="A.8.6",
                            )

                        high = get_element_val(high)
                        high_x, high_y = high.split(" ")[0:2]
                        high_x = int(high_x)
                        high_y = int(high_y)

                        if high_x != Xsiz - 1:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "RectifiedGrid.limits.GridEnvelope.high[x](=%d) != Xsiz(=%d) - 1"
                                % (high_x, Xsiz),
                                conformance_class="A.8.6",
                            )
                        if high_y != Ysiz - 1:
                            error_report.EmitError(
                                "INSPIRE_TG",
                                "RectifiedGrid.limits.GridEnvelope.high[y](=%d) != Ysiz(=%d) - 1"
                                % (high_y, Ysiz),
                                conformance_class="A.8.6",
                            )
                    else:
                        error_report.EmitError(
                            "INSPIRE_TG", "Cannot find low/high node in RectifiedGrid"
                        )

                    check_oi_rg_consistency(
                        filename, gdal.SerializeXMLTree(rg), error_report
                    )

                rangetype = find_xml_node(oic, "gmlcov:rangeType")
                if rangetype is None:
                    rangetype = find_xml_node(oic, "rangeType")
                if rangetype is None:
                    error_report.EmitError(
                        "INSPIRE_TG",
                        "Cannot find gmlcov:rangeType in OrthoImageryCoverage",
                    )
                else:
                    datarecord = find_xml_node(rangetype, "swe:DataRecord")
                    if datarecord is None:
                        datarecord = find_xml_node(rangetype, "DataRecord")
                    if datarecord is None:
                        error_report.EmitError(
                            "INSPIRE_TG",
                            "Cannot find swe:DataRecord in OrthoImageryCoverage.rangeType",
                        )
                    else:
                        count_fields = 0
                        min_vals = []
                        max_vals = []
                        for child_idx in range(XML_FIRST_CHILD_IDX, len(datarecord)):
                            child = datarecord[child_idx]
                            if child[XML_TYPE_IDX] == gdal.CXT_Element and (
                                child[XML_VALUE_IDX] == "swe:field"
                                or child[XML_VALUE_IDX] == "field"
                            ):
                                count_fields += 1

                                interval = None
                                constraint = find_xml_node(child, "swe:constraint")
                                if constraint is None:
                                    constraint = find_xml_node(child, "constraint")
                                if constraint is not None:
                                    AllowedValues = find_xml_node(
                                        constraint, "swe:AllowedValues"
                                    )
                                    if AllowedValues is None:
                                        AllowedValues = find_xml_node(
                                            constraint, "AllowedValues"
                                        )
                                    if AllowedValues is not None:
                                        interval = find_xml_node(
                                            AllowedValues, "swe:interval"
                                        )
                                        if interval is None:
                                            interval = find_xml_node(
                                                AllowedValues, "interval"
                                            )
                                        if interval is not None:
                                            interval = get_element_val(interval)
                                if interval is None:
                                    error_report.EmitError(
                                        "INSPIRE_TG",
                                        "Cannot find constraint.AllowedValues.interval for field %d"
                                        % count_fields,
                                    )
                                    min_vals.append(None)
                                    max_vals.append(None)
                                else:
                                    min_val, max_val = interval.split(" ")
                                    min_val = int(min_val)
                                    max_val = int(max_val)
                                    min_vals.append(min_val)
                                    max_vals.append(max_val)

                        # Check number of fields regarding number of components
                        if pclr is None:
                            if count_fields != Csiz:
                                error_report.EmitError(
                                    "INSPIRE_TG",
                                    "count(OrthoImageryCoverage.rangeType.field)(=%d) != Csiz(=%d) "
                                    % (count_fields, Csiz),
                                    conformance_class="A.8.6",
                                )
                            else:
                                # Check consistency of each channel bit-depth
                                # with the corresponding rangeType.field.
                                for i in range(Csiz):
                                    if tab_Ssiz[i] >= 128:
                                        tab_Ssiz[i] -= 128
                                        minSsiz = -(2 ** tab_Ssiz[i])
                                        maxSsiz = 2 ** tab_Ssiz[i] - 1
                                    else:
                                        minSsiz = 0
                                        maxSsiz = 2 ** (tab_Ssiz[i] + 1) - 1
                                    if (
                                        min_vals[i] is not None
                                        and max_vals[i] is not None
                                    ):
                                        if min_vals[i] != minSsiz:
                                            error_report.EmitError(
                                                "INSPIRE_TG",
                                                "rangeType.field[%d].min(=%d) != min(Ssiz[%d])(=%d)"
                                                % (i, min_vals[i], i, minSsiz),
                                                conformance_class="A.8.6",
                                            )
                                        if max_vals[i] != maxSsiz:
                                            error_report.EmitError(
                                                "INSPIRE_TG",
                                                "rangeType.field[%d].max(=%d) != max(Ssiz[%d])(=%d)"
                                                % (i, max_vals[i], i, maxSsiz),
                                                conformance_class="A.8.6",
                                            )
                        else:
                            if count_fields != 3:
                                error_report.EmitError(
                                    "INSPIRE_TG",
                                    "count(OrthoImageryCoverage.rangeType.field)(=%d) != 3 (for color table)"
                                    % (count_fields),
                                    conformance_class="A.8.6",
                                )

    # Validate content of COD marker
    cod = find_marker(cs, "COD")
    if not cod:
        error_report.EmitError("GENERAL", "No COD marker found")
    else:
        while True:
            SPcod_transformation = get_field_val(cod, "SPcod_transformation")
            if SPcod_transformation is None:
                error_report.EmitError(
                    "GENERAL",
                    "cod.SPcod_transformation not found (and perhaps other fields)",
                )
                break

            Scod = int_or_none(get_field_val(cod, "Scod"))

            SPcod_NumDecompositions = int(get_field_val(cod, "SPcod_NumDecompositions"))
            if (inspire_tg or Rsiz == 2) and siz:
                if XTsiz <= Xsiz - XOsiz and YTsiz <= Ysiz - YOsiz:
                    max_dim = max(XTsiz, YTsiz)
                else:
                    max_dim = max(Xsiz - XOsiz, Ysiz - YOsiz)
                if max_dim > 128 * 2**SPcod_NumDecompositions:
                    error_report.EmitError(
                        "PROFILE_1",
                        "Not enough decomposition levels = %d (max_dim=%d, 128 * 2**SPcod_NumDecompositions=%d)"
                        % (
                            SPcod_NumDecompositions,
                            max_dim,
                            128 * 2**SPcod_NumDecompositions,
                        ),
                    )

            SPcod_xcb_minus_2 = int(get_field_val(cod, "SPcod_xcb_minus_2"))
            SPcod_ycb_minus_2 = int(get_field_val(cod, "SPcod_ycb_minus_2"))
            if (inspire_tg or Rsiz == 2) and SPcod_xcb_minus_2 > 6 - 2:
                error_report.EmitError(
                    "PROFILE_1",
                    "SPcod_xcb_minus_2 = %d, whereas max allowed for Profile 1 is 4"
                    % SPcod_xcb_minus_2,
                )
            if (inspire_tg or Rsiz == 2) and SPcod_ycb_minus_2 > 6 - 2:
                error_report.EmitError(
                    "PROFILE_1",
                    "SPcod_ycb_minus_2 = %d, whereas max allowed for Profile 1 is 4"
                    % SPcod_ycb_minus_2,
                )
            if SPcod_xcb_minus_2 + SPcod_ycb_minus_2 > 8:
                error_report.EmitError(
                    "GENERAL",
                    "SPcod_xcb_minus_2 + SPcod_ycb_minus_2 = %d, whereas max allowed is 8"
                    % (SPcod_xcb_minus_2 + SPcod_ycb_minus_2),
                )

            for i in range(SPcod_NumDecompositions + 1):
                SPcod_Precincts = get_field_val(cod, "SPcod_Precincts%d" % i)
                if SPcod_Precincts is not None and (Scod & 1) == 0:
                    error_report.EmitWarning(
                        "GENERAL",
                        "User-defined precincts %d found but "
                        "SPcod_transformation did not advertise it" % i,
                    )
                elif SPcod_Precincts is None and (Scod & 1) != 0:
                    error_report.EmitWarning(
                        "GENERAL",
                        "No user-defined precincts %d defined but SPcod_transformation advertized it"
                        % i,
                    )
                elif SPcod_Precincts is None and inspire_tg:
                    error_report.EmitWarning(
                        "INSPIRE_TG",
                        "No user-defined precincts %d defined" % i,
                        recommendation=39,
                    )

            break

    # Check QCD marker
    qcd = find_marker(cs, "QCD")
    if not qcd:
        error_report.EmitError("GENERAL", "No QCD marker found")

    # Check SOT marker
    sot = find_marker(cs, "SOT")
    if not sot:
        error_report.EmitError("GENERAL", "No SOT marker found")

    # Check RGN marker presence
    rgn = find_marker(cs, "RGN")
    if inspire_tg and rgn:
        error_report.EmitError(
            "INSPIRE_TG",
            "RGN marker found, which is not allowed",
            requirement=26,
            conformance_class="A.8.16",
        )

    # Check EOC marker
    eoc = find_marker(cs, "EOC")
    if not eoc:
        error_report.EmitError("GENERAL", "No EOC marker found")

    return error_report


def main(argv=sys.argv):
    i = 1
    filename = None
    oidoc = None
    ogc_schemas_location = None
    inspire_tg = False
    expected_gmljp2 = False
    datatype = "imagery"
    while i < len(argv):
        if argv[i] == "-oidoc":
            if i >= len(argv) - 1:
                return Usage()
            oidoc = argv[i + 1]
            i = i + 1
        elif argv[i] == "-ogc_schemas_location":
            if i >= len(argv) - 1:
                return Usage()
            ogc_schemas_location = argv[i + 1]
            i = i + 1
        elif argv[i] == "-datatype":
            if i >= len(argv) - 1:
                return Usage()
            datatype = argv[i + 1]
            i = i + 1
        elif argv[i] == "-inspire_tg":
            inspire_tg = True
        elif argv[i] == "-expected_gmljp2":
            expected_gmljp2 = True
        elif argv[i][0] == "-":
            return Usage()
        elif filename is None:
            filename = argv[i]
        else:
            return Usage()

        i = i + 1

    if filename is None:
        return Usage()

    if ogc_schemas_location is None:
        try:
            os.stat("SCHEMAS_OPENGIS_NET")
            ogc_schemas_location = "SCHEMAS_OPENGIS_NET"
        except OSError:
            pass

    if ogc_schemas_location is not None:
        try:
            os.stat("%s/xml.xsd" % ogc_schemas_location)
        except OSError:
            try:
                os.stat("%s/SCHEMAS_OPENGIS_NET/xml.xsd" % ogc_schemas_location)
                ogc_schemas_location = "%s/SCHEMAS_OPENGIS_NET" % ogc_schemas_location
            except OSError:
                print(
                    "Cannot find %s/xml.xsd. -ogc_schemas_location value is probably wrong"
                    % ogc_schemas_location
                )
                return 1

    return validate(
        filename, oidoc, inspire_tg, expected_gmljp2, ogc_schemas_location, datatype
    ).error_count


if __name__ == "__main__":
    sys.exit(main(sys.argv))
