#!python
"""
Script to check the compliance of a file with the CF/Radial 1.2 standard
"""

import argparse
import time

import netCDF4
import numpy as np

# variable types
DOUBLE = np.float64
FLOAT = np.float32
BYTE = np.byte
INT = np.int32
SHORT = np.int16
STRING = np.unicode_
CHAR = np.dtype("S1")


class AttributeTable:
    """
    A class represeting a table of required/optional variable attributes.
    """

    def __init__(self, text, section):
        self.text = text
        self.section = section
        self.attributes = {}
        self.required_attrs = []
        self.optional_attrs = []

    def add_attr(self, attr_name, required, _type=None, value=None):
        """Add an attribute to the table."""
        self.attributes[attr_name] = Attribute(_type, value)
        if required:
            self.required_attrs.append(attr_name)
        else:
            self.optional_attrs.append(attr_name)

    def req_attr(self, attr_name, _type=None, value=None):
        """Add a required attribute to the table."""
        self.attributes[attr_name] = Attribute(_type, value)
        self.required_attrs.append(attr_name)

    def opt_attr(self, attr_name, _type=None, value=None):
        """Add an optional attribute to the table."""
        self.attributes[attr_name] = Attribute(_type, value)
        self.optional_attrs.append(attr_name)

    def check_attr(self, attr_name, required, test_var, verb):
        """Check an attribute, log errors or notes if detected."""
        if attr_name not in test_var.ncattrs():
            if required:
                t = f"Required {self.text} '{attr_name}' missing."
                log_error(self.section, t)
                return
            if verb:
                t = f"Optional {self.text} '{attr_name}' missing."
                log_note(self.section, t)
            return

        attr_obj = self.attributes[attr_name]
        attr = getattr(test_var, attr_name)

        # check for incorrect type
        if attr_obj.type_bad(type(attr)):
            tup = (self.text, attr_name, type(attr), attr_obj._type)
            t = "%s '%s' has incorrect type: %s should be %s." % tup
            log_error(self.section, t)

        # check for incorrect value
        if attr_obj.value_bad(attr):
            tup = (self.text, attr_name, attr, attr_obj.value)
            t = "%s '%s' has incorrect value: %s should be %s." % tup
            log_error(self.section, t)

    def check(self, test_var, verb=False):
        """Check all attributes for errror and notes."""
        # check for required attributes
        for attr_name in self.required_attrs:
            self.check_attr(attr_name, True, test_var, verb)
        for attr_name in self.optional_attrs:
            self.check_attr(attr_name, False, test_var, verb)


class Attribute:
    """
    A class for holding and checking netCDF variable attributes.

    Parameters
    ----------
    _type : type or None
        Expected type of the attribute, None for no expected type.
    value : any or None
        Expected value for the attribute, None for no expected value.

    """

    def __init__(self, _type=None, value=None):
        """initialize the object."""
        self._type = _type
        self.value = value

    def type_bad(self, _type):
        """
        Return True if the provided type does not matches the expected type.
        """
        if self._type is None or self._type == _type:
            return False
        return True

    def value_bad(self, value):
        """
        Return True if the provided value does not match the expected value.
        """
        if self.value is None or self.value == value:
            return False
        return True


class VariableTable:
    """
    A class represeting a table of required/optional variables.
    """

    def __init__(self, text, section):
        """Initialize the table."""
        self.text = text
        self.section = section
        self.variables = {}
        self.required_vars = []
        self.optional_vars = []

    def add_var(self, var_name, required, dtype=None, dim=None, units=None):
        """Add an attribute to the table."""
        self.variables[var_name] = Variable(dtype, dim, units)
        if required:
            self.required_vars.append(var_name)
        else:
            self.optional_vars.append(var_name)

    def req_var(self, var_name, dtype=None, dim=None, units=None):
        """Add a required variable to the table."""
        self.variables[var_name] = Variable(dtype, dim, units)
        self.required_vars.append(var_name)

    def opt_var(self, var_name, dtype=None, dim=None, units=None):
        """Add an optional variable to the table."""
        self.variables[var_name] = Variable(dtype, dim, units)
        self.optional_vars.append(var_name)

    def check_var(self, var_name, required, dataset, verb):
        """Check a specific that a variable exists has correct attributes."""
        if var_name not in dataset.variables:
            if required:
                t = f"Required {self.text} '{var_name}' missing."
                log_error(self.section, t)
                return
            if verb:
                t = f"Optional {self.text} '{var_name}' missing."
                log_note(self.section, t)
            return

        # the variable exist, load the variable and variable object
        var_obj = self.variables[var_name]
        var = dataset.variables[var_name]

        # check for incorrect type
        if var_obj.dtype_bad(var.dtype):
            tup = (self.text, var_name, var.dtype, var_obj.dtype)
            t = "%s '%s' has incorrect type: %s should be %s" % tup
            log_error(self.section, t)
        # check for incorrect dim
        if var_obj.dim_bad(var.dimensions):
            tup = (self.text, var_name, var.dimensions, var_obj.dim)
            t = "%s '%s' has incorrect dimensions: %s should be %s" % tup
            log_error(self.section, t)
        # check for bad units
        if "units" not in var.ncattrs() and var_obj.units is not None:
            t = f"{self.text} '{var_name}' is missing a unit attribute."
            log_error(self.section, t)
            return
        if var_obj.units is not None and var_obj.units_bad(var.units):
            tup = (self.text, var_name, var.units, var_obj.units)
            t = "%s '%s' has incorrect units: %s should be %s" % tup
            log_error(self.section, t)

    def check(self, dataset, verb=False):
        """Perform all checks on the variables."""
        # check required variables
        for var_name in self.required_vars:
            self.check_var(var_name, True, dataset, verb)
        # check optional variables
        for var_name in self.optional_vars:
            self.check_var(var_name, False, dataset, verb)
        return


