import re
import os
import sys

from osgeo.ogr import wkbPolygon
from itertools import permutations
from .file import (copy_file, add_geom_suffix, remove_geom_suffix, change_gis_mi_folder, tuflowify_value,
                   replace_with_output_filepath, get_geom_suffix, globify, TuflowPath,
                   copy_rainfall_grid_csv, insert_variables, predict_encoding, expand_path, copy_file2)
from .settings import MinorConvertException, FatalConvertException, SEPARATE, GROUPED_TEMP
from .gis import (GIS_GPKG, GIS_MIF, GIS_SHP, GRID_ASC, GRID_FLT, GRID_GPKG, GRID_TIF, GRID_NC,
                  ogr_copy, ogr_format, ogr_format_2_ext, gdal_copy, gdal_format_2_ext,
                  ogr_iter_geom, geom_type_2_suffix, get_database_name)

CONTROL_FILES = [
    'GEOMETRY CONTROL FILE', 'BC CONTROL FILE', 'ESTRY CONTROL FILE', 'EVENT FILE', 'READ FILE',
    'RAINFALL CONTROL FILE', 'EXTERNAL STRESS FILE', 'QUADTREE CONTROL FILE', 'AD CONTROL FILE',
    'ESTRY CONTROL FILE AUTO'
    ]


class DefineBlock:

    def __init__(self, type_, name_):
        self.type = type_
        self.name = name_
        self.first_if_written = True
        self.any_if_written = False

    def __eq__(self, other):
        if isinstance(other, DefineBlock):
            return self.type == other.type and self.name == other.name
        return False


