import re
import os


SEPARATE = 1
GROUPED_BY_CONTROL_FILE_1 = 2
GROUPED_BY_CONTROL_FILE_2 = 3  # will include trd, tef as separate control file
GROUPED_BY_TCF = 4
GROUPED_TEMP = 5


class FatalConvertException(Exception):
    """Exception that causes tool to exit."""

    pass


class MinorConvertException(Exception):
    """Exception that will allow tool to continue but notify that an error has occurred at the end of script."""

    pass


class ConvertSettings:
    """Class for handling the conversion settings."""

    def __init__(self, *args, convert_settings=None):
        from helpers.gis import GIS_GPKG, GRID_TIF

        if convert_settings:
            self.errors = convert_settings.errors
            self.tcf = convert_settings.tcf
            self.verbose = convert_settings.verbose
            self.no_ext = convert_settings.no_ext
            self.output_profile = convert_settings.output_profile
            self.written_projection = convert_settings.written_projection
            self.spatial_database = convert_settings.spatial_database
            self.read_spatial_database = convert_settings.read_spatial_database
            self.read_spatial_database_tcf = convert_settings.read_spatial_database_tcf
            self.model_name = convert_settings.model_name
            self.output_gis_format = convert_settings.output_gis_format
            self.output_grid_format = convert_settings.output_grid_format
            self.control_file = convert_settings.control_file
            self.control_file_out = convert_settings.control_file_out
            self.written_spatial_database_header = convert_settings.written_spatial_database_header
            self.projection_wkt = convert_settings.projection_wkt
            self.root_folder = convert_settings.root_folder
            self.output_folder = convert_settings.output_folder
            self.wildcards = convert_settings.wildcards
            self.map_output_grids = convert_settings.map_output_grids
            self.written_tif_projection = convert_settings.written_tif_projection
            self.tif_projection_path = convert_settings.tif_projection_path
            self.grid_format_command_exists = convert_settings.grid_format_command_exists
            self.line_count = 0
            self.output_zones = convert_settings.output_zones
        else:
            self.errors = False
            self.verbose = 'high'
            self.no_ext = None
            self.output_profile = SEPARATE
            self.written_projection = False
            self.written_tif_projection = False
            self.tif_projection_path = None
            self.spatial_database = None
            self.read_spatial_database = None
            self.read_spatial_database_tcf = None
            self.tcf = None
            self.output_gis_format = GIS_GPKG
            self.output_grid_format = GRID_TIF
            self.control_file = None
            self.control_file_out = None
            self.written_spatial_database_header = True
            self.projection_wkt = None
            self.root_folder = None
            self.output_folder = None
            self.wildcards = [r'(<<.{1,}?>>)']
            self.map_output_grids = []
            self.grid_format_command_exists = False
            self.line_count = 0
            self.output_zones = []

            self._parse_args(*args)
            if not self.tcf:
                raise FatalConvertException('Must provide TCF file location.')
            if not self.root_folder:
                self.root_folder = self._guess_root_folder()
                if self.root_folder is None:
                    raise FatalConvertException('Unable to guess root folder from TCF. Please assign a root folder using argument -rf <folder name>')
            if not self.output_folder:
                self.output_folder = self.tcf.parent
                self.output_folder = self.output_folder / self.tcf.stem / self.root_folder.name
            else:
                self.output_folder = self.output_folder / self.root_folder.name
            self.model_name = self.get_model_name(self.tcf)
            self.written_tif_projection = not self.output_grid_format == GRID_TIF

    def _parse_args(self, *args):
        """Routine to parse the CLI arguments."""

        from helpers.file import TuflowPath
        from helpers.gis import arg_to_ogr_format, arg_to_gdal_format
        while args:
            if args[0].lower() == '-rf':
                if len(args) < 2:
                    raise FatalConvertException('Root folder location required after -rf argument')
                self.root_folder = TuflowPath(args[1])
                args = args[1:]
            elif args[0].lower() == '-o':
                if len(args) < 2:
                    raise FatalConvertException('Output folder location required after -o argument')
                self.output_folder = TuflowPath(args[1])
                args = args[1:]
            elif args[0].lower() == '-tcf' :
                if len(args) < 2:
                    raise FatalConvertException('TCF file location required after -tcf argument')
                self.tcf = TuflowPath(args[1]).resolve()
                if not self.tcf.exists():
                    raise FatalConvertException(f'TCF does not exist: {self.tcf}')
                args = args[1:]
            elif re.findall(r'-gis([_-]?format)?', args[0], flags=re.IGNORECASE):
                if len(args) < 2:
                    raise FatalConvertException('GIS format (e.g. GPKG) required after -gis_format argument')
                self.output_gis_format = arg_to_ogr_format(args[1])
                if self.output_gis_format is None:
                    raise FatalConvertException(f'GIS format is not recognised: {args[1]}')
                args = args[1:]
            elif re.findall(r'-grid([_-]?format)?', args[0], flags=re.IGNORECASE):
                if len(args) < 2:
                    raise FatalConvertException('GRID format (e.g. TIF) required after -grid_format argument')
                self.output_grid_format = arg_to_gdal_format(args[1])
                if self.output_grid_format is None:
                    raise FatalConvertException(f'GRID format is not recognised: {args[1]}')
                args = args[1:]
            elif args[0].lower() == '-op':
                if len(args) < 2:
                    raise FatalConvertException('Output profile must be specified after -op argument. E.g. SEPARATE, TCF, CF')
                self.output_profile = self._arg_to_output_profile(args[1])
                if self.output_grid_format is None:
                    raise FatalConvertException(f'Output profile is not recognised: {args[1]}')

            args = args[1:]

    def _arg_to_output_profile(self, arg):
        """Helper to convert OUTPUT PROFILE (-op) to the correct setting."""

        if re.findall(r'^(SEP(ARATE)?)$', arg.strip(' \t"\''), flags=re.IGNORECASE):
            return SEPARATE
        if re.findall(r'^((GROUPED[_\s-]BY\s)?TCF)$', arg.strip(' \t"\''), flags=re.IGNORECASE):
            return GROUPED_BY_TCF
        if re.findall(r'^((GROUPED[_\s-]BY[_\s-])?CF[_\s-]?1?|(GROUPED[_\s-]BY\s)?CONTROL[_\s-]FILE[_\s-]?1?)$', arg.strip(' \t"\''), flags=re.IGNORECASE):
            return GROUPED_BY_CONTROL_FILE_1
        if re.findall(r'^((GROUPED[_\s-]BY[_\s-])?CF[_\s-]?2|(GROUPED[_\s-]BY\s)?CONTROL[_\s-]FILE[_\s-]?2)$', arg.strip(' \t"\''), flags=re.IGNORECASE):
            return GROUPED_BY_CONTROL_FILE_2

        return None

    def _guess_root_folder(self):
        """Routine to find the root folder if one hasn't been given."""

        # see if there's a folder named TUFLOW up to 5 folders above TCF
        root = self.tcf.find_parent('TUFLOW', 5)
        if root:
            return root

        # adopt the folder above 'runs' or 'model' which ever is highest
        runs = self.tcf.find_parent('RUNS', 4)
        model = self.tcf.find_in_walk_dir('MODEL', 4)

        if runs and model:
            if len(runs.parts) <= len(model.parts):
                return runs.parent
            else:
                return model.parent
        if runs is None and model is None:
            return
        elif runs:
            return runs.parent
        elif model:
            return model.parent

    def is_grouped_database(self):
        """Will return whether the output profile is a grouped database."""

        return self.output_profile in (GROUPED_BY_TCF, GROUPED_BY_CONTROL_FILE_1, GROUPED_BY_CONTROL_FILE_2,
                                       GROUPED_TEMP)

    def get_model_name(self, tcf):
        """Extract the model name from the tcf minus all the wildcards."""

        from helpers.file import TuflowPath

        p1 = r'[_\s]?~[es]\d?~'
        p2 = r'~[es]\d?~[_\s]?'
        m1 = re.sub(p1, '', tcf.name, flags=re.IGNORECASE)
        m2 = re.sub(p2, '', tcf.name, flags=re.IGNORECASE)
        if m1.count('_') != m2.count('_'):
            model_name = m1 if m1.count('_') > m2.count('_') else m2
        else:
            model_name = m1

        if model_name[-1] == '_':
            model_name = model_name[:-1]

        return str(TuflowPath(model_name).with_suffix(''))

    def copy_settings(self, control_file, output_file):
        """Copy the settings to a new object. Will update the settings based on the control file and output file."""

        from helpers.file import TuflowPath

        cf_ = control_file
        if control_file.suffix.upper() == '.TRD':
            control_file = self.control_file.parent / control_file.name

        settings = ConvertSettings(None, convert_settings=self)
        if self.output_profile == GROUPED_BY_CONTROL_FILE_1:
            if control_file.suffix.upper() == '.TCF':
                settings.spatial_database = TuflowPath('..') / 'model' / 'gis' / f'{settings.model_name}_{control_file.suffix.upper()[1:]}.gpkg'
            elif control_file.suffix.upper() in ['.TRD', '.TEF']:
                settings.spatial_database = self.spatial_database
            else:
                settings.spatial_database = TuflowPath(
                    'gis') / f'{settings.model_name}_{control_file.suffix.upper()[1:]}.gpkg'
        elif self.output_profile == GROUPED_BY_CONTROL_FILE_2:
            if control_file.suffix.upper() == '.TCF':
                settings.spatial_database = TuflowPath('..') / TuflowPath('model') / TuflowPath(
                    'gis') / f'{settings.model_name}_{control_file.suffix.upper()[1:]}.gpkg'
            elif control_file.suffix.upper() in ['.TRD', '.TEF']:
                if control_file.suffix.upper() == '.TEF':
                    name_ = f'{settings.model_name}_{control_file.suffix.upper()[1:]}.gpkg'
                else:
                    name_ = f'{control_file.stem}_TRD.gpkg'
                gpkg_path = TuflowPath('..') / 'model' / 'gis' / name_
                settings.spatial_database = os.path.relpath((control_file.parent / gpkg_path).resolve(), control_file.parent)
            else:
                settings.spatial_database = TuflowPath(
                    'gis') / f'{settings.model_name}_{control_file.suffix.upper()[1:]}.gpkg'
        elif self.output_profile == GROUPED_BY_TCF:
            settings.spatial_database = TuflowPath('..') / TuflowPath('model') / TuflowPath('gis') / f'{self.model_name}.gpkg'

        settings.control_file = control_file
        settings.control_file_out = output_file

        if self.need_to_write_spatial_database(settings, cf_):
            settings.written_spatial_database_header = False

        return settings

    def need_to_write_spatial_database(self, settings, control_file=None):
        """Will return whether a SPATIAL DATABASE == .. command has already been written to the control file."""

        from helpers.gis import GIS_GPKG, GRID_GPKG
        from helpers.control_file import get_commands

        if control_file is None:
            control_file = settings.control_file

        gis_found = False
        for command in get_commands(control_file, settings):
            if command.is_read_gis() or command.is_read_grid() or command.is_read_projection():
                gis_found = True
                break

        if settings.output_gis_format == GIS_GPKG or settings.output_grid_format == GRID_GPKG:
            if settings.output_profile == GROUPED_BY_CONTROL_FILE_2:
                return gis_found
            if settings.output_profile == GROUPED_BY_CONTROL_FILE_1 and \
                    settings.control_file.suffix.upper() not in ['.TRD', '.TEF']:
                return gis_found
            if settings.output_profile == GROUPED_BY_TCF and settings.control_file.suffix.upper() == '.TCF':
                return True
        else:
            return False

    def read_tcf(self):
        """Routine to make an initial read of the TCF to extract some settings."""

        from helpers.control_file import get_commands
        from helpers.gis import GIS_GPKG, GIS_MIF, GIS_SHP, GRID_TIF, ogr_projection, gdal_format_2_ext, get_database_name
        from helpers.file import globify, TuflowPath

        if self.tcf is None:
            return

        self.written_projection = False
        self.control_file = self.tcf
        for command in get_commands(self.tcf, self):
            if command.is_read_projection():
                if self.output_gis_format == GIS_GPKG:
                    if 'GPKG' in command.command:
                        self.written_projection = True
                if self.output_gis_format == GIS_SHP:
                    if 'SHP' in command.command:
                        self.written_projection = True
                if self.output_gis_format == GIS_MIF:
                    if 'MIF' in command.command:
                        self.written_projection = True
                self.projection_wkt = ogr_projection(command.get_gis_src_file())
            elif command.is_grid_format():
                self.grid_format_command_exists = True
            elif command.is_spatial_database_command():
                self.read_spatial_database = command.value
            elif command.is_event_file():
                self.read_tef((self.control_file.parent / command.value).resolve(), self)
            elif command.is_map_output_format():
                if re.findall(rf'{gdal_format_2_ext(self.output_grid_format).upper()[1:]}',
                                  command.value, flags=re.IGNORECASE):
                    self.map_output_grids = re.findall(rf'{gdal_format_2_ext(self.output_grid_format).upper()[1:]}',
                                                       command.value, flags=re.IGNORECASE)
                else:
                    self.map_output_grids = [f'{gdal_format_2_ext(self.output_grid_format).upper()[1:]}']
                if 'TIF' not in self.map_output_grids:
                    self.written_tif_projection = True
            elif self.output_grid_format == GRID_TIF and command.command == 'TIF PROJECTION':
                self.written_tif_projection = True
            elif self.output_grid_format == GRID_TIF and command.command == 'GEOMETRY CONTROL FILE':
                gcf = (self.tcf.parent / command.value).resolve()
                settings = self.copy_settings(gcf, gcf)
                if gcf.exists():
                    for g_command in get_commands(gcf, settings):
                        if g_command.is_spatial_database_command():
                            settings.read_spatial_database = command.value
                        elif g_command.is_read_grid():
                            for _ in g_command.iter_grid(settings):  # consider READ GRID ZPTS == GRID | POLYGON
                                # expand glob
                                rel_path = os.path.relpath(g_command.value, gcf.parent)
                                rel_path = globify(rel_path, settings.wildcards)
                                for src_file in gcf.parent.glob(rel_path, settings.wildcards):
                                    db, _ = get_database_name(src_file)
                                    self.tif_projection_path = os.path.relpath(TuflowPath(db).with_suffix('.tif'), self.tcf.parent)
                                    break
                                if self.tif_projection_path is not None:
                                    break
                        elif self.tif_projection_path is not None:
                            break
            elif command.is_output_zone():
                self.output_zones.extend([x.strip() for x in command.value.split('|')])
            elif command.is_read_file():
                self.read_file((self.control_file.parent / command.value).resolve(), self)

        self.read_spatial_database = None

    def read_tef(self, event_file, settings):
        """Routine to make an initial read of the TEF and extract wildcard names."""

        from helpers.control_file import get_event_commands

        for command in get_event_commands(event_file, settings):
            if command.is_event_source():
                event_wildcard, _ = command.get_event_source()
                if event_wildcard is not None and re.escape(event_wildcard) not in settings.wildcards:
                    settings.wildcards.append(re.escape(event_wildcard))
            elif command.is_output_zone():
                self.output_zones.extend([x.strip() for x in command.value.split('|')])


    def read_file(self, read_file, settings):
        from helpers.control_file import get_commands

        for command in get_commands(read_file, settings):
            if command.is_output_zone():
                self.output_zones.extend([x.strip() for x in command.value.split('|')])


    def process_spatial_database_command(self, value):
        """Process a SPATIAL DATABASE command and set the read spatial database setting to an appropriate value."""

        value = str(value)

        if value.upper() == 'TCF':
            self.read_spatial_database = self.read_spatial_database_tcf
        elif value.upper() == 'NONE':
            self.read_spatial_database = None
            self.read_spatial_database_tcf = None
        else:
            self.read_spatial_database = (self.control_file.parent / value).resolve()

        if self.control_file.suffix.upper() == '.TCF':
            self.read_spatial_database_tcf = self.read_spatial_database