class Variable:
    """
    A class for holding and checking netCDF variables.

    Parameters
    ----------
    dtype : type or None
        Expected dtype of the variable, None for no expected type.
    dim : tuple of str
        Expected dimensions of the variable, None for no expected dimensions.
    units : str
        Expected units attribute of the variable.  None for no expected units.

    """

    def __init__(self, dtype=None, dim=None, units=None):
        """initalize the object."""
        self.dtype = dtype
        self.dim = dim
        self.units = units

    def dtype_bad(self, dtype):
        """True if the provided dtype does not match the expected dtype."""
        if self.dtype is None:
            return False
        elif self.dtype == dtype:
            return False
        else:
            return True

    def dim_bad(self, dim):
        """True if the provided dim does not match the expected dim."""
        if self.dim is None:
            return False
        elif self.dim == dim:
            return False
        else:
            return True

    def units_bad(self, units):
        """True is the provided units does not match the expected units."""
        if self.units is None:
            return False
        elif self.units == units:
            return False
        else:
            return True


def log_error(section, text):
    """
    Log an error, print it to standard out.
    """
    print(f"ERROR: ({section}) {text}")


def log_note(section, text):
    """
    Log a note (unmet optional part of the standard), print to standard out.
    """
    print(f"NOTE: ({section}) {text}")


def check_attribute(section, obj, text, attr_name, valid_choices):
    """Checks an attribute which has a set number of valid values."""
    if attr_name in obj.ncattrs():
        attr = getattr(obj, attr_name)
        if attr not in valid_choices:
            tup = (text, attr_name, attr, " ".join(valid_choices))
            t = "%s '%s' has an invalid value: %s must be one of %s" % tup
            log_error(section, t)
    return


def check_char_variable(section, dataset, text, var_name, valid_options):
    """Check a char variable which has a set number of valid values."""
    if var_name in dataset.variables:
        var = dataset.variables[var_name][:]
        value = var.tostring().strip("\x00").strip()
        if value not in valid_options:
            tup = (text, var_name, value, " ".join(valid_options))
            t = "%s '%s' has an invalid value: %s must be one of %s" % tup
            log_error(section, t)
    return


def check_chararr_variable(section, dataset, text, var_name, valid_options):
    """
    Check a variable array of chars which has a set number of valid values.
    """
    if var_name in dataset.variables:
        for i, chars in enumerate(dataset.variables[var_name]):
            value = chars.tostring().strip("\x00").strip()
            if value not in valid_options:
                tup = (text, var_name, i, value, " ".join(valid_options))
                t = (
                    "%s '%s' has an invalid value in position %i: "
                    "%s must be one of %s" % tup
                )
                log_error(section, t)
    return


def check_valid_time_format(section, dataset, text, var_name):
    """Check that a variable is a valid UTC time formatted string."""
    if var_name in dataset.variables:
        s = str(netCDF4.chartostring(dataset.variables[var_name][:]))
        try:
            time.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
        except:
            tup = (text, var_name, s, "yyyy-mm-ddThh:mm:ssZ")
            t = "%s '%s' has an invalid format: %s should be %s" % (tup)
            log_error(section, t)


def check_metagroup(section, dataset, meta_group_name, valid_meta_group_vars):
    """Check the meta_group attributes of a meta group."""
    # check that all variable present have a meta_group attribute and it is
    # set correctly.
    for var_name in valid_meta_group_vars:
        if var_name in dataset.variables:
            var = dataset.variables[var_name]
            if "meta_group" not in var.ncattrs():
                tup = (meta_group_name, var_name)
                text = "%s %s does not have a `meta_group` attribute" % (tup)
                log_error(section, text)
            else:
                if var.meta_group != meta_group_name:
                    tup = (meta_group_name, var_name, var.meta_group, meta_group_name)
                    text = (
                        "%s %s 'meta_group' attribute has incorrect "
                        "value: %s should be %s" % (tup)
                    )
                    log_error(section, text)

    # check if other variables have meta_group attribute set to the
    # current meta_group.
    # XXX this might not be considered an error but rather a note
    for var_name in find_all_meta_group_vars(dataset, meta_group_name):
        if var_name not in valid_meta_group_vars:
            text = (
                "variable {} should not have its meta_group attribute "
                "set to '{}'".format(var_name, meta_group_name)
            )
            log_error(section, text)