class Command:
    """Class for handling TUFLOW command."""

    def __init__(self, line, settings):
        self.comment_index = 0
        self.prefix = ''
        self.original_text = line
        self.command_orig, self.value_orig = self.strip_command(line)
        self.iter_geom_index = -1
        self.geom_count = 0
        self.multi_geom_db = None
        self.in_define_block = False
        self.define_blocks = []
        self.auto_estry = False
        self.auto_estry_old = False
        self.settings = settings

        self.command = self.command_orig.upper() if self.command_orig is not None else None

        abs_path = re.findall(r'(([A-Za-z]:\\)|\\\\)', str(self.value_orig)) and sys.platform == 'win32'
        if not abs_path and self.is_value_a_file() and str(self.value_orig)[0] in ['\\', '/']:
            self.value_orig = f'.{self.value_orig}'

        if self.is_read_gis() or (self.is_read_grid() and not self.is_rainfall_grid_nc()) or self.is_read_projection():
            self.value = tuflowify_value(self.value_orig, settings)
            self.value_expanded_path = expand_path(self.value, settings, gis_file=True)
        elif self.is_read_tin():
            self.value = tuflowify_value(self.value_orig, settings)
            self.value = TuflowPath(str(self.value).split(' >> ')[0])
            self.value_expanded_path = expand_path(self.value, settings)
        elif self.is_value_a_file():
            self.value = TuflowPath(insert_variables(self.value_orig, settings.variables))
            self.value_ = tuflowify_value(self.value_orig, settings)
            self.value_ = TuflowPath(str(self.value_).split(' >> ')[0])
            self.value_expanded_path = expand_path(self.value_, settings)
        else:
            self.value = insert_variables(self.value_orig, settings.variables)
            self.value_expanded_path = self.value

        if self.is_control_file() and str(self.value).upper() == 'AUTO' or self.command == 'ESTRY CONTROL FILE AUTO':
            self.auto_estry = True
            self.auto_estry_old = self.command == 'ESTRY CONTROL FILE AUTO'
            self.value = settings.control_file.with_suffix('.ecf').name
            self.value_ = tuflowify_value(str(self.value), settings)
            self.value_ = TuflowPath(str(self.value).split(' >> ')[0])
            self.value_expanded_path = expand_path(self.value, settings)

        if self.is_spatial_database_command():
            self.settings.process_spatial_database_command(self.value)

    def __str__(self):
        return self.original_text

    def count_geom(self, settings):
        """
        Returns the number of geometry types in the READ GIS command.

        E.g.
         - MIF file with 2 geometry types will return 2
         - "gis\shape_file_L.shp | gis\shape_file_P.shp" - will return 2
        """

        count = 0
        for i, value in enumerate(str(self.value).split('|')):
            value = tuflowify_value(value.strip(), settings)
            if self.is_value_a_number(value=value, iter_index=i):
                pass
            elif self.is_modify_conveyance() and i > 0:
                pass
            elif self.gis_format(settings, value) != GIS_MIF:
                count += 1
            else:
                for _ in ogr_iter_geom(value):
                    count += 1

        return count

    def iter_geom(self, settings):
        """
        Iterate through the geometry types in the READ GIS command.

        While iterating, self.value can be modified so that each geometry on the line is considered separately.

        E.g.
         - for a mif file containing points an lines - will return all points, then next iteration will return lines.
        """

        value_orig = self.value  # store original value
        self.geom_count = self.count_geom(settings)
        for value in str(self.value).split('|'):

            # change value so that other routines will work automatically
            self.value = tuflowify_value(value.strip(), settings)

            # store the database so if converting to gpkg can all be written to the same file
            if self.multi_geom_db is None:
                self.multi_geom_db, _ = get_database_name(self.value)
                self.multi_geom_db = TuflowPath(self.multi_geom_db).with_suffix('.gpkg')

            self.iter_geom_index += 1
            if self.is_value_a_number(value=value):
                self.value = value.strip()
                yield 'VALUE'
            elif self.is_modify_conveyance() and self.iter_geom_index > 0:
                self.command = re.sub(r'(?<=\s)GIS(?=\s)', 'GRID', self.command)
                yield 'GRID'
            elif self.gis_format(settings) != GIS_MIF:
                yield None
            else:  # for mif files, need to open the file and see what's in there
                self.iter_geom_index -= 1  # wind back one and iterate through layer
                for geom_type in ogr_iter_geom(self.get_gis_src_file()):
                    self.iter_geom_index += 1
                    yield geom_type

        # reset all these values
        self.value = value_orig
        self.iter_geom_index = -1
        self.multi_geom_db = None

    def iter_grid(self, settings):
        """Iterate READ GRID command values - this is to handle READ GRID == grid | polygon"""

        if '|' in str(self.value):
            value_orig = self.value
            self.geom_count = 2

            grid, gis = [x.strip() for x in str(self.value).split('|', 1)]

            self.value = tuflowify_value(grid.strip(), settings)
            self.iter_geom_index += 1
            yield 'GRID'

            command_orig = self.command
            self.command = 'READ GIS'  # trick into thinking this is now a READ GIS
            self.value = tuflowify_value(gis.strip(), settings)

            if self.gis_format(settings) == GIS_MIF:
                for geom_type in ogr_iter_geom(self.get_gis_src_file()):
                    if geom_type == wkbPolygon:
                        self.iter_geom_index += 1
                        yield geom_type
            else:
                self.iter_geom_index += 1
                yield None

            # reset all these values
            self.value = value_orig
            self.command = command_orig
            self.iter_geom_index = -1

        else:
            yield 'GRID'

    def iter_tin(self, settings):
        if '|' in str(self.value):
            value_orig = self.value
            self.geom_count = 2

            tin, gis = [x.strip() for x in self.value.split('|', 1)]

            if settings.control_file:
                self.value = (settings.control_file.parent / tin).resolve()
            else:
                self.value = tin.strip()
            self.iter_geom_index += 1
            yield 'TIN'

            command_orig = self.command
            self.command = 'READ GIS'  # trick into thinking this is now a READ GIS
            self.value = tuflowify_value(gis.strip(), settings)

            if self.gis_format(settings) == GIS_MIF:
                for geom_type in ogr_iter_geom(self.get_gis_src_file()):
                    if geom_type == wkbPolygon:
                        self.iter_geom_index += 1
                        yield geom_type
            else:
                self.iter_geom_index += 1
                yield None

            # reset all these values
            self.value = value_orig
            self.command = command_orig
            self.iter_geom_index = -1

        else:
            yield 'TIN'

    def iter_files(self, settings):
        if settings.control_file is None:
            return
        if self.is_read_gis() or (self.is_read_grid() and not self.is_rainfall_grid_nc()) or self.is_read_projection() or self.is_read_tin():
            try:
                rel_path = os.path.relpath(self.value, settings.control_file.parent)
            except ValueError:
                rel_path = self.value
        elif self.is_value_a_file():
            try:
                rel_path = os.path.relpath(self.value_, settings.control_file.parent)
            except ValueError:
                rel_path = self.value_
        else:
            rel_path = TuflowPath(self.value)
        if not settings.use_scenarios or not re.findall(r'<<~?[s]\d?~?>>', str(rel_path), flags=re.IGNORECASE):
            rel_path = globify(rel_path, settings.wildcards)
            for file in settings.control_file.parent.glob(rel_path, settings.wildcards):
                yield file
        else:
            wild_cards = list(set([x.strip() for x in re.findall(r'<<~?s\d?~?>>', str(rel_path), flags=re.IGNORECASE)]))
            if wild_cards and not settings.scenarios:
                return
            if len(wild_cards) > len(settings.scenarios):
                scenarios = settings.scenarios + ['*' for x in range(len(wild_cards) - len(settings.scenarios))]
            else:
                scenarios = settings.scenarios[:]

            files = []
            for i in range(len(scenarios) - len(wild_cards) + 1):
                scenario_slice = scenarios[i:i+len(wild_cards)]
                for perm in permutations(scenario_slice):
                    rp = str(rel_path)
                    for wc, s in zip(wild_cards, perm):
                        rp = re.sub(re.escape(wc), s, rp, flags=re.IGNORECASE)
                    rp = globify(rp, settings.wildcards)
                    for file in settings.control_file.parent.glob(rp, settings.wildcards):
                        if file not in files:
                            yield file
                            files.append(file)

    def gis_format(self, settings, value=None):
        """
        Return GIS vector format as an OGR Format driver name (.e.g. 'ESRI Shapefile')
        of input file in READ GIS command.
        """

        if not self.is_valid() or self.value is None and not self.is_read_gis():
            return None

        no_ext_is_gpkg = settings.read_spatial_database is not None \
                          and settings.no_ext != GIS_MIF or settings.no_ext == GIS_GPKG
        no_ext_is_mif = settings.read_spatial_database is None \
                        and settings.no_ext != GIS_GPKG or settings.no_ext == GIS_MIF

        if value is None:
            value = self.value

        return ogr_format(value, no_ext_is_mif, no_ext_is_gpkg)

    def needs_grouped_database(self, settings):
        """Returns whether the converted output GIS file is going to a grouped database."""

        return settings.is_grouped_database() and (
                (self.is_read_gis() and settings.output_gis_format == GIS_GPKG) or
                (self.is_read_grid() and settings.output_grid_format == GRID_GPKG) or
                (self.is_read_projection() and settings.output_gis_format == GIS_GPKG)) and \
                not self.is_read_tin()

    def is_one_line_multi_layer(self, settings):
        """
        Returns True/False if the multiple output GIS layers
        are going to be written to one line - also outputs true only if each layer needs a full path
        """

        return self.is_read_gis() and self.geom_count > 1 and not self.needs_grouped_database(settings)

    def get_gis_src_file(self, db=True, lyrname=True, combine=None):
        """Returns full path to GIS input file in a 'database >> layername' format even if not required."""

        if db and lyrname:
            if combine is None:
                return self.value
            else:
                if 'generate_from_db' in combine:
                    return f'{combine["generate_from_db"]} >> {TuflowPath(combine["generate_from_db"]).with_suffix("").name}'
                elif 'db' in combine and 'lyrname' in combine:
                    return f'{combine["db"]} >> {combine["lyrname"]}'
                elif 'db' in combine:
                    return f'{combine["db"]} >> {get_database_name(self.value)[1]}'
                elif 'lyrname' in combine:
                    return f'{get_database_name(self.value)[0]} >> {combine["lyrname"]}'
                else:
                    return self.value
        elif not lyrname:
            return get_database_name(self.value)[0]
        elif not db:
            return get_database_name(self.value)[1]

    def get_gis_dest_file(self, input_file, settings, geom=None, return_relative=False):
        """Returns full path to GIS output file in a 'database >> layername' format even if not required."""

        try:
            # input database and layer name
            db, lyr = get_database_name(input_file)

            if not self.needs_grouped_database(settings) and TuflowPath(db).stem != lyr:
                # don't name the file after the database - name it after the layer
                db = (TuflowPath(db).parent / lyr).with_suffix(TuflowPath(db).suffix)
                if self.geom_count > 1 and settings.output_gis_format == GIS_GPKG:
                    db = remove_geom_suffix(db)
                    if self.iter_geom_index == 0:
                        self.multi_geom_db = db

            if geom:  # if there is a geom type, make sure it's added to the lyr name
                lyr = add_geom_suffix(lyr, geom_type_2_suffix(geom))
                if settings.output_gis_format != GIS_GPKG or self.geom_count < 2:
                    db = add_geom_suffix(db, geom_type_2_suffix(geom))

            if self.is_one_line_multi_layer(settings):
                # temporarily switch this to a single database so that output GIS files are written to a single database
                settings.spatial_database = remove_geom_suffix(self.multi_geom_db)
                if re.findall(settings.wildcards[0], str(settings.spatial_database)):
                    settings.spatial_database = TuflowPath(remove_geom_suffix(get_database_name(input_file)[0])).with_suffix('.gpkg')
                settings.output_profile = GROUPED_TEMP

            # figure out the output database
            if self.needs_grouped_database(settings) and settings.spatial_database is not None:
                db = (settings.control_file.parent / settings.spatial_database).resolve()
            elif self.is_read_tin():
                pass
            elif self.is_read_gis() or self.is_read_projection():
                db = TuflowPath(db).with_suffix(ogr_format_2_ext(settings.output_gis_format))
            else:
                db = TuflowPath(db).with_suffix(gdal_format_2_ext(settings.output_grid_format))

            # for control file text, return relative reference
            control_file = None
            if return_relative:
                control_file = settings.control_file

            # convert input filepath to output filepath
            db = replace_with_output_filepath(db, settings.root_folder, settings.output_folder, control_file)
            if self.is_read_gis() or self.is_read_projection():
                input_gis_format = ogr_format(input_file)
                db = change_gis_mi_folder(db, input_gis_format, settings.output_gis_format)  # change any mi folder to gis (and vise/versa)
                if settings.output_profile == GROUPED_TEMP:  # reset these values
                    settings.spatial_database = None
                    settings.output_profile = SEPARATE

            return TuflowPath(f'{db} >> {lyr}')

        except Exception as e:
            settings.errors = True
            raise MinorConvertException(f'Error: {e}')

    def is_spatial_database_command(self):
        """Returns True/False if command is setting the spatial database."""

        return self.command == 'SPATIAL DATABASE'

    def make_new_text(self, settings, geom=None):
        """Generate new text to be inserted into the control file."""

        if not self.is_valid():
            return self.original_text

        if self.iter_geom_index > 0:  # multi GIS layer on single line and not the first layer
            text = f' | {self.make_new_value(settings, geom)}'
        elif self.value is None or self.auto_estry_old:
            text = f'{self.make_new_command(settings)}'
        else:
            text = f'{self.make_new_command(settings)} == {self.make_new_value(settings, geom)}'

        if self.is_read_gis() or self.is_read_grid() or self.is_read_tin():
            return text  # add comments only after whole line has been completed (in case of multiple layers)
        else:
            return self.re_add_comments(text)

    def re_add_comments(self, new_command):
        """Re-add any comments after the command - try and maintain their position as best as possible."""

        if self.comment:
            if len(new_command) >= self.comment_index:
                self.comment_index = len(new_command) + 1
            dif = self.comment_index - len(new_command)
            new_command = f'{new_command}{" " * dif}{self.comment}'

        return f'{new_command}\n'

    def make_new_value(self, settings, geom=None):
        """Generate a new value for the output control file text 'command == value'"""

        if self.value is None:
            return None

        outpath = self.value

        try:
            # get the output path
            if self.is_value_a_number(value=self.value):
                db, lyr = get_database_name(self.value)
                if self.iter_geom_index == 0:
                    return lyr
                else:
                    return str(TuflowPath(db).name)
            elif self.is_read_gis() or (self.is_read_grid() and not self.is_rainfall_grid_nc()) or self.is_read_projection() or self.is_read_tin():
                outpath = self.get_gis_dest_file(self.value, settings, geom, return_relative=True)
            else:
                if isinstance(self.value, str) or isinstance(self.value, TuflowPath):
                    outpath = str(TuflowPath(self.value))

            if self.is_read_gis() or self.is_read_projection():
                db, lyr = get_database_name(outpath)
                if settings.output_gis_format == GIS_GPKG and settings.is_grouped_database():
                    return TuflowPath(lyr)
                elif TuflowPath(db).stem != lyr:
                    return outpath
                else:
                    return TuflowPath(db)

            if self.is_read_grid():
                db, lyr = get_database_name(outpath)
                if settings.output_grid_format == GRID_GPKG and settings.is_grouped_database():
                    return TuflowPath(lyr)
                elif TuflowPath(db).with_suffix('').name != lyr:
                    return self.value
                else:
                    return TuflowPath(db)

            if self.is_read_tin():
                db, lyr = get_database_name(outpath)
                return TuflowPath(db)

            if self.is_gis_format():
                if settings.output_gis_format == GIS_GPKG:
                    return 'GPKG'
                if settings.output_gis_format == GIS_MIF:
                    return 'MI'
                if settings.output_gis_format == GIS_SHP:
                    return 'SHP'
            elif self.is_grid_format():
                if settings.output_grid_format == GRID_ASC:
                    return 'ASC'
                if settings.output_grid_format == GRID_FLT:
                    return 'FLT'
                if settings.output_grid_format == GRID_GPKG:
                    return 'GPKG'
                if settings.output_grid_format == GRID_TIF:
                    return 'TIF'
                if settings.output_grid_format == GRID_NC:
                    return 'NC'

            if self.is_map_output_format():
                if not re.findall(rf'{gdal_format_2_ext(settings.output_grid_format).upper()[1:]}',
                                  self.value, flags=re.IGNORECASE):
                    return re.sub(r'(ASC|FLT|GPKG|TIF|NC)',
                                  f'{gdal_format_2_ext(settings.output_grid_format).upper()[1:]}',
                                  self.value, flags=re.IGNORECASE)

            if settings.use_scenarios and self.is_start_define() and self.define_blocks[-1].type in ['SCENARIO', 'EVENT']:
                scenarios_upper = [x.upper() for x in settings.scenarios]
                return ' | '.join([x.strip() for x in self.value.split('|') if x.upper().strip() in scenarios_upper])

            if self.auto_estry:
                return self.value_orig

        except Exception as e:
            settings.errors = True
            raise MinorConvertException(f'Error: {e}')

        return outpath

    def make_new_command(self, settings):
        """Generate a new command for the output control file text 'command == value'"""

        if self.is_read_projection():
            return f'{self.prefix}{self.new_prj_command(settings)}'

        if self.is_read_gis():
            return f'{self.prefix}{self.new_gis_command(settings)}'

        if self.is_map_output_setting():
            return f'{self.prefix}{self.new_map_output_setting_command(settings)}'

        if self.command[:4] == 'ELSE' and not self.first_scenario_if_written():
            self.set_first_scenario_if_written(True)
            if 'IF' in self.command:
                return f'{self.prefix}{self.command_orig[4:]}'
            else:
                return f'{self.prefix}{self.new_if_statement_for_else_block()}\n{self.prefix}{self.command_orig}'

        return f'{self.prefix}{self.command_orig}'

    def new_gis_command(self, settings):
        if settings.output_gis_format != GIS_MIF:
            return re.sub(r'(?<=\s)MI(?=\s)', 'GIS', self.command_orig, flags=re.IGNORECASE)

        return self.command_orig

    def new_prj_command(self, settings):
        """Output projection command."""
        if settings.output_gis_format == GIS_MIF:
            return 'MI Projection'
        else:
            return f'{ogr_format_2_ext(settings.output_gis_format).upper()[1:]} Projection'

    def new_map_output_setting_command(self, settings):
        """
        New grid specific map output setting command.

        e.g. ASC MAP OUTPUT INTERVAL -> TIF MAP OUTPUT INTERVAL
        """

        grid_type = re.findall(r'(ASC|FLT|GPKG|TIF|NC)', self.command)
        if not grid_type:
            return self.command_orig

        grid_type = grid_type[0]

        if grid_type in settings.map_output_grids:
            return self.command_orig

        return re.sub(rf'{grid_type}', f'{gdal_format_2_ext(settings.output_grid_format).upper()[1:]}',
                      self.command_orig, flags=re.IGNORECASE)

    def is_valid(self):
        """Returns if command contains a valid command (i.e. not a comment or blank)."""

        return self.command is not None

    def is_control_file(self):
        """Returns whether command is referencing a control file."""

        return self.command in CONTROL_FILES and self.value is not None and TuflowPath(self.value).suffix.upper() != '.CSV'

    def is_event_file(self):
        """Return whether command is referencing the TUFLOW EVENT FILE."""

        return self.command == 'EVENT FILE'

    def is_quadtree_control_file(self):
        """Return whether command is referencing QUADTREE CONTROL FILE."""

        return self.command == 'QUADTREE CONTROL FILE'

    def is_quadtree_single_level(self):
        """Return whether command is referencing a single level quadtree domain."""

        return self.is_quadtree_control_file() and self.value.upper() == 'SINGLE LEVEL'

    def is_bc_dbase_file(self):
        """Returns whether command is referencing the bc_dbase.csv."""

        return self.command == 'BC DATABASE' or self.command == 'AD BC DATABASE'

    def is_pit_inlet_dbase_file(self):
        """Returns whether command is referecing the pit_inlet_dbase.csv"""

        return self.command and ('PIT INLET DATABASE' in self.command or 'DEPTH DISCHARGE DATABASE' in self.command)

    def is_rainfall_grid(self):
        """Returns whether command is referencing READ GRID RF"""

        return self.is_rainfall_grid_nc() or self.is_rainfall_grid_csv()

    def is_rainfall_grid_csv(self):
        """Returns whether the command is referencing READ GRID RF == CSV"""

        return self.command == 'READ GRID RF' and self.value is not None \
               and self.value.suffix.upper() == '.CSV'

    def is_rainfall_grid_nc(self):
        """Returns whether the command is referencing READ GRID RF == NC"""

        return self.command == 'READ GRID RF' and self.value_orig is not None \
               and TuflowPath(self.value_orig).suffix.upper() == '.NC'

    def is_read_gis(self):
        """Returns whether command is a 'READ GIS' command."""

        return self.is_valid() \
               and ('READ GIS' in self.command or 'CREATE TIN ZPTS' in self.command or 'READ MI' in self.command or
                    'READ MID' in self.command)

    def is_read_grid(self):
        """Returns whether command is a 'READ GRID' command."""

        return self.is_valid() and 'READ GRID' in self.command and self.value_orig is not None \
               and TuflowPath(self.value_orig).suffix.upper() != '.CSV' and 'RF' not in self.command

    def is_read_file(self):
        """Returns whether command is a 'READ FILE' command."""

        return self.command == 'READ FILE'

    def is_read_tin(self):
        """Returns whether command is a 'READ TIN' command."""

        return self.is_valid() and 'READ TIN' in self.command

    def is_value_a_file(self):
        """
        Returns whether the value is pointing to a file.

        E.g.
        Can include 'Read Materials == CSV/TMF' or 'Read Soils == TSOILF'
        """

        return self.value_orig is not None and TuflowPath(self.value_orig).suffix and not self.is_value_a_number() \
            and not self.is_value_a_number_tuple() and not self.is_start_define()

    def is_read_projection(self):
        """Returns whether command is a set model projection command."""

        return self.command is not None and 'PROJECTION' in self.command and 'CHECK' not in self.command \
            and (bool(re.findall(r'\.(shp)|(prj)|(mi)|(gpkg)', str(self.value_orig), flags=re.IGNORECASE)) or
                 TuflowPath(self.value_orig).suffix == '')

    def is_value_a_number(self, value=None, iter_index=None):
        """Returns whether the value of the command is a number."""

        if not self.is_valid() or not self.value_orig:
            return False

        if iter_index is None:
            iter_index = self.iter_geom_index

        try:
            if value is not None:
                float(value)
            else:
                float(str(self.value_orig))
            return True
        except (ValueError, TypeError):
            if iter_index == 1 and re.findall('^<<.+?>>$', value.strip()) and re.findall('<<.+?>>', value)[0] == value.strip():
                return True
            return False

    def return_number(self, value):
        if self.is_set_code() or self.is_set_mat() or self.is_set_soil():
            return int(value)
        else:
            return float(value)

    def is_value_a_number_tuple(self, value=None, iter_index=None):
        if not self.is_valid() or not self.value_orig:
            return False

        if iter_index is None:
            iter_index = self.iter_geom_index

        try:
            if value is not None:
                db, lyr = get_database_name(value)
                try:
                    values = re.split(r',\s?|\s+|\t+', lyr)
                    [float(v) for v in values]
                except (TypeError, ValueError):
                    values = re.split(r',\s?|\s+|\t+', str(TuflowPath(db).name))
                    [float(v) for v in values]
            else:
                values = re.split(r',\s?|\s+|\t+', self.value_orig)
                [float(v) for v in values]
            return True
        except (TypeError, ValueError):
            return False

    def return_number_tuple(self, value):
        values = re.split(r',\s?|\s+|\t+', value)
        return tuple([float(x) for x in values])

    def is_map_output_format(self):
        """
        Returns whether command is READ MAP OUTPUT FORMAT and
        contains any output grids formats that should be converted.
        """

        if self.command == 'MAP OUTPUT FORMAT':
            return re.findall(r'(ASC|FLT|GPKG|TIF|NC)', self.value, flags=re.IGNORECASE)

        return False

    def is_map_output_setting(self):
        """
        Returns whether command is a setting for a grid map output

        e.g. ASC Map Output Interval
        """

        return 'MAP OUTPUT INTERVAL' in self.command or 'MAP OUTPUT DATA TYPES' in self.command \
               and re.findall(r'(ASC|FLT|GPKG|TIF|NC)', self.command)

    def is_z_shape(self):
        """Returns whether READ GIS command is also a READ GIS Z SHAPE command."""

        return self.is_valid() and self.is_read_gis() \
               and ('Z SHAPE' in self.command or 'Z LINE' in self.command or 'CREATE TIN ZPTS' in self.command)

    def is_2d_zpts(self):
        """Returns whether READ GIS command is also a READ GIS Zpts command."""

        return self.is_valid() and (self.is_read_gis() or self.is_read_grid) \
               and 'ZPTS' in self.command

    def is_modify_conveyance(self):
        """Returns whether READ GIS command is also a READ GIS Zpts Modify Conveyance command."""

        return self.is_2d_zpts() and 'MODIFY CONVEYANCE' in self.command

    def is_2d_lfcsh(self):
        """Returns whether READ GIS command is also a READ GIS LAYERED FC SHAPE command."""

        return self.is_valid() and self.is_read_gis() \
                and 'LAYERED FC SHAPE' in self.command or 'FLC' in self.command

    def is_2d_bc(self, control_file = None):
        """Returns whether READ GIS command is also a 2d READ GIS BC command."""

        if control_file is None:
            control_file = self.settings.control_file
        if control_file is None:
            return self.is_valid() and self.is_read_gis() and 'BC' in self.command
        else:
            return self.is_valid() and self.is_read_gis() and 'BC' in self.command and control_file.suffix.upper() == '.TBC'

    def is_set_code(self):
        return self.is_valid() and 'SET CODE' in self.command

    def is_set_mat(self):
        return self.is_valid() and 'SET MAT' in self.command

    def is_set_soil(self):
        return self.is_valid() and 'SET SOIL' in self.command

    def can_use_multiple_gis_inputs(self, control_file):
        """Returns whether the command can use multiple files on the same line (usually separated by | )."""

        return self.is_z_shape() or self.is_2d_bc(control_file) or self.is_2d_lfcsh() or self.is_2d_zpts()

    def is_gis_format(self):
        """Returns whether command is setting the model GIS FORMAT."""

        return self.command == 'GIS FORMAT'

    def is_grid_format(self):
        """Returns whether command is setting the model GRID FORMAT."""

        return self.command == 'GRID FORMAT' or self.command == 'RF GRID FORMAT'

    def is_read_database(self):
        """Returns whether command is a database command."""

        return self.is_mat_csv() or self.is_bc_dbase_file() or self.is_rainfall_grid() \
            or self.is_pit_inlet_dbase_file() or self.is_soil_dbase()

    def is_mat_dbase(self):
        """Returns whether command is a Read Material(s) File"""

        return self.is_mat_csv() or self.is_mat_tmf()

    def is_mat_csv(self):
        """Returns whether command is a Read Material(s) File == .CSV"""

        return self.is_valid() and self.is_value_a_file() and TuflowPath(self.value).suffix.upper() == '.CSV'\
               and bool(re.findall(r'^READ MATERIALS? FILE', self.command))

    def is_mat_tmf(self):
        """Returns whether command is a Read Material(s) File == .TMF"""

        return self.is_valid() and self.is_value_a_file() and TuflowPath(self.value).suffix.upper() == '.TMF'\
               and bool(re.findall(r'^READ MATERIALS? FILE', self.command))

    def is_soil_dbase(self):
        """Returns whether command is a Read Soil(s) File"""

        return self.is_valid() and self.is_value_a_file() and TuflowPath(self.value).suffix.upper() == '.TSOILF' \
                  and bool(re.findall(r'^READ SOILS? FILE', self.command))

    def is_start_define(self):
        """Returns whether command is the start of a define block."""

        return self.is_valid() and bool(re.findall(r'^(DEFINE|(ELSE\s)?IF|START 1D)', self.command))

    def is_end_define(self):
        if self.is_valid():
            return bool(re.findall(r'^(END DEFINE|END IF|END 1D|ELSE IF)', self.command))

        return False

    def is_else_if(self):
        if self.is_valid():
            return bool(re.findall(r'^(ELSE\s*IF)', self.command))

        return False

    def is_else(self):
        if self.is_valid():
            return bool(re.findall(r'^(ELSE)$', self.command.strip()))

        return False

    def define_start_type(self):
        """Returns the type of block the define block is."""

        if not self.is_valid():
            return None

        if re.findall(r'^(DEFINE|(ELSE\s)?IF)', self.command):
            return re.sub(r'^(DEFINE|(ELSE\s)?IF)', '', self.command).strip()
        elif self.command == 'START 1D DOMAIN':
            return '1D DOMAIN'
        else:
            return None

    def in_1d_domain_block(self):
        for define_block in self.define_blocks:
            if define_block.type == '1D DOMAIN':
                return True

        return False

    def in_scenario_block(self, scenario_name=None):
        if not self.define_blocks:
            return False

        in_scenario = True
        if isinstance(scenario_name, str):
            scenario_name = [scenario_name]
        for define_block in reversed(self.define_blocks):
            if define_block.type in ['EVENT', 'SCENARIO']:
                if scenario_name is None:
                    return True
                elif not scenario_name:
                    return False
                found = False
                for sn in scenario_name:
                    if not [x.strip().upper() for x in define_block.name.split('|') if x.strip()[0] != '!']:
                        found = True
                        break
                    if sn.upper() in [x.strip().upper() for x in define_block.name.split('|') if x.strip()[0] != '!']:
                        found = True
                        break

                if not found:
                    in_scenario = False

        return in_scenario

    def set_first_scenario_if_written(self, b):
        for define_block in reversed(self.define_blocks):
            if define_block.type in ['EVENT', 'SCENARIO']:
                define_block.first_if_written = False
                break

    def first_scenario_if_written(self):
        for define_block in reversed(self.define_blocks):
            if define_block.type in ['EVENT', 'SCENARIO', 'SCENARIO (ELSE)']:
                return define_block.first_if_written

        return True

    def set_any_scenario_if_written(self, b):
        for define_block in reversed(self.define_blocks):
            if define_block.type in ['EVENT', 'SCENARIO']:
                define_block.any_if_written = b
                break

    def any_scenario_if_written(self):
        for define_block in reversed(self.define_blocks):
            if define_block.type in ['EVENT', 'SCENARIO']:
                return define_block.any_if_written

        return True

    def new_if_statement_for_else_block(self):
        text = ''
        for define_block in reversed(self.define_blocks):
            if 'SCENARIO' in define_block.type:
                text = 'IF Scenario == '
            else:
                text = 'IF Event == '
            text = '{0}{1}'.format(text, ' | '.join([x.strip(' \t\n!') for x in define_block.name.split('|')]))
            return text
        return text

    def in_output_zone_block(self, output_zone_name=None):
        for define_block in self.define_blocks:
            if define_block.type == 'OUTPUT ZONE':
                if output_zone_name is None:
                    return True
                if isinstance(output_zone_name, str):
                    if output_zone_name.upper() in [x.strip().upper() for x in define_block.name.split('|')]:
                        return True
                elif isinstance(output_zone_name, list):
                    for sn in output_zone_name:
                        if sn.upper() in [x.strip().upper() for x in define_block.name.split('|')]:
                            return True

        return False

    def is_output_zone(self):
        return self.command and 'MODEL OUTPUT ZONE' in self.command

    def specified_output_zones(self):
        return [x.strip() for x in re.split(r'[\s|]', self.value) if x.strip()]

    def is_set_variable(self):
        return self.is_valid() and 'SET VARIABLE' in self.command

    def parse_variable(self):
        return self.command.split('SET VARIABLE')[1].strip(), self.value

    def is_set_command(self):
        return self.is_valid() and not self.is_set_variable() and re.findall('^SET', self.command)

    def retro_fix(self, txt, settings):
        """
        Retroactively fixes a command line that is reading multiple layers on a single line
        and the output format is GPKG. The retroactive fix converts from:

        gis\file.gpkg >> layer1 | gis\file.gpkg >> layer2
        to
        gis\file.gpkg >> layer1 && layer2
        """

        try:
            if '|' not in txt or self.geom_count == 1 or settings.is_grouped_database() or self.is_modify_conveyance():
                return txt

            command, value = [x.strip() for x in txt.split('==')]
            files = [x.strip() for x in value.split('|')]

            if settings.output_gis_format == GIS_GPKG:
                db, _ = get_database_name(files[0])
                layers = sorted([get_database_name(x)[1] for x in files],
                                key=lambda x: {'_R': 0, '_L': 1, '_P': 2, '': -1}[get_geom_suffix(x)])

                return f'{self.prefix}{command} == {db} >> {" && ".join(layers)}'
            else:
                files = sorted(files, key=lambda x: {'_R': 0, '_L': 1, '_P': 2, '': -1}[get_geom_suffix(x)])
                return f'{self.prefix}{command} == {" | ".join(files)}'

        except Exception as e:
            settings.errors = True
            raise MinorConvertException(f'Error: {e}')

    def correct_comment_index(self, command, index_):
        """Fix comment index by turning tabs into spaces."""

        TAB_LEN = 4

        command_len = len(command.strip())
        tab_count = command[command_len:].count('\t')
        if tab_count == 0:
            return

        i = command_len
        for c in command[command_len:]:
            if c == ' ':
                i += 1
            elif c == '\t':
                if i % TAB_LEN != 0:
                    i = (int(i / TAB_LEN) + 1) * TAB_LEN
                else:
                    i += TAB_LEN

        self.comment_index = i

    def strip_command(self, text):
        """
        Strip command into components:

        'Command == Value  ! comment  # comment'
        """

        t = text
        c, v, self.comment = None, None, ''
        if t.strip() and not t[0] in ('!', '#'):
            if '!' in t or '#' in t:
                i = t.index('!') if '!' in t else 9e29
                j = t.index('#') if '#' in t else 9e29
                self.comment_index = k = min(i, j)
                t, self.comment = t[:k], t[k:].strip()
                self.correct_comment_index(t, k)
            if '==' in t:
                c, v = t.split('==', 1)
                v = v.strip()
            else:
                c, v = t, None
            if c.strip():
                self.prefix = re.split(r'\w', c, flags=re.IGNORECASE)[0]  # prefix is leading spaces / tabs
            c = c.strip(' \n\t|')

        return c, v


