import os
import sys
import json
import glob
import re
from collections import OrderedDict

import grass.script as gs

# Semantic label can have any form. Explanatory metadata can be stored
# separately. It is suggested to follow some standard e.g. remote
# sensing band names should be STAC common names, see
# https://stacspec.org/
# https://github.com/radiantearth/stac-spec/blob/master/extensions/eo/README.md#band-object


class SemanticLabelReaderError(Exception):
    pass


class SemanticLabelReader:
    """Semantic label reader"""

    def __init__(self):
        self._json_files = glob.glob(
            os.path.join(os.environ["GISBASE"], "etc", "i.band.library", "*.json")
        )
        if not self._json_files:
            raise SemanticLabelReaderError("No semantic label definitions found")

        self._read_config()

    def _read_config(self):
        """Read configuration"""
        self.config = dict()
        for json_file in self._json_files:
            try:
                with open(json_file) as fd:
                    config = json.load(fd, object_pairs_hook=OrderedDict)
            except json.decoder.JSONDecodeError as e:
                raise SemanticLabelReaderError(
                    "Unable to parse '{}': {}".format(json_file, e)
                )

            # check if configuration is valid
            self._check_config(config)

            self.config[os.path.basename(json_file)] = config

    @staticmethod
    def _check_config(config):
        """Check if config is valid

        :todo: check shortcut uniqueness

        :param dict config: configuration to be validated
        """
        for items in config.values():
            for item in ("shortcut", "bands"):
                if item not in items.keys():
                    raise SemanticLabelReaderError(
                        "Invalid band definition: <{}> is missing".format(item)
                    )
            if len(items["bands"]) < 1:
                raise SemanticLabelReaderError(
                    "Invalid band definition: no bands defined"
                )

    @staticmethod
    def _print_label_extended(label, item):
        """Print label specific metadata

        :param str label: label identifier
        :param str item: items to be printed out
        """

        def print_kv(k, v, indent):
            if isinstance(v, OrderedDict):
                print("{}{}:".format(" " * indent * 2, k))
                for ki, vi in v.items():
                    print_kv(ki, vi, indent * 2)
            else:
                print("{}{}: {}".format(" " * indent * 2, k, v))

        indent = 4
        print("{}label: {}".format(" " * indent, label))
        for k, v in item[label].items():
            print_kv(k, v, indent)

    def _print_label(self, semantic_label=None, tag=None):
        sys.stdout.write(semantic_label)
        if tag:
            sys.stdout.write(" {}".format(tag))
        sys.stdout.write(os.linesep)

    def print_info(self, shortcut=None, band=None, semantic_label=None, extended=False):
        """Prints semantic label information to stdout.

        Can be filtered by semantic label identifier.

        :param str shortcut: shortcut to filter (eg. S2) or None
        :param str band: band (eg. 1) or None
        :param str semantic_label: semantic_label filter (eg. S2_8A) or None
        :param bool extended: print also extended metadata
        """
        if semantic_label:
            try:
                shortcut, band = semantic_label.split("_")
            except ValueError:
                shortcut = semantic_label
                band = None
        found = False
        for root in self.config.values():
            for item in root.values():
                try:
                    if shortcut and re.match(shortcut, item["shortcut"]) is None:
                        continue
                except re.error as e:
                    raise SemanticLabelReaderError("Invalid pattern: {}".format(e))

                found = True
                if band and band not in item["bands"]:
                    raise SemanticLabelReaderError(
                        "Band <{}> not found in <{}>".format(band, shortcut)
                    )

                # print generic information
                if extended:
                    for subitem in item.keys():
                        if subitem == "bands":
                            # bands item is processed bellow
                            continue
                        print("{}: {}".format(subitem, item[subitem]))

                    # print detailed band information
                    if band:
                        self._print_label_extended(band, item["bands"])
                    else:
                        for iband in item["bands"]:
                            self._print_label_extended(iband, item["bands"])
                else:
                    # basic information only
                    if band:
                        self._print_label(
                            semantic_label=item["shortcut"],
                            tag=item["bands"][band].get("tag"),
                        )
                    else:
                        for iband in item["bands"]:
                            self._print_label(
                                semantic_label=item["shortcut"],
                                tag=item["bands"][iband].get("tag"),
                            )

        # print warning when defined shortcut not found
        if not found:
            gs.warning(
                "Metadata for semantic label <{}> not found".format(semantic_label)
            )

    def find_file(self, semantic_label):
        """Find file by semantic label.

        Match is case-insensitive.

        :param str semantic_label: semantic label identifier to search for (eg. S2_1)

        :return str: file basename if found or None
        """
        try:
            shortcut, band = semantic_label.split("_")
        except ValueError:
            # raise SemanticLabelReaderError("Invalid band identifier <{}>".format(
            #    semantic_label
            # ))
            shortcut = None

        for filename, config in self.config.items():
            for root in config.keys():
                if (
                    shortcut
                    and config[root]["shortcut"].upper() == shortcut.upper()
                    and band.upper()
                    in map(lambda x: x.upper(), config[root]["bands"].keys())
                ):
                    return filename

        return None

    def get_bands(self):
        """Get list of band identifiers.

        :return list: list of valid band identifiers
        """
        bands = []
        for root in self.config.values():
            for item in root.values():
                for band in item["bands"]:
                    bands.append("{}_{}".format(item["shortcut"], band))
        return bands