def find_all_meta_group_vars(dataset, meta_group_name):
    """
    Return a list of all variables which are in a given meta_group.
    """
    return [
        k
        for k, v in dataset.variables.items()
        if "meta_group" in v.ncattrs() and v.meta_group == meta_group_name
    ]


def check_cfradial_compliance(dataset, verb=False):
    """
    Check a netcdf dataset for CF/Radial compliance.

    Parameters
    ----------
    dataset : Dataset
        NetCDF Dataset to check against CF/Radial version 1.2 standard
    verb : bool
        True to turn on verbose messages (Notes on missing optional
        variables/attributes).  False (default) to suppress

    """
    ##########################
    # 3 Convention hierarchy #
    ##########################
    if "Conventions" in dataset.ncattrs():
        if "CF/Radial" not in dataset.Conventions:
            text = "Convention attribute does not contain 'CF/Radial'"
            log_error("3", text)

    #################################
    # 4: CF/Radial base conventions #
    #################################

    # 4.1 Global attributes
    global_attrs = AttributeTable("global attribute", "4.1")
    global_attrs.req_attr("Conventions", STRING)
    global_attrs.opt_attr("version", STRING)
    global_attrs.req_attr("title", STRING)
    global_attrs.req_attr("institution", STRING)
    global_attrs.req_attr("references", STRING)
    global_attrs.req_attr("source", STRING)
    global_attrs.req_attr("history", STRING)
    global_attrs.req_attr("comment", STRING)
    global_attrs.req_attr("instrument_name", STRING)
    global_attrs.opt_attr("site_name", STRING)
    global_attrs.opt_attr("scan_name", STRING)
    global_attrs.opt_attr("scan_id", INT)
    global_attrs.opt_attr("platform_is_mobile", STRING)
    global_attrs.opt_attr("n_gates_vary", STRING)
    global_attrs.check(dataset, verb)

    check_attribute(
        "4.1", dataset, "global attribute", "platform_is_mobile", ["true", "false"]
    )

    check_attribute(
        "4.1", dataset, "global attribute", "n_gates_vary", ["true", "false"]
    )

    n_gates_vary_flag = False
    if "n_gates_vary" in dataset.ncattrs() and dataset.n_gates_vary == "true":
        n_gates_vary_flag = True

    mobile_platform = False
    if "platform_is_mobile" in dataset.ncattrs():
        if dataset.platform_is_mobile == "true":
            mobile_platform = True

    # 4.2 Dimensions
    section = "4.2"

    if "time" not in dataset.dimensions:
        log_error(section, "Required dimension 'time' missing.")
    if "range" not in dataset.dimensions:
        log_error(section, "Required dimension 'range' missing.")
    if "sweep" not in dataset.dimensions:
        log_error(section, "Required dimension 'sweep' missing.")
    if n_gates_vary_flag and "n_points" not in dataset.dimensions:
        text = "Dimension 'n_points' is required and missing."
        log_error(section, text)
    if n_gates_vary_flag is False and "n_points" in dataset.dimensions:
        text = "Dimension 'n_points must not be included."
        log_error(section, text)
    if verb:
        if "frequency" not in dataset.dimensions:
            log_note(section, "Optional dimension 'frequency' missing")

    # 4.3 Global variables
    global_vars = VariableTable("global variable", "4.3")
    global_vars.req_var("volume_number", INT)
    global_vars.opt_var("platform_type", CHAR)
    global_vars.opt_var("instrument_type", CHAR)
    global_vars.opt_var("primary_axis", CHAR)
    global_vars.req_var("time_coverage_start", CHAR)
    global_vars.req_var("time_coverage_end", CHAR)
    global_vars.opt_var("time_reference", CHAR)
    global_vars.check(dataset, verb)

    valid_platform_types = [
        "fixed",
        "vehicle",
        "ship",
        "aircraft",
        "aircraft_fore",
        "aircraft_aft",
        "aircraft_tail",
        "aircraft_belly",
        "aircraft_roof",
        "aircraft_nose",
        "satellite_orbit",
        "satellite_geostat",
    ]
    check_char_variable(
        "4.3", dataset, "global variable", "platform_type", valid_platform_types
    )

    check_char_variable(
        "4.3", dataset, "global variable", "instrument_type", ["radar", "lidar"]
    )

    check_char_variable(
        "4.3",
        dataset,
        "global variable",
        "primary_axis",
        ["axis_z", "axis_y", "axis_x"],
    )

    check_valid_time_format("4.3", dataset, "global_variable", "time_coverage_start")

    check_valid_time_format("4.3", dataset, "global_variable", "time_coverage_end")

    check_valid_time_format("4.3", dataset, "global_variable", "time_reference")

    # 4.4 Coordinate variables
    coordinate_vars = VariableTable("coordinate variable", "4.4")
    coordinate_vars.req_var("time", DOUBLE, ("time",))
    coordinate_vars.req_var("range", FLOAT, ("range",), "meters")
    coordinate_vars.check(dataset, verb)

    # 4.4.1 Attributes for time coordinate variable
    time_attrs = AttributeTable("time attribute", "4.4.1")
    time_attrs.req_attr("standard_name", STRING, "time")
    time_attrs.req_attr("long_name", STRING, "time_in_seconds_since_volume_start")
    time_attrs.req_attr("units", STRING)
    if "time" in dataset.variables:
        time_attrs.check(dataset.variables["time"], verb)

    # time unit must be 'seconds since yyyy-mm-ddThh:mm:ssZ'
    if "time" in dataset.variables:
        if "units" in dataset.variables["time"].ncattrs():
            time_units = dataset.variables["time"].units

            # begins with 'seconds since '
            if not time_units.startswith("seconds since "):
                t = (
                    "time attribute 'units' has an invalid format: "
                    + str(time_units)
                    + "should begin with 'seconds since '"
                )
                log_error("4.4.1", t)

            # end with yyy-mm-ddThh:mm:ssZ
            time_str = time_units[-20:]
            try:
                time.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")
            except:
                tup = (time_str, "yyyy-mm-ddThh:mm:ssZ")
                t = (
                    "'time' attribute 'units' has an invalid formatted time"
                    "value: %s should be %s" % (tup)
                )
                log_error("4.4.1", t)

            # and must match time_reference or time_coverage_start
            if "time_reference" in dataset.variables:
                v = dataset.variables["time_reference"]
                s = str(netCDF4.chartostring(v[:]))
                if s != time_str:
                    tup = (time_str, s)
                    t = (
                        "time attribute 'units' does not match time in "
                        "time_reference variable: %s verses %s" % (tup)
                    )
                    log_error("4.4.1", t)
            elif "time_coverage_start" in dataset.variables:
                v = dataset.variables["time_coverage_start"]
                s = str(netCDF4.chartostring(v[:]))
                if s != time_str:
                    tup = (time_str, s)
                    t = (
                        "time attribute 'units' does not match time in "
                        "time_coverage_start variable: %s verses %s" % (tup)
                    )
                    log_error("4.4.1", t)

    # 4.4.2 Attribute for range coordinate variables
    range_attrs = AttributeTable("range attribute", "4.4.2")
    range_attrs.req_attr("standard_name", STRING, "projection_range_coordinate")
    range_attrs.req_attr("long_name", STRING, "range_to_measurement_volume")
    range_attrs.req_attr("units", STRING, "meters")
    range_attrs.req_attr("spacing_is_constant", STRING)
    range_attrs.req_attr("meters_to_center_of_first_gate", FLOAT)
    range_attrs.opt_attr("meters_between_gates", FLOAT)
    range_attrs.req_attr("axis", STRING, "radial_range_coordinate")
    if "range" in dataset.variables:
        range_var = dataset.variables["range"]
        range_attrs.check(range_var, verb)
        check_attribute(
            "4.4.2",
            range_var,
            "range attribute",
            "spacing_is_constant",
            ["true", "false"],
        )

    # 4.5 Ray dimension variables
    raydim_vars = VariableTable("ray dimension variable", "4.5")
    raydim_vars.req_var("ray_n_gates", INT, ("time",))
    raydim_vars.req_var("ray_start_index", INT, ("time",))

    if n_gates_vary_flag:
        raydim_vars.check(dataset)
    else:
        if "ray_n_gates" in dataset.variables:
            t = "ray dimension variable 'ray_n_gates' must be exist."
            log_error("4.5", t)
        if "ray_start_index" in dataset.variables:
            t = "ray dimension variable 'ray_start_index' must be exist."
            log_error("4.5", t)

    # 4.6 Location variables
    dim = ()
    if "platform_is_mobile" in dataset.ncattrs():
        if dataset.platform_is_mobile == "true":
            dim = ("time",)
    location_vars = VariableTable("location variable", "4.6")
    location_vars.req_var("latitude", DOUBLE, dim, "degrees_north")
    location_vars.req_var("longitude", DOUBLE, dim, "degrees_east")
    location_vars.req_var("altitude", DOUBLE, dim, "meters")
    location_vars.opt_var("altitude_agl", DOUBLE, dim, "meters")
    location_vars.check(dataset, verb)

    # 4.7 Sweep variables
    sweep_vars = VariableTable("sweep variable", "4.7")
    sweep_vars.req_var("sweep_number", INT, ("sweep",))
    sweep_vars.req_var("sweep_mode", CHAR)
    sweep_vars.req_var("fixed_angle", FLOAT, ("sweep",), "degrees")
    sweep_vars.req_var("sweep_start_ray_index", INT, ("sweep",))
    sweep_vars.req_var("sweep_end_ray_index", INT, ("sweep",))
    sweep_vars.opt_var("target_scan_rate", FLOAT, ("sweep",), "degrees_per_second")
    sweep_vars.check(dataset, verb)

    valid_sweep_modes = [
        "sector",
        "coplane",
        "rhi",
        "vertical_pointing",
        "idle",
        "azimuth_surveillance",
        "elevation_surveillance",
        "sunscan",
        "pointing",
        "manual_ppi",
        "manual_rhi",
    ]
    check_chararr_variable(
        "4.7", dataset, "sweep variable", "sweep_mode", valid_sweep_modes
    )

    # 4.8 Sensor pointing variables
    sensor_vars = VariableTable("sensor pointing variable", "4.8")
    sensor_vars.req_var("azimuth", FLOAT, ("time",), "degrees")
    sensor_vars.req_var("elevation", FLOAT, ("time",), "degrees")
    sensor_vars.opt_var("scan_rate", FLOAT, ("time",), "degrees_per_second")
    sensor_vars.opt_var("antenna_transition", BYTE, ("time",))
    sensor_vars.check(dataset, verb)

    # 4.8.1 Attributes for azimuth(time) variable
    azimuth_attrs = AttributeTable("azimuth attribute", "4.8.1")
    azimuth_attrs.req_attr("standard_name", STRING, "beam_azimuth_angle")
    azimuth_attrs.req_attr("long_name", STRING, "azimuth_angle_from_true_north")
    azimuth_attrs.req_attr("units", STRING, "degrees")
    azimuth_attrs.req_attr("axis", STRING, "radial_azimuth_coordinate")
    if "azimuth" in dataset.variables:
        azimuth_attrs.check(dataset.variables["azimuth"], verb)

    # 4.8.2 Attributes for elevation(time) variable
    elev_attrs = AttributeTable("elevation attribute", "4.8.2")
    elev_attrs.req_attr("standard_name", STRING, "beam_elevation_angle")
    elev_attrs.req_attr("long_name", STRING, "elevation_angle_from_horizontal_plane")
    elev_attrs.req_attr("units", STRING, "degrees")
    elev_attrs.req_attr("axis", STRING, "radial_elevation_coordinate")
    if "elevation" in dataset.variables:
        elev_attrs.check(dataset.variables["elevation"], verb)

    # 4.9 Moving platform geo-reference variables
    georef_vars = VariableTable("moving platform geo-reference variable", "4.9")
    georef_vars.req_var("heading", FLOAT, ("time",), "degrees")
    georef_vars.req_var("roll", FLOAT, ("time",), "degrees")
    georef_vars.req_var("pitch", FLOAT, ("time",), "degrees")
    georef_vars.req_var("drift", FLOAT, ("time",), "degrees")
    georef_vars.req_var("rotation", FLOAT, ("time",), "degrees")
    georef_vars.req_var("tilt", FLOAT, ("time",), "degrees")
    if mobile_platform:
        georef_vars.check(dataset, verb)
    else:  # fixed platfrom
        for v in ["heading", "roll", "pitch", "drift", "rotation", "tilt"]:
            if v in dataset.variables:
                t = "variable '%s' must be omitted for fixed platforms" % (v)
                log_error("4.9", t)

    # 4.10 Moments field data variables

    # assume all variables with dimensions ('time', 'range') are field data
    # XXX a better way to do this might be to check against section 6.2
    for var_name, var in dataset.variables.items():
        if var.dimensions == ("time", "range"):
            # check the data type
            if var.dtype not in [BYTE, SHORT, INT, FLOAT, DOUBLE]:
                tup = (var_name, var.dtype)
                t = "field variable '%s' has invalid type: %s" % (tup)
                log_error("4.10", t)

            # check attributes
            if mobile_platform:
                coordinates_value = (
                    "elevation azimuth range heading roll " "pitch rotation tilt"
                )
            else:
                coordinates_value = "elevation azimuth range"

            # TODO check standard_name, against variable name
            # TODO check units correct for given standard_name
            text = "field variable %s" % var_name
            field_attrs = AttributeTable(text, "4.10")
            field_attrs.opt_attr("long_name", STRING)
            field_attrs.req_attr("standard_name", STRING)
            field_attrs.req_attr("units", STRING)
            field_attrs.req_attr("_FillValue")
            if var.dtype in [BYTE, SHORT, INT]:
                field_attrs.req_attr("scale_factor", FLOAT)
                field_attrs.req_attr("add_offset", FLOAT)
            field_attrs.req_attr("coordinates", STRING, coordinates_value)
            field_attrs.check(var, verb)

    #####################
    # 5 Sub-conventions #
    #####################

    # 5.1 The instrument_parameters sub-convention
    ip_vars = VariableTable("instrument_parameters variable", "5.1")
    ip_vars.opt_var("frequency", FLOAT, ("frequency",), "s-1")
    ip_vars.opt_var("follow_mode", CHAR)
    ip_vars.opt_var("pulse_width", FLOAT, ("time",), "seconds")
    ip_vars.opt_var("prt_mode", CHAR)
    ip_vars.opt_var("prt", FLOAT, ("time",), "seconds")
    ip_vars.opt_var("prt_ratio", FLOAT, ("time",))
    ip_vars.opt_var("polarization_mode", CHAR)
    ip_vars.opt_var("nyquist_velocity", FLOAT, ("time",), "meters_per_second")
    ip_vars.opt_var("unambiguous_range", FLOAT, ("time",), "meters")
    ip_vars.opt_var("n_samples", INT, ("time",))
    ip_vars.opt_var("sampling_ratio", FLOAT, ("time"))
    ip_vars.check(dataset, verb)

    # first dimension should be sweep for _modes variables
    for v in ["follow_mode", "prt_mode", "polarization_mode"]:
        if v in dataset.variables:
            dim_0 = dataset.variables[v].dimensions[0]
            if dim_0 != "sweep":
                text = (
                    "instrument_parameters {} must have a first "
                    "dimension of sweep, not {}".format(v, dim_0)
                )
                log_error("5.1", text)

    # check valid options for _modes variables
    valid_follow_modes = ["none", "sun", "vehicle", "aircraft", "target", "manual"]
    check_chararr_variable(
        "5.1", dataset, "instrument_parameters", "follow_mode", valid_follow_modes
    )

    valid_prt_modes = ["fixed", "staggered", "dual"]
    check_chararr_variable(
        "5.1", dataset, "instrument_parameters", "prt_mode", valid_prt_modes
    )

    valid_polarization_modes = [
        "horizontal",
        "vertical",
        "hv_alt",
        "hv_sim",
        "circular",
    ]
    check_chararr_variable(
        "5.1",
        dataset,
        "instrument_parameters",
        "polarization_mode",
        valid_polarization_modes,
    )

    # check that meta_group attribute is correctly set.
    valid_ip_vars = [
        "frequency",
        "follow_mode",
        "pulse_width",
        "prt_mode",
        "prt",
        "prt_ratio",
        "polarization_mode",
        "nyquist_velocity",
        "unambiguous_range",
        "n_samples",
        "sampling_ratio",
    ]
    check_metagroup("5.1", dataset, "instrument_parameters", valid_ip_vars)

    # 5.2 The radar_parameters sub-convention
    rp_vars = VariableTable("radar_parameters", "5.2")
    rp_vars.opt_var("radar_antenna_gain_h", FLOAT, (), "dB")
    rp_vars.opt_var("radar_antenna_gain_v", FLOAT, (), "dB")
    rp_vars.opt_var("radar_beam_width_h", FLOAT, (), "degrees")
    rp_vars.opt_var("radar_beam_width_v", FLOAT, (), "degrees")
    rp_vars.opt_var("radar_receiver_bandwidth", FLOAT, (), "s-1")
    rp_vars.opt_var("radar_measured_transmit_power_h", FLOAT, ("time",), "dBm")
    rp_vars.opt_var("radar_measured_transmit_power_b", FLOAT, ("time",), "dBm")
    rp_vars.check(dataset, verb)

    valid_rp_vars = [
        "radar_antenna_gain_h",
        "radar_antenna_gain_v",
        "radar_beam_width_h",
        "radar_beam_width_v",
        "radar_receiver_bandwidth",
        "radar_measured_transmit_power_h",
        "radar_measured_transmit_power_b",
    ]
    check_metagroup("5.2", dataset, "radar_parameters", valid_rp_vars)

    # 5.3 The lidar_parameter sub-convention
    lp_vars = VariableTable("lidar_parameters", "5.3")
    lp_vars.opt_var("lidar_beam_divergence", FLOAT, (), "milliradians")
    lp_vars.opt_var("lidar_field_of_view", FLOAT, (), "milliradians")
    lp_vars.opt_var("lidar_aperature_diameter", FLOAT, (), "cm")
    lp_vars.opt_var("lidar_aperture_efficiency", FLOAT, (), "percent")
    lp_vars.opt_var("lidar_peak_power", FLOAT, (), "watts")
    lp_vars.opt_var("lidar_pulse_energy", FLOAT, (), "joules")
    lp_vars.check(dataset, verb)

    valid_lp_vars = [
        "lidar_beam_divergence",
        "lidar_field_of_view",
        "lidar_aperature_diameter",
        "lidar_aperture_efficiency" "lidar_peak_power",
        "lidar_pulse_energy",
    ]
    check_metagroup("5.3", dataset, "lidar_parameters", valid_lp_vars)

    # 5.4 The radar_calibration sub-convention
    valid_rc_vars = [
        "r_calib_index",
        "r_calib_time",
        "r_calib_pulse_width",
        "r_calib_ant_gain_h",
        "r_calib_ant_gain_v",
        "r_calib_xmit_power_h",
        "r_calib_xmit_power_v",
        "r_calib_two_way_waveguide_loss_h",
        "r_calib_two_way_waveguide_loss_v",
        "r_calib_two_way_radome_loss_h",
        "r_calib_two_way_radome_loss_v",
        "r_calib_receiver_mismatch_loss",
        "r_calib_radar_constant_h",
        "r_calib_radar_constant_v",
        "r_calib_noise_hc",
        "r_calib_noise_vc",
        "r_calib_noise_hx",
        "r_calib_noise_vx",
        "r_calib_receiver_gain_hc",
        "r_calib_receiver_gain_vc",
        "r_calib_receiver_gain_hx",
        "r_calib_receiver_gain_vx",
        "r_calib_base_dbz_1km_hc",
        "r_calib_base_dbz_1km_vc",
        "r_calib_base_dbz_1km_hx",
        "r_calib_base_dbz_1km_vx",
        "r_calib_sun_power_hc",
        "r_calib_sun_power_vc",
        "r_calib_sun_power_hx",
        "r_calib_sun_power_vx",
        "r_calib_noise_source_power_h",
        "r_calib_noise_source_power_v",
        "r_calib_power_measure_loss_h",
        "r_calib_power_measure_loss_v",
        "r_calib_coupler_forward_loss_h",
        "r_calib_coupler_forward_loss_v",
        "r_calib_zdr_correction",
        "r_calib_ldr_correction_h",
        "r_calib_ldr_correction_v",
        "r_calib_system_phidp",
        "r_calib_test_power_h",
        "r_calib_test_power_v",
        "r_calib_receiver_slope_hc",
        "r_calib_receiver_slope_vc",
        "r_calib_receiver_slope_hx",
        "r_calib_receiver_slope_vx",
    ]
    check_metagroup("5.4", dataset, "radar_calibration", valid_rc_vars)

    # 5.4.1 Dimensions

    # 5.4.2 Variables
    d = ("r_calib",)
    rc_vars = VariableTable("radar_calibration", "5.4.2")
    rc_vars.opt_var("r_calib_index", BYTE, ("time",))
    rc_vars.opt_var("r_calib_time", CHAR)
    rc_vars.opt_var("r_calib_pulse_width", FLOAT, d, "seconds")
    rc_vars.opt_var("r_calib_ant_gain_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_ant_gain_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_xmit_power_h", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_xmit_power_v", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_two_way_waveguide_loss_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_two_way_waveguide_loss_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_two_way_radome_loss_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_two_way_radome_loss_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_receiver_mismatch_loss", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_radar_constant_h", FLOAT, d, "dB")  # m/mW
    rc_vars.opt_var("r_calib_radar_constant_v", FLOAT, d, "dB")  # m/mW
    rc_vars.opt_var("r_calib_noise_hc", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_noise_vc", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_noise_hx", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_noise_vx", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_receiver_gain_hc", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_receiver_gain_vc", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_receiver_gain_hx", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_receiver_gain_vx", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_base_dbz_1km_hc", FLOAT, d, "dBZ")
    rc_vars.opt_var("r_calib_base_dbz_1km_vc", FLOAT, d, "dBZ")
    rc_vars.opt_var("r_calib_base_dbz_1km_hx", FLOAT, d, "dBZ")
    rc_vars.opt_var("r_calib_base_dbz_1km_vx", FLOAT, d, "dBZ")
    rc_vars.opt_var("r_calib_sun_power_hc", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_sun_power_vc", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_sun_power_hx", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_sun_power_vx", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_noise_source_power_h", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_noise_source_power_v", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_power_measure_loss_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_power_measure_loss_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_coupler_forward_loss_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_coupler_forward_loss_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_zdr_correction", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_ldr_correction_h", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_ldr_correction_v", FLOAT, d, "dB")
    rc_vars.opt_var("r_calib_system_phidp", FLOAT, d, "degrees")
    rc_vars.opt_var("r_calib_test_power_h", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_test_power_v", FLOAT, d, "dBm")
    rc_vars.opt_var("r_calib_receiver_slope_hc", FLOAT, d)
    rc_vars.opt_var("r_calib_receiver_slope_vc", FLOAT, d)
    rc_vars.opt_var("r_calib_receiver_slope_hx", FLOAT, d)
    rc_vars.opt_var("r_calib_receiver_slope_vx", FLOAT, d)
    rc_vars.check(dataset, verb)

    if "r_calib_time" in dataset.variables:
        r_calib_time = dataset.variables["r_calib_time"]
        dim_0 = r_calib_time.dimensions[0]
        if dim_0 != "r_calib":
            text = (
                "radar_calibration r_calib_time must have first "
                "dimension of 'r_calib' not '%s'" % (dim_0)
            )
            log_error("5.4.2", text)
        else:
            for i, time_arr in enumerate(r_calib_time):
                s = time_arr.tostring().strip("\x00").strip()
                try:
                    time.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
                except:
                    tup = (i, s, "yyyy-mm-ddThh:mm:ssZ")
                    t = (
                        "radar_calibration r_calib_time has an invalid time "
                        "format in position %i: %s must be %s" % (tup)
                    )
                    log_error("5.4.2", t)

    # 5.5 The lidar_calibration sub-convention
    # Not yet defined in the standard

    # 5.6 The platform_velocity sub-convention
    d = ("time",)
    pv_vars = VariableTable("platform_velocity", "5.6")
    pv_vars.req_var("eastward_velocity", FLOAT, d, "meters_per_second")
    pv_vars.req_var("northward_velocity", FLOAT, d, "meters_per_second")
    pv_vars.req_var("vertical_velocity", FLOAT, d, "meters_per_second")
    pv_vars.req_var("eastward_wind", FLOAT, d, "meters_per_second")
    pv_vars.req_var("northward_wind", FLOAT, d, "meters_per_second")
    pv_vars.req_var("vertical_wind", FLOAT, d, "meters_per_second")
    pv_vars.req_var("heading_rate", FLOAT, d, "degrees_per_second")
    pv_vars.req_var("roll_rate", FLOAT, d, "degrees_per_second")
    pv_vars.req_var("pitch_rate", FLOAT, d, "degrees_per_second")

    valid_pv_vars = [
        "eastward_velocity",
        "northward_velocity",
        "vertical_velocity",
        "eastward_wind",
        "northward_wind",
        "vertical_wind",
        "heading_rate",
        "roll_rate",
        "pitch_rate",
    ]

    if mobile_platform:
        pv_vars.check(dataset, verb)
        check_metagroup("5.6", dataset, "platform_velocity", valid_pv_vars)
    else:
        for var_name in valid_pv_vars:
            if var_name in dataset.variables:
                t = "variable %s should not exist as the platform is" "stationary" % (
                    var_name
                )
                log_error("5.6", t)

    # 5.7 The geometry_correction sub-convention
    gc_vars = VariableTable("geometry_correction", "5.7")
    gc_vars.opt_var("azimuth_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("elevation_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("range_correction", FLOAT, (), "meters")
    gc_vars.opt_var("longitude_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("latitude_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("pressure_altitude_correction", FLOAT, (), "meters")
    gc_vars.opt_var("radar_altitude_correction", FLOAT, (), "meters")
    gc_vars.opt_var("eastward_ground_speed_correction", FLOAT, (), "meter_per_second")
    gc_vars.opt_var("northward_ground_speed_correction", FLOAT, (), "meter_per_second")
    gc_vars.opt_var("vertical_velocity_correction", FLOAT, (), "meter_per_second")
    gc_vars.opt_var("heading_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("roll_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("pitch_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("drift_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("rotation_correction", FLOAT, (), "degrees")
    gc_vars.opt_var("tilt_correction", FLOAT, (), "degrees")
    gc_vars.check(dataset, verb)

    valid_gc_vars = [
        "azimuth_correction",
        "elevation_correction",
        "range_correction",
        "longitude_correction",
        "latitude_correction",
        "pressure_altitude_correction",
        "radar_altitude_correction",
        "eastward_ground_speed_correction",
        "northward_ground_speed_correction",
        "vertical_velocity_correction",
        "heading_correction",
        "roll_correction",
        "pitch_correction",
        "drift_correction",
        "rotation_correction",
        "tilt_correction",
    ]
    check_metagroup("5.7", dataset, "geometry_correction", valid_gc_vars)

    ####################
    # 6 Standard Names #
    ####################

    # 6.1 Proposed standard names for metadata variables

    # 6.2 Standard names for moments variables
    return


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Check a file for CF/Radial version 1.2 compliance."
    )
    parser.add_argument("filename", type=str, help="netcdf file to check")
    parser.add_argument(
        "--verb",
        "-v",
        dest="verb",
        action="store_true",
        default=False,
        help="turn on verbose messages",
    )
    args = parser.parse_args()

    dataset = netCDF4.Dataset(args.filename, "r")
    check_cfradial_compliance(dataset, args.verb)