class EventCommand(Command):
    """Class for handling commands in the TEF."""

    def is_start_define(self):
        """Returns whether command is starting a DEFINE EVENT block."""

        return self.command == 'DEFINE EVENT'

    def is_end_define(self):
        """Returns whether command is ending a DEFINE EVENT black."""

        return self.command == 'END DEFINE'

    def is_event_source(self):
        """Returns whether command is defining the event source."""

        return self.command == 'BC EVENT SOURCE'

    def get_event_source(self):
        """Parse the event source command and return the wildcard and replacement text."""

        if not self.is_valid():
            return None, None

        if not self.is_event_source():
            return None, None

        event_def = [x.strip() for x in str(self.value).split('|', 1)]
        if len(event_def) >= 2:
            return event_def[0], event_def[1]
        elif event_def:
            return event_def[0], None
        else:
            return None, None



def get_commands(control_file, settings):
    """Iterate through a control file and yield parsed Command."""

    if control_file.exists():
        with control_file.open(errors='surrogateescape') as f:
            define_blocks = []
            block_names = []
            block_types = []
            neg_blocks = []
            was_end_define = False
            was_else_if = False
            was_else = False
            for line in f:
                command = Command(line, settings)
                if was_end_define and define_blocks:
                    define_blocks.pop()
                    if neg_blocks:
                        for _ in range(len(set(neg_blocks[-1])) - 1):
                            define_blocks.pop()
                    was_end_define = False
                    if was_else or not was_else_if:
                        block_names.pop()
                        block_types.pop()
                        neg_blocks.pop()
                        was_else = False
                        was_else_if = False
                if command.is_end_define():
                    was_end_define = True
                    if command.is_else_if():
                        was_else_if = True
                    else:
                        was_else_if = False
                if command.is_start_define():
                    define_block_name = command.value
                    define_block_type = command.define_start_type()
                    define_blocks.append(DefineBlock(define_block_type, define_block_name))
                    if define_block_name is not None:
                        neg_block_names = ' | '.join([f'!{x.strip()}' for x in define_block_name.split('|')])
                    else:
                        neg_block_names = ''
                    if command.is_else_if():
                        block_names[-1].append(neg_block_names)
                        block_types[-1].append(define_block_type)
                        neg_blocks[-1].append(neg_block_names)
                    else:
                        block_names.append([neg_block_names])
                        block_types.append([define_block_type])
                        neg_blocks.append([neg_block_names])
                    if was_end_define:
                        define_block = define_blocks.pop(-2)
                        define_blocks[-1].first_if_written = define_block.first_if_written
                        define_blocks[-1].any_if_written = define_block.any_if_written
                        was_end_define = False
                if define_blocks:
                    command.in_define_block = True
                    if command.is_else():
                        was_else = True
                        block = define_blocks.pop()
                        db = [DefineBlock(f'{type_} (ELSE)', name_) for type_, name_ in zip(block_types[-1], block_names[-1])]
                        for x in db:
                            x.any_if_written = block.any_if_written
                            x.first_if_written = block.first_if_written
                        define_blocks.extend(db)
                    elif command.is_else_if():
                        block = define_blocks.pop()
                        define_blocks.extend([DefineBlock(type_, name_) for type_, name_ in zip(block_types[-1][:-1], block_names[-1][:-1])])
                        define_blocks.append(block)
                        block_types[-1].pop(0)
                        block_names[-1].pop(0)

                    command.define_blocks = define_blocks

                yield command


def get_event_commands(control_file, settings):
    """Iterate through event file and yield parsed event Command."""

    in_event = False
    if control_file.exists():
        with control_file.open(errors='surrogateescape') as f:
            for line in f:
                command = EventCommand(line, settings)
                if command.is_end_define():
                    in_event = False
                elif in_event:
                    yield command
                elif command.is_start_define():
                    in_event = True


def get_bc_dbase_sources(bc_dbase, check_if_file=False):
    """Iterate through bc_dbase.csv and yield source files."""

    if bc_dbase.exists():
        with bc_dbase.open(errors='surrogateescape') as f:
            for i, line in enumerate(f):
                try:
                    float(line.split(',')[0])  # rainfall grid csv can just start at first line
                except Exception:
                    if i == 0:  # assume first row (and only first row) is header
                        continue
                inflow = line.split(',')
                if len(inflow) < 2:
                    continue
                if not inflow[1]:
                    continue
                file = inflow[1].strip()
                if check_if_file:
                    try:
                        float(file.strip('\'"'))
                        continue  # is number not file
                    except ValueError:
                        pass
                yield file


def convert_control_file(control_file, settings, type_=None):
    """Convert a TUFLOW Control file from one GIS and GRID format to another."""

    if not control_file.is_relative_to(settings.root_folder):
        if control_file.suffix.upper() == '.TCF':
            raise FatalConvertException(f'{control_file} is not relative to {settings.root_folder}')
        else:
            settings.errors = True
            raise MinorConvertException(f'Error: {control_file} is not relative to {settings.root_folder}')

    # output control file name
    output_file = settings.output_folder / control_file.relative_to(settings.root_folder)
    output_file.parent.mkdir(parents=True, exist_ok=True)
    if settings.verbose == 'high':
        print(f'OUTPUT FILE: {output_file}')

    # copy settings so each control file can have its own
    # IMPORTANT NOTE: control_file and settings_copy.control_file can be different for TRD files!
    settings_copy = settings.copy_settings(control_file, output_file)

    # iterate through control file, test what the command is, and act accordingly
    with output_file.open('w') as f:
        for command in get_commands(control_file, settings_copy):

            if settings_copy.use_scenarios and command.in_scenario_block() and not command.in_scenario_block(settings_copy.scenarios):
                if len(command.command) >= 2 and command.command[:2] == 'IF':
                    command.set_first_scenario_if_written(False)  # set this to false so if an 'else if' is valid, replace with 'if' when written to new control file
                elif len(command.command) >= 6 and command.command[:6] == 'END IF' and command.any_scenario_if_written():
                    f.write(command.make_new_text(settings_copy))
                continue
            elif settings_copy.use_scenarios and command.in_scenario_block():
                command.set_any_scenario_if_written(True)

            # insert spatial database command at top of file (just before first valid command)
            if command.is_valid() and not settings_copy.written_spatial_database_header:
                f.write(f'Spatial Database == {settings_copy.spatial_database}\n\n')
                settings_copy.written_spatial_database_header = True

            # GIS FORMAT ==
            if command.is_gis_format():
                if settings.verbose == 'high':
                    print(f'format GIS: {command.command} == {command.value_orig}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error writing new GIS FORMAT command to {control_file}: {e}')

                if not settings_copy.grid_format_command_exists:
                    f.write(f'GRID Format == {gdal_format_2_ext(settings_copy.output_grid_format).upper()[1:]}\n')

            # READ GIS [..] ==
            elif command.is_read_gis():
                if settings.verbose == 'high':
                    print(f'Read GIS: {command.command} == {command.value_orig}')

                new_command = ''
                copy_count = None
                for type_ in command.iter_geom(settings_copy):
                    if type_ == 'GRID':
                        geom = None
                    elif type_ == 'VALUE':
                        geom = None
                    else:
                        geom = type_

                    try:
                        new_command = f'{new_command}{command.make_new_text(settings_copy, geom)}'
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new READ GIS command to {control_file} - {command.command_orig}: {e}')
                        continue

                    if not command.can_use_multiple_gis_inputs(control_file):
                        new_command = command.re_add_comments(new_command)
                        command.iter_geom_index = -1

                    if type_ == 'VALUE':
                        continue  # don't need to copy any layers

                    # expand glob
                    for src_file in command.iter_files(settings_copy):
                        if copy_count is None:
                            copy_count = 1
                        else:
                            copy_count += 1

                        try:
                            dest_file = command.get_gis_dest_file(src_file, settings_copy, geom)
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: Could not determine destination to copy file for {src_file}')
                            continue

                        try:
                            if type_ == 'GRID':
                                gdal_copy(src_file, dest_file, settings_copy.projection_wkt, settings_copy.force_projection)
                            else:
                                ogr_copy(src_file, dest_file, geom, settings_copy)
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: converting {src_file} to {dest_file}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

                try:
                    new_command_2 = command.retro_fix(new_command, settings_copy)
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: Problem encountered correcting single line - multiple layer READ GIS command {new_command}: {e}')

                if command.can_use_multiple_gis_inputs(control_file):
                    new_command_2 = command.re_add_comments(new_command_2)

                f.write(new_command_2)

            # READ GRID [..] ==
            elif command.is_read_grid():
                if settings.verbose == 'high':
                    print(f'Read GRID: {command.command} == {command.value_orig}')

                if command.is_rainfall_grid_nc():  # don't convert or parse this one
                    try:
                        f.write(command.make_new_text(settings))
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new READ GRID NC command {command.command_orig}: {e}')
                        continue

                    # glob is expanded in 'copy_file' routine
                    try:
                        dest = copy_file(control_file, TuflowPath(command.value), output_file, settings_copy.wildcards)
                        if dest is None:
                            settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: copying NC grid: {e}')

                    continue

                new_command = ''
                copy_count = None
                for type_ in command.iter_grid(settings_copy):  # consider READ GRID ZPTS == GRID | POLYGON
                    geom = None if type_ == 'GRID' else type_

                    try:
                        new_command = f'{new_command}{command.make_new_text(settings_copy, geom)}'
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new READ GRID command to {control_file} - {command.command_orig}: {e}')
                        continue

                    # expand glob
                    for src_file in command.iter_files(settings_copy):
                        if copy_count is None:
                            copy_count = 1
                        else:
                            copy_count += 1

                        try:
                            dest_file = command.get_gis_dest_file(src_file, settings_copy, geom)
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: Could not determine destination to copy file for {src_file}')
                            continue

                        if type_ == 'GRID':
                            try:
                                gdal_copy(src_file, dest_file, settings_copy.projection_wkt, settings_copy.force_projection)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: converting {src_file} to {dest_file}\n{e}')
                        else:
                            try:
                                ogr_copy(src_file, dest_file, geom)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: converting {src_file} to {dest_file}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

                new_command = command.re_add_comments(new_command)

                f.write(new_command)

            # READ TIN [..] ==
            elif command.is_read_tin():
                if settings.verbose == 'high':
                    print(f'Read TIN: {command.command} == {command.value_orig}')

                new_command = ''
                copy_count = None
                for type_ in command.iter_tin(settings_copy):  # consider READ TIN ZPTS == TIN | POLYGON
                    geom = None if type_ == 'TIN' else type_

                    try:
                        new_command = f'{new_command}{command.make_new_text(settings_copy, geom)}'
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new READ TIN command to {control_file} - {command.command_orig}: {e}')
                        continue

                    # expand glob
                    for src_file in command.iter_files(settings_copy):
                        if copy_count is None:
                            copy_count = 1
                        else:
                            copy_count += 1

                        if type_ == 'TIN':
                            try:
                                rp = os.path.relpath(src_file, control_file.parent.resolve())
                                file_dest = (output_file.parent / rp).resolve()
                                copy_file2(src_file, file_dest)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: converting {src_file} to {dest_file}\n{e}')
                        else:
                            try:
                                dest_file = command.get_gis_dest_file(src_file, settings_copy, geom)
                                ogr_copy(src_file, dest_file, geom)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: converting {src_file} to {dest_file}')

                if copy_count is None:
                    settings_copy.no_files_copied.append(
                        (settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

                new_command = command.re_add_comments(new_command)

                f.write(new_command)

            # [MI | SHP | GPKG] PROJECTION ==
            elif command.is_read_projection():
                if settings.verbose == 'high':
                    print(f'Read PROJECTION: {command.command} == {command.value_orig}')

                # copy original projection for reference
                settings_copy.output_gis_format = ogr_format(command.value)  # temporarily change output gis format

                # check if converting GPKG to GPKG and changing output profile - if so then just write new command, don't copy original proj command
                if 'GPKG' in command.command and (
                        (not re.findall(r'\.gpkg', str(command.value_orig), flags=re.IGNORECASE) and not settings_copy.output_profile != SEPARATE)
                        or
                        (re.findall(r'\.gpkg', str(command.value_orig), flags=re.IGNORECASE) and settings_copy.output_profile != SEPARATE)
                ):
                    try:
                        f.write(command.make_new_text(settings_copy))
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new PROJECTION command to {control_file} - {command.command_orig}: {e}')
                else:
                    f.write(str(TuflowPath(command.original_text)))
                    if not settings_copy.written_projection and command.original_text and command.original_text[-1] != '\n':
                        f.write('\n')

                # expand glob
                copy_count = None
                for copy_count, src_file in enumerate(command.iter_files(settings_copy)):
                    try:
                        dest_file = command.get_gis_dest_file(src_file, settings_copy)
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: Could not determine destination to copy file for {src_file}')
                        continue

                    try:
                        if settings_copy.output_gis_format == settings.output_gis_format:
                            ogr_copy(src_file, dest_file, settings=settings_copy)
                        else:
                            ogr_copy(src_file, dest_file)
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: converting {src_file} to {dest_file}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

                settings_copy.output_gis_format = settings.output_gis_format  # reset output gis format

                # write out converted projection file - if not already written
                if not settings_copy.written_projection:
                    try:
                        f.write(command.make_new_text(settings_copy))
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: writing new PROJECTION command to {control_file} - {command.command_orig}: {e}')

                    settings_copy.written_projection = True

                    # expand glob
                    for src_file in command.iter_files(settings_copy):
                        try:
                            ogr_copy(src_file, command.get_gis_dest_file(src_file, settings_copy))
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: converting {src_file} to {dest_file}')

                if not settings_copy.written_tif_projection and settings.tif_projection_path is not None:
                    f.write(f'TIF Projection == {settings.tif_projection_path}\n')
                    settings_copy.written_tif_projection = True

            # SPATIAL DATABASE ==
            elif command.is_spatial_database_command():
                if settings.verbose == 'high':
                    print(f'Set SPATIAL DATABASE: {command.command} == {command.value_orig}')

                settings_copy.process_spatial_database_command(command.value)

            # CONTROL FILE
            elif command.is_control_file():
                if settings.verbose == 'high':
                    print(f'Control FILE: {command.command} == {command.value_orig}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new CONTROL FILE command to {control_file} - {command.command_orig}: {e}')

                if command.is_quadtree_single_level():
                    continue

                # expand glob
                copy_count = None
                for copy_count, cf in enumerate(command.iter_files(settings_copy)):
                    cf = cf.resolve()

                    try:
                        convert_control_file(cf, settings_copy)
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: Could not convert control file {cf}: {e}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

            # BC DATABASE ==
            elif command.is_bc_dbase_file():
                if settings.verbose == 'high':
                    print(f'BC DATABASE: {command.command} == {command.value_orig}')

                in_bc_dbase = (settings_copy.control_file.parent / command.value).resolve()

                # glob is expanded in 'copy_file' routine
                # copy bc_dbase.csv
                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new BC DATABASE command to {control_file} - {command.command_orig}: {e}')

                something_copied = False
                for dbase in command.iter_files(settings_copy):
                    try:
                        rp = os.path.relpath(dbase, control_file.parent.resolve())
                        file_dest = (output_file.parent / rp).resolve()
                        copy_file2(dbase, file_dest)
                        something_copied = True
                    except Exception:
                        settings_copy.errors = True
                        print(f'Error: Could not copy BC DATABASE file {dbase}')
                        continue

                    for source in get_bc_dbase_sources(dbase):
                        try:
                            dest = copy_file(dbase, source, file_dest, settings_copy.wildcards)
                            if dest is None:
                                settings_copy.no_files_copied.append(('BC DBASE Source', source))
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: Could not copy INFLOW file {source}')
                if not something_copied:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

            # PIT INLET DATABASE == | DEPTH DISCHARGE DATABASE ==
            elif command.is_pit_inlet_dbase_file():
                if settings.verbose == 'high':
                    print(f'Pit DATABASE: {command.command} == {command.value_orig}')

                in_pit_dbase = (settings_copy.control_file.parent / command.value).resolve()

                # glob is expanded in 'copy_file' routine
                # copy bc_dbase.csv
                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new PIT INLET DATABASE command to {control_file} - {command.command_orig}: {e}')

                something_copied = False
                for dbase in command.iter_files(settings_copy):
                    try:
                        rp = os.path.relpath(dbase, control_file.parent.resolve())
                        file_dest = (output_file.parent / rp).resolve()
                        copy_file2(dbase, file_dest)
                        something_copied = True
                    except Exception:
                        settings_copy.errors = True
                        print(f'Error: Could not copy PIT DATABASE file {dbase}')
                        continue

                    for source in get_bc_dbase_sources(dbase):
                        try:
                            dest = copy_file(dbase, source, file_dest, settings_copy.wildcards)
                            if dest is None:
                                settings_copy.no_files_copied.append(('PIT DBASE Source', source))
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: Could not copy CURVE file {source}')
                if not something_copied:
                    settings_copy.no_files_copied.append(
                        (settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

            # READ GRID RF == CSV
            elif command.is_rainfall_grid_csv():
                if settings.verbose == 'high':
                    print(f'RAINFALL GRID CSV: {command.command} == {command.value_orig}')

                # copy csv and command
                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new READ GRID RF (CSV) command to {control_file} - {command.command_orig}: {e}')

                # expand glob
                copy_count = None
                for src_file in command.iter_files(settings_copy):
                    # copy src_file
                    try:
                        rel_path = os.path.relpath(src_file, settings_copy.control_file.parent)
                        dest_file = (output_file.parent / rel_path).resolve()
                    except ValueError:
                        dest_file = src_file

                    if dest_file != src_file:
                        try:
                            copy_rainfall_grid_csv(src_file, dest_file, settings_copy)
                        except MinorConvertException as e:
                            settings_copy.errors = True
                            print(f'Error: Could not copy READ GRID RF CSV file {source}')

                    for rf_grid in get_bc_dbase_sources(src_file):
                        # expand glob
                        pattern = globify(rf_grid, settings_copy.wildcards)
                        for src_file_grid in src_file.parent.glob(pattern):
                            if copy_count is None:
                                copy_count = 1
                            else:
                                copy_count += 1

                            src_file_grid = tuflowify_value(src_file_grid, settings_copy)

                            try:
                                dest_file_grid = command.get_gis_dest_file(src_file_grid, settings_copy)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: Could not determine destination to copy file for {src_file_grid}')
                                continue

                            try:
                                gdal_copy(src_file_grid, dest_file_grid, settings_copy.projection_wkt, settings_copy.force_projection)
                            except MinorConvertException as e:
                                settings_copy.errors = True
                                print(f'Error: converting {src_file_grid} to {dest_file_grid}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

            # READ FILE ==
            elif command.is_read_file():
                if settings.verbose == 'high':
                    print(f'Read FILE: {command.command} == {command.value_orig}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new CONTROL FILE command to {control_file} - {command.command_orig}: {e}')

                ext = control_file.suffix.upper()

                # expand glob
                copy_count = None
                for copy_count, cf in enumerate(command.iter_files(settings_copy)):
                    cf = cf.resolve()

                    try:
                        convert_control_file(cf, settings_copy, ext)
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: Could not convert control file {cf}: {e}')

                if copy_count is None:
                    settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))

            elif command.is_mat_csv():
                if settings.verbose == 'high':
                    print(f'Read Material FILE: {command.command} == {command.value_orig}')

                in_mat_file = (settings_copy.control_file.parent / command.value).resolve()

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new READ FILE command to {control_file} - {command.command_orig}: {e}')

                try:
                    out_mat_file = copy_file(control_file, TuflowPath(command.value), output_file, settings_copy.wildcards)
                    if out_mat_file is None:
                        settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: Could not copy PIT INLET DATABASE file {command.value}')

                for source in get_bc_dbase_sources(in_mat_file, check_if_file=True):
                    try:
                        dest = copy_file(in_mat_file, source, out_mat_file, settings_copy.wildcards)
                        if dest is None:
                            settings_copy.no_files_copied.append(('MAT', command.original_text.strip()))
                    except MinorConvertException as e:
                        settings_copy.errors = True
                        print(f'Error: Could not copy mannings n file {source}')

            #  any command that has a value that is a file and hasn't been processed yet
            elif command.is_value_a_file():
                if settings.verbose == 'high':
                    print(f'Read Other FILE: {command.command} == {command.value_orig}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new READ FILE command to {control_file} - {command.command_orig}: {e}')

                event_command = EventCommand(command.original_text, settings_copy)
                if event_command.is_event_source():
                    continue

                # glob is expanded in 'copy_file' routine
                try:
                    dest = copy_file(control_file, TuflowPath(command.value), output_file, settings_copy.wildcards)
                    if dest is None:
                        settings_copy.no_files_copied.append((settings_copy.control_file.suffix[1:].upper(), command.original_text.strip()))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: Could not copy READ FILE file {command.value}')

            # CONTAINS COMMAND
            elif command.is_valid():
                if settings.verbose == 'high':
                    if command.value_orig is not None:
                        print(f'TUFLOW COMMAND: {command.command_orig} == {command.value_orig}')
                    else:
                        print(f'TUFLOW COMMAND: {command.command_orig}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException as e:
                    settings_copy.errors = True
                    print(f'Error: writing new COMMAND to {control_file} - {command.command_orig}: {e}')

            # COMMENT or BLANK line
            else:
                if settings.verbose == 'high':
                    if command.original_text.strip():
                        print(f'Comment: {command.original_text}')

                try:
                    f.write(command.make_new_text(settings_copy))
                except MinorConvertException:
                    pass

    if settings_copy.errors:
        settings.errors = True
    if settings_copy.warnings:
        settings.warnings = True
