from __future__ import annotations

from collections import defaultdict
from datetime import datetime
from enum import Enum
from itertools import chain
from typing import Any, Sequence, Union

import shlex
try:
    import pandas as pd
except ImportError:
    pd = None
import warnings

from tuflow.tuflow_swmm.swmm_processing_feedback import ScreenProcessingFeedback


class SectionType(Enum):
    NO_KEYWORDS = 0  # No keywords defining different options
    KEYWORDS = 1
    GEOMETRY = 2
    WIDE = 3  # Similar to keywords but options are converted from multi-line to wide format


class GeometryType(Enum):
    NODES = 0
    CONDUITS = 1
    LINKS = 2
    INLETS = 3
    SUBCATCHMENTS = 4
    MISC = 5


# These are used to build the COORDINATES dataframe
primary_node_sections = {
    'Junctions',
    'Outfalls',
    'Dividers',
    # 'Inflows', This is a bc on existing nodes not a primary node field
    'Storage',
}

primary_link_sections = {
    'Conduits',
    'Pumps',
    'Weirs',
    'Orifices',
    'Outlets',
}

# Section merges: list of tuples (prefix, table, merge_col1, merge_col2)
sections_to_append = {
    'Conduits': [
        ('xsec', 'XSections', 'Name', 'Link'),
        ('losses', 'Losses', 'Name', 'Link'),
    ],
    'Orifices': [
        ('xsec', 'XSections', 'Name', 'Link'),
    ],
    'Weirs': [
        ('xsec', 'XSections', 'Name', 'Link'),
    ],
    'Subcatchments': [
        ('Subareas', 'Subareas', 'Name', 'Subcatchment'),
        ('Infiltration', 'Infiltration', 'Name', 'Subcatchment'),
    ]
}

# Tables that have the tag option and the object type
tag_table_type = {
    'Conduits': 'Link',
    'Orifices': 'Link',
    'Outlets': 'Link',
    'Pumps': 'Link',
    'Weirs': 'Link',
    'Dividers': 'Node',
    'Junctions': 'Node',
    'Outfalls': 'Node',
    'Storage': 'Node',
    'Subcatchments': 'Subcatch',
    'Raingages': 'RainGage',
}

# These are the tables that support descriptions
tables_with_description = {
    'Raingages',
    'Subcatchments',
    'Landuses',
    'Junctions',
    'Outfalls',
    'Dividers',
    'Storage',
    'Conduits',
    'Pumps',
    'Weirs',
    'Orifices',
    'Outlets',
    'Curves',
    'Timeseries',
    'Patterns',
}

class SwmmSection:
    name: str

    def __init__(self,
                 name: str,
                 prefix: str,
                 geomtype: Union[GeometryType, None],
                 sec_type: SectionType,
                 cols_common_start: Sequence[tuple[str, Any]],
                 cols_keywords: dict[str, Sequence[tuple[str, Any]]] = None,
                 keyword_col: Union[int, None] = None,
                 cols_common_end: Sequence[tuple[str, Any]] | None = None):
        self.name = name
        self.prefix = prefix
        self.geometry = geomtype
        self.section_type = sec_type
        self.cols_common_start = cols_common_start
        self.cols_keywords = cols_keywords
        self.keyword_col = keyword_col  # 0 based
        self.cols_common_end = cols_common_end
        self.custom_mapping = False  # Don't pass as argument set separately because it is rare

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.__str__()

    def long_name(self):
        return f'{self.prefix}--{self.name}' if self.prefix != '' else self.name

    def get_all_columns(self):
        all_cols = {x[0]: x[1] for x in self.cols_common_start}

        if self.section_type == SectionType.KEYWORDS:
            for keyword_cols in filter(lambda z: z is not None, self.cols_keywords.values()):
                all_cols.update({x[0]: x[1] for x in filter(lambda y: y is not None, keyword_cols)})

            # Add common columns at the end
            if self.cols_common_end:
                all_cols.update({x[0]: x[1] for x in self.cols_common_end})

        return all_cols

    def get_all_column_types(self):
        return self.get_all_columns().values()

    def get_all_column_names(self):
        return self.get_all_columns().keys()

    def convert_gpkg_df_to_swmm(self, df, feedback=ScreenProcessingFeedback()):
        if pd is None:
            raise ImportError("pandas is required to use SwmmSection.convert_gpkg_df_to_swmm()")
        swmm_cols = []

        def _quote_if_needed(path: str) -> str:
            path_str = str(path)
            if ' ' in path_str and not (path_str.startswith('"') and path_str.endswith('"')):
                return f'"{path_str}"'
            return path_str

        # We want nullable integers and have to use 'Int64' (see
        # https://pandas.pydata.org/pandas-docs/stable/user_guide/integer_na.html)
        for col, coltype in self.cols_common_start:
            if coltype == int:
                df[col] = df[col].astype('float').astype('Int64')

        # Coverages and loadings work fine using the non-custom method for this direction
        if self.custom_mapping and self.name.upper() not in ["COVERAGES", "LOADINGS", "PATTERNS", "INFILTRATION"]:
            # Temp
            swmm_cols = [x[0] for x in self.cols_common_start]
            if 'Description' in df.columns:
                swmm_cols.append('Description')

            if self.name.upper() == 'TRANSECTS':
                nc_cols = ['Name', 'Nleft', 'Nright', 'Nchanl']
                df_nc = df[nc_cols].copy(deep=True)
                df_nc['Datatype'] = 'NC'
                df_nc = df_nc[['Datatype'] + nc_cols]

                df['Datatype'] = 'X1'
                df['Name2'] = df['Name']
                df['Nsta'] = 'Nsta'  # Will be replaced layer
                df['dummy1'] = '0'
                df['dummy2'] = '0'
                df['dummy3'] = '0'
                df_x1 = df[[
                    'Datatype',
                    'Name',
                    'Name2',
                    'Nsta',
                    'Xleft',
                    'Xright',
                    'dummy1',
                    'dummy2',
                    'dummy3',
                    'Lfactor',
                    'Wfactor',
                    'Eoffset']]
                # We need a name, datatype, and param columns
                df_nc.columns = ['Datatype', 'Name'] + [f'Param{x + 1}' for x, _ in enumerate(df_nc.columns[2:])]
                df_x1.columns = ['Datatype', 'Name'] + [f'Param{x + 1}' for x, _ in enumerate(df_x1.columns[2:])]
                df_nc = df_nc.astype(str)
                df_x1 = df_x1.astype(str)
                df_out = pd.concat([df_nc, df_x1]).sort_values('Name', kind='stable')

                df = df_out
            elif self.name.upper() == 'TRANSECTS_COORDS':
                df['Datatype'] = 'GR'
                df = df[['Datatype', 'Name', 'Elev', 'Station']]
                df = df.rename(columns={
                    'Elev': 'Param1',
                    'Station': 'Param2',
                })
            elif self.name.upper() == 'INLETS':
                # This may end up being split between two lines
                swmm_cols = ['Name', 'Type', 'Param1', 'Param2', 'Param3', 'Param4', 'Param5']

                combo_rows = df['Type'].str.upper() == 'COMBINATION'
                # Duplicate combo-rows and assign them to 'CURB'
                df_combo_curb = df[combo_rows].copy(deep=True)
                df_combo_curb['Type'] = 'CURB'
                # Set original combo rows to GRATE and merge curb in
                df.loc[combo_rows, 'Type'] = 'GRATE'

                df = pd.concat([df, df_combo_curb], axis=0)
                # print(df)

                grate_rows = (df['Type'].str.upper() == 'GRATE') | (df['Type'].str.upper() == 'DROP_GRATE')
                curb_rows = (df['Type'].str.upper() == 'CURB') | (df['Type'].str.upper() == 'DROP_CURB')
                slotted_rows = df['Type'].str.upper() == 'SLOTTED'
                custom_rows = df['Type'].str.upper() == 'CUSTOM'

                # initialize Param1-5 columns
                for col in swmm_cols[2:]:
                    df[col] = None

                # Copy values from appropriate columns
                df.loc[grate_rows, 'Param1'] = df.loc[grate_rows, 'Grate_Length']
                df.loc[grate_rows, 'Param2'] = df.loc[grate_rows, 'Grate_Width']
                df.loc[grate_rows, 'Param3'] = df.loc[grate_rows, 'Grate_Type']
                df.loc[grate_rows, 'Param4'] = df.loc[grate_rows, 'Grate_Aopen']
                df.loc[grate_rows, 'Param5'] = df.loc[grate_rows, 'Grate_vsplash']

                df.loc[curb_rows, 'Param1'] = df.loc[curb_rows, 'Curb_Length']
                df.loc[curb_rows, 'Param2'] = df.loc[curb_rows, 'Curb_Height']
                df.loc[curb_rows, 'Param3'] = df.loc[curb_rows, 'Curb_Throat'].astype(str)

                df.loc[slotted_rows, 'Param1'] = df.loc[slotted_rows, 'Slotted_Length']
                df.loc[slotted_rows, 'Param2'] = df.loc[slotted_rows, 'Slotted_Width']

                df.loc[custom_rows, 'Param1'] = df.loc[custom_rows, 'Custom_Curve']

                df = df[swmm_cols]

            elif self.name == 'Timeseries':
                swmm_cols = ['Name', 'Date', 'Time', 'Value']
                if 'Description' in df.columns:
                    swmm_cols.append('Description')
                # if Fname is filled in we are reading from a file date->FILE and Time->Filename
                df['Date'] = df['Date'].astype(str)
                no_filename = (~df['Fname'].isnull()) & (df['Fname'] != '')
                df.loc[no_filename, 'Date'] = 'FILE'
                df['Time'] = df['Time'].astype(str)
                df.loc[no_filename, 'Time'] = df.loc[no_filename, 'Fname'].apply(_quote_if_needed)
                df['Value'] = df['Value'].astype(str).replace({'nan': '', '<NA': ''})

                df = df[swmm_cols]

            elif self.name == 'Curves':
                # Just replace nan with spaces and it will be a valid file
                df = df[swmm_cols].astype(str).replace({'nan': '', '<NA>': ''})
            else:
                raise ValueError("Need to finish for case")
        elif self.section_type == SectionType.WIDE:
            # for common columns nothing needed except convert to text
            swmm_cols = [x[0] for x in self.cols_common_start]

            # for variable columns and end columns they will be reduced to a few with generic names (Param1, Param2)
            # convert to nullable integers
            for k, v in self.cols_keywords.items():
                if v:
                    for col, coltype in v:
                        if coltype == int:
                            df[col] = df[col].astype('Int64')

            # We need to stack the dataframe
            df_lines = []
            for prefix, cols in self.cols_keywords.items():
                cols = swmm_cols + [f'{prefix}_{col[0]}'.lstrip('_') for col in cols]
                df_line = df[cols].copy(deep=True)
                # Need to insert prefix if it is not ''
                old_cols = df_line.columns
                if prefix != '':
                    df_line.loc[:, 'Prefix'] = prefix.upper()
                    new_cols = [old_cols[0]] + ['Prefix'] + list(old_cols[1:])
                    df_line = df_line[new_cols]
                df_line.columns = [df_line.columns[0]] + [f'Param_{i + 1}' for i, _ in enumerate(df_line.columns[1:])]
                df_line = df_line.reindex(sorted(df_line.columns), axis=1)

                # Remove blank lines
                if prefix != '':
                    df_line = df_line.dropna(how='all', subset=df_line.columns[2:])

                df_lines.append(df_line)

            df = pd.concat(df_lines)
        else:
            # for common columns nothing needed except convert to text
            swmm_cols = [x[0] for x in self.cols_common_start]

            # for variable columns and end columns they will be reduced to a few with generic names (Param1, Param2)
            if self.section_type == SectionType.KEYWORDS:
                # convert ot nullable integers
                for k, v in self.cols_keywords.items():
                    if v:
                        for col, coltype in v:
                            if coltype == int:
                                df[col] = df[col].astype('Int64')

                num_variable_columns = max([len(x) for x in self.cols_keywords.values() if x is not None])
                variable_cols = [f'Param{x}' for x in range(1, num_variable_columns + 1)]
                swmm_cols.extend(variable_cols)
                if self.cols_common_end:
                    swmm_cols.extend([x[0] for x in self.cols_common_end])

                df[variable_cols] = None

                for k, v in self.cols_keywords.items():
                    param_num = 1
                    col_keyword_name = df.columns[self.keyword_col]
                    if k == 'Default':
                        keyword_rows = ~df[col_keyword_name].isin(self.cols_keywords.keys())
                    else:
                        keyword_rows = df[col_keyword_name] == k
                    if v:
                        for gpkg_col in v:
                            with warnings.catch_warnings():
                                warnings.simplefilter(action='ignore', category=FutureWarning)
                                df.loc[keyword_rows, f'Param{param_num}'] = \
                                    df.loc[keyword_rows, gpkg_col[0]]
                            param_num = param_num + 1

            # Add tag column if it exists
            if 'Tag' in df.columns:
                swmm_cols.append('Tag')

            # Keep description column if it exists
            if 'Description' in df.columns:
                swmm_cols.append('Description')

            df = df[swmm_cols].astype(str).replace({'nan': '', '<NA>': ''})
            # df = df[swmm_cols].applymap(lambda x: str(x) if pd.isnull(x) else None)

        # If we are the options table we may need to convert date formats
        if self.name == 'Options':
            date_fields = ['START_DATE', 'REPORT_START_DATE', 'END_DATE']
            for date_field in date_fields:
                if len(df[df['Option'] == date_field]) > 0:
                    date_text = df.loc[df['Option'] == date_field, 'Value'].iloc[0]
                    try:
                        conv_date = datetime.fromisoformat(date_text).date()
                        df.loc[df['Option'] == date_field, 'Value'] = conv_date.strftime('%m/%d/%Y')
                    except ValueError:
                        # Wasn't an isoformat nothing to do
                        pass

        return df

    def convert_text_to_dataframe(self, text_array, extra_dfs, feedback=ScreenProcessingFeedback()):

        def _tokenize(line: str, maxsplit: int | None = None) -> list[str]:
            tokens = shlex.split(line, posix=False)
            if maxsplit is not None and len(tokens) > maxsplit + 1:
                tokens = tokens[:maxsplit] + [' '.join(tokens[maxsplit:])]
            return [t.strip() for t in tokens]

        # Strip out any description statements (use a single ;) and store
        descriptions = {}
        curr_descript = ''
        out_lines = []
        for line in text_array:
            if line.startswith(';'):
                curr_descript = line[1:]
            else:
                tokens = _tokenize(line)
                start_word = tokens[0] if tokens else ''
                if curr_descript != '':
                    descriptions[start_word] = curr_descript
                    curr_descript = ''
                out_lines.append(line)

        text_array = out_lines

        df = None
        if self.custom_mapping:
            if self.name.upper() == 'TRANSECTS':
                section_text_values = [_tokenize(x) for x in text_array]

                section_data = {
                    'Name': [],
                    'Xleft': [],
                    'Xright': [],
                    'Lfactor': [],
                    'Wfactor': [],
                    'Eoffset': [],
                    'Nleft': [],
                    'Nright': [],
                    'Nchanl': [],
                }
                section_data_coords = {
                    'Name': [],
                    'Elev': [],
                    'Station': [],
                }
                curr_name = None
                curr_n_values = None
                curr_elev_stations = []
                for row in section_text_values:
                    data_type = row[0]
                    if data_type.upper() == 'NC':
                        curr_n_values = [float(x) for x in row[1:4]]
                    elif data_type.upper() == 'X1':
                        curr_name = row[1]
                        section_data['Name'].append(curr_name)
                        # Skip Nsta because we are not going to store
                        section_data['Xleft'].append(row[3])
                        section_data['Xright'].append(row[4])
                        # Skip unused 0 0 0
                        lfactor = 0.
                        wfactor = 0.
                        eoffset = 0.
                        try:
                            lfactor = row[8]
                            wfactor = row[9]
                            eoffset = row[10]
                        except:
                            pass  # defaulted
                        section_data['Lfactor'].append(lfactor)
                        section_data['Wfactor'].append(wfactor)
                        section_data['Eoffset'].append(eoffset)
                        # Add the current n values
                        section_data['Nleft'].append(curr_n_values[0])
                        section_data['Nright'].append(curr_n_values[1])
                        section_data['Nchanl'].append(curr_n_values[2])
                    elif data_type.upper() == 'GR':
                        # starting with row1 we have pairs of elev/station
                        for elev, station in zip(row[1::2], row[2::2]):
                            section_data_coords['Name'].append(curr_name)
                            section_data_coords['Elev'].append(elev)
                            section_data_coords['Station'].append(station)
                    # Create the dataframe
                    df = pd.DataFrame(section_data)
                    # Create the coords dataframe
                    extra_dfs['Transects_coords'] = pd.DataFrame(section_data_coords)

            if self.name == 'Patterns':
                section_data = {x[0]: list() for x in self.cols_common_start}
                section_text_values = [_tokenize(x) for x in text_array]

                curr_name = None
                curr_interval = None
                curr_factors = []
                for row in section_text_values:
                    row_vals = {x[0]: None for x in self.cols_common_start}

                    name = row[0]

                    if curr_name is not None:
                        if name == curr_name:
                            # Continuing from previous addd to factors
                            for icol in range(1, len(row)):
                                curr_factors.append(float(row[icol]))
                        else:
                            # We finished an entry
                            row_vals['Name'] = curr_name
                            row_vals['Interval'] = curr_interval
                            for ifact in range(0, len(curr_factors)):
                                row_vals[f'Factor{ifact + 1}'] = curr_factors[ifact]

                            # Add row to the table
                            for row_col, row_val in row_vals.items():
                                section_data[row_col].append(row_val)

                            # Start next entry
                            curr_name = name
                            curr_interval = row[1]
                            curr_factors = []

                            for icol in range(2, len(row)):
                                curr_factors.append(float(row[icol]))
                    else:  # No current entry
                        # Start next entry
                        curr_name = name
                        curr_interval = row[1]
                        curr_factors = []

                        for icol in range(2, len(row)):
                            curr_factors.append(float(row[icol]))

                # Do the final entry
                row_vals = {x[0]: None for x in self.cols_common_start}
                row_vals['Name'] = curr_name
                row_vals['Interval'] = curr_interval
                for ifact in range(0, len(curr_factors)):
                    row_vals[f'Factor{ifact + 1}'] = curr_factors[ifact]
                for row_col, row_val in row_vals.items():
                    section_data[row_col].append(row_val)

                # Create the dataframe
                df = pd.DataFrame(section_data)

            elif self.name == 'Timeseries':
                section_data = {x[0]: list() for x in self.cols_common_start}
                section_text_values = [_tokenize(x) for x in text_array]

                for row in section_text_values:
                    row_vals = {x[0]: None for x in self.cols_common_start}

                    row_vals['Name'] = row[0]

                    # See if we are a file
                    if row[1].upper() == 'FILE':
                        # FILE entries only have the file name, but still need to be recorded
                        row_vals['Fname'] = row[2] if len(row) > 2 else ''
                        row_vals['Date'] = ''
                        row_vals['Time'] = ''
                        row_vals['Value'] = ''

                        for row_col, row_val in row_vals.items():
                            section_data[row_col].append(row_val)
                    else:
                        row_vals['Fname'] = ''
                        # See if the next entry is a date
                        date = None
                        try:
                            date_formats_to_try = [
                                '%m-%d-%Y',
                                '%m/%d/%Y',
                                '%d%b%Y',
                                '%-d%b%Y',
                            ]
                            for date_format in date_formats_to_try:
                                try:
                                    date = datetime.strptime(row[1], date_format)
                                    if date is not None:
                                        break
                                except:
                                    # Must not have been a date field will still be None
                                    pass
                            curr_date = date
                        except BaseException:
                            # print(e)
                            pass

                        # If we have a date the time should be in the next column
                        timecol = 1
                        if date:
                            row_vals['Date'] = row[1]
                            timecol = 2

                        # We can have pairs of time/value on lines that we will write as multiple entries
                        for icol in range(timecol, len(row) - 1, 2):
                            row_vals['Time'] = row[icol]
                            row_vals['Value'] = float(row[icol + 1])

                            # Add row to the table
                            for row_col, row_val in row_vals.items():
                                section_data[row_col].append(row_val)
                df = pd.DataFrame(section_data)

            if self.name == 'Coverages' or self.name == 'Loadings':
                # After the first column can be any number of pairs of Landuse/Percent add to different rows
                section_data = {x[0]: list() for x in self.cols_common_start}
                section_text_values = [_tokenize(x) for x in text_array]

                for row in section_text_values:
                    # print(row)
                    subcatchment = str(row[0])
                    for i in range(1, len(row) - 1):
                        section_data[self.cols_common_start[0][0]].append(str(subcatchment))
                        section_data[self.cols_common_start[1][0]].append(str(row[i]))
                        section_data[self.cols_common_start[2][0]].append(float(row[i + 1]))

                df = pd.DataFrame(section_data)

            if self.name == 'Curves':
                # Read curves where First row with a new curve has a inlet_type
                # row1 may have first curve points x,y
                # after row1 there is no inlet_type just x and y values
                section_text_values = [_tokenize(x) for x in text_array]

                curves_data = {x[0]: list() for x in self.cols_common_start}

                curr_curve = None
                curr_type = None
                first_points = True

                for row in section_text_values:
                    # print(row)
                    curve_name = row[0]

                    coord_col = 1
                    if curve_name != curr_curve:
                        # New curve
                        curr_curve = curve_name
                        curr_type = row[1]
                        coord_col = 2
                        first_points = True

                    # Read pairs of coordinates and add to curve
                    while coord_col + 1 < len(row):
                        x = float(row[coord_col])
                        y = float(row[coord_col + 1])
                        # print(x)
                        # print(y)

                        curves_data['Name'].append(curve_name)
                        curves_data['Type'].append(curr_type if first_points else None)
                        curves_data['xval'].append(x)
                        curves_data['yval'].append(y)
                        first_points = False

                        coord_col += 2
                df = pd.DataFrame(curves_data)

            elif self.name == 'Inlets':  # Use wide format and use COMBINATION for Curb/Grate combination
                section_text_values = [_tokenize(x) for x in text_array]
                inlet_cols_list = [x[0] for x in self.cols_common_start]
                # print(inlet_cols_list)
                inlet_data = {x: list() for x in inlet_cols_list}
                # print(inlet_data.keys())

                curr_name = None
                row_vals = None

                for row in section_text_values:
                    name = row[0]
                    inlet_type = row[1]

                    if name == curr_name:  # Continued from before
                        row_vals['Type'] = 'COMBINATION'
                    else:
                        if curr_name:  # We previously set row values
                            for row_col, row_val in row_vals.items():
                                inlet_data[row_col].append(row_val)
                        # default values
                        row_vals = {x: None for x in inlet_data.keys()}
                        row_vals['Name'] = name
                        row_vals['Type'] = inlet_type
                        curr_name = name

                    if inlet_type.upper() == 'GRATE' or inlet_type.upper() == 'DROP_GRATE':
                        row_vals['Grate_Length'] = float(row[2])
                        row_vals['Grate_Width'] = float(row[3])
                        row_vals['Grate_Type'] = row[4]
                        row_vals['Grate_Aopen'] = float(row[5]) if len(row) > 5 else None
                        row_vals['Grate_vsplash'] = float(row[6]) if len(row) > 6 else None
                    elif inlet_type.upper() == 'CURB' or inlet_type.upper() == 'DROP_CURB':
                        row_vals['Curb_Length'] = float(row[2])
                        row_vals['Curb_Height'] = float(row[3])
                        row_vals['Curb_Throat'] = str(row[4]) if len(row) > 4 else ''
                    elif inlet_type.upper() == 'SLOTTED':
                        row_vals['Slotted_Length'] = float(row[2])
                        row_vals['Slotted_Width'] = float(row[3])
                    else:  # Must be custom
                        row_vals['Custom_Curve'] = row[2]

                # Add the last row of values
                if curr_name:  # We previously set row values
                    for row_col, row_val in row_vals.items():
                        inlet_data[row_col].append(row_val)

                df = pd.DataFrame(inlet_data)
                df['Curb_Throat'] = df['Curb_Throat'].astype(str)
            elif self.name == 'Infiltration':
                # Custom mapping because we have to check for the method at the end
                section_text_values = [_tokenize(x) for x in text_array]

                methods = []
                section_text_values_new = []
                for row in section_text_values:
                    method_check = row[-1].upper()
                    if method_check in ('HORTON', 'MODIFIED_HORTON', 'GREEN_AMPT', 'MODIFIED_GREEN_AMPT', 'CURVE_NUMBER'):
                        methods.append(method_check)
                        section_text_values_new.append(row[:-1])
                    else:
                        methods.append(None)
                        section_text_values_new.append(row)

                section_data = {}
                for i_col, (col_name, col_type) in enumerate(self.cols_common_start[:-1]):
                    values = [col_type(x[i_col]) if i_col < len(x) else None for x in section_text_values_new]
                    section_data[col_name] = values

                section_data['Method'] = methods

                df = pd.DataFrame(section_data)
        else:
            if self.section_type == SectionType.KEYWORDS:
                # Handle non-keyword columns (front)
                section_text_values = [_tokenize(x) for x in text_array]

                section_data = {}
                for i_col, (col_name, col_type) in enumerate(self.cols_common_start):
                    values = [col_type(x[i_col]) if i_col < len(x) else None for x in section_text_values]
                    section_data[col_name] = values

                # Handle keyword columns
                keyword_cols_list = [[z[0] for z in y] for y in self.cols_keywords.values() if y is not None]
                # print(keyword_cols_list)
                keyword_cols = {x: list() for x in chain.from_iterable(keyword_cols_list)}
                # print(keyword_cols.keys())

                # Prep non-keyword end columns
                end_cols = None
                if self.cols_common_end:
                    end_cols = {x[0]: list() for x in self.cols_common_end}

                i_first_key_col = len(self.cols_common_start)
                for irow, row in enumerate(section_text_values):
                    # print(row)
                    # default values
                    row_vals = {x: None for x in keyword_cols.keys()}

                    key_val = row[self.keyword_col].upper()

                    if key_val not in self.cols_keywords:
                        if 'Default' not in self.cols_keywords:
                            feedback.reportError(
                                f'Unknown keyword {key_val} used for row {irow + 1} of section {self.name}',
                                fatalError=True)

                    key_definitions = self.cols_keywords[key_val] if key_val in self.cols_keywords else \
                        self.cols_keywords['Default']

                    last_key_col = i_first_key_col
                    if key_definitions is not None:
                        for i_key_col, (k, v) in enumerate(key_definitions):
                            i_key_col_actual = i_key_col + i_first_key_col
                            if i_key_col_actual < len(row):
                                if v == int:
                                    try:
                                        row_vals[k] = v(row[i_key_col_actual])
                                    except ValueError:
                                        # Couldn't convert try double first
                                        feedback.pushInfo(f'Expecting integer for {irow + 1} of section {self.name}. '
                                                          f'Converting as float.')
                                        row_vals[k] = v(float(row[i_key_col_actual]))
                                else:
                                    row_vals[k] = v(row[i_key_col_actual])
                                last_key_col = i_key_col_actual
                            else:
                                row_vals[k] = None

                    for row_col, row_val in row_vals.items():
                        keyword_cols[row_col].append(row_val)

                    # Handle non-keyword columns end
                    if self.cols_common_end:
                        i_first_end_col = last_key_col + 1
                        for i_col, (col_name, col_type) in enumerate(self.cols_common_end):
                            end_col = i_col + i_first_end_col
                            end_cols[col_name].append(col_type(row[end_col]) if end_col < len(row) else None)

                #     values = [col_type(x[end_col]) if end_col < len(x) else None for x in
                #               section_text_values]

                section_data.update(keyword_cols)
                if self.cols_common_end:
                    section_data.update(end_cols)
                # print(section_data)

                # Handle non-keyword columns end
                # i_first_end_col = i_first_key_col + len(keyword_cols.keys())
                # for i_col, (col_name, col_type) in enumerate(self.cols_common_end):
                #     end_col = i_col + i_first_end_col
                #     print(end_col)
                #     values = [col_type(x[end_col]) if end_col < len(x) else None for x in
                #               section_text_values]
                #     print(values)
                #     section_data[col_name] = values

            elif self.section_type == SectionType.WIDE:
                # Handle non-keyword columns (front)
                section_text_values = [_tokenize(x) for x in text_array]

                # Handle keyword columns
                # keyword_cols_list = [[z[0] for z in y] for y in self.cols_keywords.values() if y is not None]
                keyword_cols_list = [[f'{prefix}_{z[0]}' for z in y] for prefix, y in
                                     self.cols_keywords.items() if y is not None]
                keyword_cols = {x: list() for x in chain.from_iterable(keyword_cols_list)}
                # remove initial _ (blank prefix)
                keyword_cols = {k.lstrip('_'): v for k, v in keyword_cols.items()}

                prefixes = [x.upper() for x in self.cols_keywords.keys() if x != '']
                prefix_col = len(self.cols_common_start)

                # Prep non-keyword end columns
                end_cols = None
                if self.cols_common_end:
                    end_cols = {x[0]: list() for x in self.cols_common_end}

                # print(section_text_values)

                # We need to combine the values from multiple rows based on the initial value (some kind of id field)
                curr_index = -1
                curr_name = None
                curr_values = {}

                all_values = []

                for irow, row in enumerate(section_text_values):
                    name = row[0]
                    # print(name)

                    if name != curr_name:
                        if curr_name is not None:
                            # print(curr_values)
                            all_values.append(curr_values)

                        curr_index = curr_index + 1
                        curr_name = name
                        curr_values = {self.cols_common_start[0][0]: name}
                        curr_values.update({x: None for x in keyword_cols.keys()})

                    # Handle common values for row - only support first row (name) for now
                    # for i_col, (col_name, col_type) in enumerate(self.cols_common_start):
                    #    curr_values[col_name] = col_type(row[i_col]) if i_col < len(row) else None

                    # Handle keywords
                    keyword = row[prefix_col].lower()

                    if keyword in self.cols_keywords.keys():
                        key_definitions = self.cols_keywords[keyword]
                        if key_definitions is not None:
                            for i_key_col, (k, v) in enumerate(key_definitions):
                                i_key_col_actual = i_key_col + prefix_col + 1
                                if i_key_col_actual < len(row):
                                    col = f'{keyword}_{k}'
                                    if v == int:
                                        try:
                                            curr_values[col] = v(row[i_key_col_actual])
                                        except ValueError:
                                            # Couldn't convert try double first
                                            feedback.pushInfo(
                                                f'Expecting integer for {irow + 1} of section {self.name}. '
                                                f'Converting as float.')
                                            curr_values[col] = v(float(row[i_key_col_actual]))
                                    else:
                                        curr_values[col] = v(row[i_key_col_actual])
                                    last_key_col = i_key_col_actual
                    else:
                        # row is for assigning to empty prefix
                        curr_values[self.cols_keywords[''][0][0]] = keyword

                # Get the last values
                if curr_name is not None:
                    # print(curr_values)
                    all_values.append(curr_values)

                section_data = defaultdict(list)
                for values in all_values:
                    # print(values)
                    for k, v in values.items():
                        section_data[k].append(v)
            else:
                if len(self.cols_common_start) == 1:
                    section_text_values = [[x] for x in text_array]
                else:
                    section_text_values = [_tokenize(x, maxsplit=len(self.cols_common_start) - 1) for x in text_array]

                # print(section_text_values)

                section_data = {}
                for i_col, (col_name, col_type) in enumerate(self.cols_common_start):
                    values = [col_type(x[i_col]) if i_col < len(x) else None for x in section_text_values]
                    section_data[col_name] = values

            df = pd.DataFrame(
                data=section_data,
            )

        # Handle descriptions
        if self.name in tables_with_description:
            if descriptions:
                df_descript = pd.DataFrame.from_dict(
                    descriptions,
                    orient='index',
                    columns=['Description']
                )
                # print(df_descript)
                df = df.merge(df_descript, how='left', left_on=df.columns[0], right_index=True)
                # We only want to keep the first item
                duplicate_rows = df.duplicated(subset=df.columns[0])
                df.loc[duplicate_rows, 'Description'] = ''
            else:
                df['Description'] = ''

        return df


def swmm_section_definitions() -> list[SwmmSection]:
    swmm_sections_info: list[SwmmSection] = []

    swmm_sections_info.append(
        SwmmSection(
            'Title',
            'Project',
            None,
            SectionType.NO_KEYWORDS,
            [('Title', str)],
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Options',
            'Project',
            None,
            SectionType.NO_KEYWORDS,
            [('Option', str),
             ('Value', str)],
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Files',
            'Project',
            None,
            SectionType.NO_KEYWORDS,
            [
                ('Operation', str),
                ('Filetype', str),
                ('Filename', str),
            ],
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Evaporation',
            'Hydrology',
            None,
            SectionType.KEYWORDS,
            [
                ('Format', str)
            ],
            {
                'CONSTANT': [('evap', float)],
                'MONTHLY': [(f'e{x}', float) for x in range(1, 13)],
                'TIMESERIES': [('Tseries', str)],
                'TEMPERATURE': [],
                'FILE': [(f'p{x}', float) for x in range(1, 13)],
                'RECOVERY': [('patternId', str)],
                'DRY_ONLY': (('DryYesNo', str),)
            },
            0
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Temperature',
            'Hydrology',
            None,
            SectionType.NO_KEYWORDS,
            [
                ('Option', str),
                ('Value', str)
            ],
            None,
            0
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Raingages',
            'Hydrology',
            None,
            SectionType.KEYWORDS,
            [
                ('Name', str),
                ('Form', str),
                ('Intvl', str),
                ('SnowCatchDeficiency', float),
                ('Format', str),
            ],
            {
                'TIMESERIES': [('Tseries', str)],
                'FILE': [('Fname', str),
                         ('Sta', str),
                         ('Units', str)],
            },
            4
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Adjustments',
            'Hydrology',
            None,
            SectionType.NO_KEYWORDS,
            [
                ('Format', str),
            ] +
            [
                (f'month{x}', float) for x in range(1, 13)
            ],
            {
                'TIMESERIES': [('Tseries', str)],
                'File': [('Fname', str),
                         ('Sta', str),
                         ('Units', str)],
            },
            0
        )
    )

    snow_shared_items = [
        ('Cmin', float),
        ('Cmax', float),
        ('Tbase', float),
        ('FWF', float),
        ('SD0', float),
        ('FW0', float),
    ]
    swmm_sections_info.append(
        SwmmSection(
            'Snowpacks',
            'Hydrology',
            None,
            SectionType.KEYWORDS,
            (('Name', str),
             ('Type', str),
             ),
            {
                'PLOWABLE': snow_shared_items + [('SNN0', float)],
                'IMPERVIOUS': snow_shared_items + [('SD100', float)],
                'PERVIOUS': snow_shared_items + [('SD100', float)],
                'REMOVAL': (('Dplow', str),
                            ('Fout', float),
                            ('Fimp', float),
                            ('Fperv', float),
                            ('Fimelt', float),
                            ('Fsub', float),
                            ('Scatch', str),)
            },
            1,
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Subcatchments',
            'Hydrology',
            GeometryType.SUBCATCHMENTS,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Rain Gage', str),
             ('Outlet', str),
             ('Area', float),
             ('PctImperv', float),
             ('Width', float),
             ('PctSlope', float),
             ('CurbLen', float),
             ('SnowPack', str))
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Subareas',
            'Hydrology',
            GeometryType.SUBCATCHMENTS,
            SectionType.NO_KEYWORDS,
            (('Subcatchment', str),
             ('Nimp', float),
             ('Nperv', float),
             ('Simp', float),
             ('Sperv', float),
             ('PctZero', float),
             ('RouteTo', str),
             ('PctRouted', float))
        )
    )

    section_infiltration = SwmmSection(
        'Infiltration',
        'Hydrology',
        GeometryType.SUBCATCHMENTS,
        SectionType.NO_KEYWORDS,
        (('Subcatchment', str),
         ('p1', float),
         ('p2', float),
         ('p3', float),
         ('p4', float),
         ('p5', float),
         ('Method', str),
         ),
    )
    section_infiltration.custom_mapping = True
    swmm_sections_info.append(
        section_infiltration
    )

    swmm_sections_info.append(
        SwmmSection(
            'Aquifers',
            'Groundwater',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Por', float),
             ('WP', float),
             ('FC', float),
             ('Ksat', float),
             ('Kslope', float),
             ('Tslope', float),
             ('ETu', float),
             ('ETs', float),
             ('Seep', float),
             ('Ebot', float),
             ('Egw', float),
             ('Umc', float),
             ('ETupat', float),),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Groundwater',
            'Groundwater',
            GeometryType.SUBCATCHMENTS,
            SectionType.NO_KEYWORDS,
            (('Subcatchment', str),
             ('Aquifer', str),
             ('Node', str),
             ('Esurf', float),
             ('A1', float),
             ('B1', float),
             ('A2', float),
             ('B2', float),
             ('A3', float),
             ('Dsw', float),
             ('Egwt', float),
             ('Ebot', float),
             ('Wgr', float),
             ('Umc', float),)
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'GWF',
            'Groundwater',
            GeometryType.SUBCATCHMENTS,
            SectionType.NO_KEYWORDS,
            (('Subcatchment', str),
             ('Type', str),
             ('Expr', str),),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Pollutants',
            'WQ',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Units', str),
             ('Crain', float),
             ('Cgw', float),
             ('cii', float),
             ('Kdecay', float),
             ('Sflag', str),
             ('CoPoll', str),
             ('CoFract', float),
             ('Cdwf', float),
             ('Cinit', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Treatment',
            'WQ',
            GeometryType.NODES,
            SectionType.NO_KEYWORDS,
            (('Node', str),
             ('Pollut', str),
             ('Result', str),
             ('Func', str),
             )
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Landuses',
            'WQ',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('SweepIntervalDays', float),
             ('AvailabilityFract', float),
             ('LastSweepDays', float))
        )
    )

    coverages = SwmmSection(
        'Coverages',
        'WQ',
        GeometryType.SUBCATCHMENTS,
        SectionType.NO_KEYWORDS,
        (('Subcatchment', str),
         ('Landuse', str),
         ('Percent', float),
         )
    )
    coverages.custom_mapping = True
    swmm_sections_info.append(coverages)

    loadings = SwmmSection(
        'Loadings',
        'WQ',
        GeometryType.SUBCATCHMENTS,
        SectionType.NO_KEYWORDS,
        (('Subcatchment', str),
         ('Landuse', str),
         ('Percent', float),)
    )
    loadings.custom_mapping = True
    swmm_sections_info.append(loadings)

    swmm_sections_info.append(
        SwmmSection(
            'Buildup',
            'WQ',
            None,
            SectionType.NO_KEYWORDS,
            (('Landuse', str),
             ('Pollutant', str),
             ('FuncType', str),
             ('C1', float),
             ('C2', float),
             ('C3', float),
             ('PerUnit', str),
             )
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Washoff',
            'WQ',
            None,
            SectionType.NO_KEYWORDS,
            (('Landuse', str),
             ('Pollutant', str),
             ('FuncType', str),
             ('C1', float),
             ('C2', float),
             ('SweepRmv1', float),
             ('BmpRmv1', float),
             )
        )
    )

    # TODO - Removals can have multiple entries
    swmm_sections_info.append(
        SwmmSection(
            'LID_Controls',
            'LID',
            None,
            SectionType.WIDE,
            (('Name', str),
             ),
            {
                '': (('Type', str),),
                'surface': (('StorHt', float),
                            ('VegFrac', float),
                            ('Rough', float),
                            ('Slope', float),
                            ('Xslope', float)),
                'soil': (('Thick', float),
                         ('Por', float),
                         ('FC', float),
                         ('WP', float),
                         ('Ksat', float),
                         ('Kcoeff', float),
                         ('Suct', float)),
                'pavement': (('Thick', float),
                             ('Vratio', float),
                             ('FracImp', float),
                             ('Perm', float),
                             ('Vclog', float),
                             ('Treg', float),
                             ('Freg', float),
                             ),
                'storage': (('Height', float),
                            ('Vratio', float),
                            ('Seepage', float),
                            ('Vclog', float),
                            ('Covrd', str)),
                'drain': (('Coeff', float),
                          ('Expon', float),
                          ('Offset', float),
                          ('Delay', float),
                          ('Hopen', float),
                          ('Hclose', float),
                          ('Qcrv', float)),
                'drainmat': (('Thick', float),
                             ('Vratio', float),
                             ('Rough', float)),
                # Handle removals later
                #                'REMOVALS': (('Pollut', float),
                #                             ('Rmvl', float))
            },
            1,
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'LID_Usage',
            'LID',
            GeometryType.SUBCATCHMENTS,
            SectionType.NO_KEYWORDS,
            (('Subcatchment', str),
             ('LID', str),
             ('Number', int),
             ('Area', float),
             ('Width', float),
             ('InitSat', float),
             ('FromImp', float),
             ('ToPerv', float),
             ('RptFile', str),
             ('DrainTo', str),
             ('FromPerv', float),)
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Junctions',
            'Nodes',
            GeometryType.NODES,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Elev', float),
             ('Ymax', float),
             ('Y0', float),
             ('Ysur', str),
             ('Apond', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Outfalls',
            'Nodes',
            GeometryType.NODES,
            SectionType.KEYWORDS,
            (('Name', str),
             ('Elev', float),
             ('Type', str),
             ),
            {
                'FREE': None,
                'NORMAL': None,
                'FIXED': (('Stage', str),),
                'TIDAL': (('Tcurve', str),),
                'TIMESERIES': (('Tseries', str),),
            },
            2,
            (('Gated', str),
             ('RouteTo', str),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Dividers',
            'Nodes',
            GeometryType.NODES,
            SectionType.KEYWORDS,
            (('Name', str),
             ('Elev', float),
             ('DivLink', str),
             ('Type', str)),
            {
                'OVERFLOW': None,
                'CUTOFF': (('Qmin', float),),
                'TABULAR': (('Dcurve', float),),
                'WEIR': (('Qmin', float),
                         ('Ht', float),
                         ('Cd', float))
            },
            3,
            (('Ymax', float),
             ('Y0', float),
             ('Ysur', float),
             ('Apond', float))
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Storage',
            'Nodes',
            GeometryType.NODES,
            SectionType.KEYWORDS,
            (('Name', str),
             ('Elev', float),
             ('Ymax', float),
             ('Y0', float),
             ('TYPE', str)),
            {
                'TABULAR': (('Acurve', str),),
                'FUNCTIONAL': (('A1', float),
                               ('A2', float),
                               ('A0', float)),
                'Default': (('L', float),
                            ('W', float),
                            ('Z', float)),
            },
            4,
            (('Ysur', float),
             ('Fevap', float),
             ('Psi', float),
             ('Ksat', float),
             ('IMD', float)),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Conduits',
            'Links',
            GeometryType.LINKS,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('From Node', str),
             ('To Node', str),
             ('Length', float),
             ('Roughness', float),
             ('InOffset', float),
             ('OutOffset', float),
             ('InitFlow', float),
             ('MaxFlow', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Pumps',
            'Links',
            GeometryType.LINKS,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('From Node', str),
             ('To Node', str),
             ('Pcurve', str),
             ('Status', str),
             ('Startup', float),
             ('Shutoff', float)),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Weirs',
            'Links',
            GeometryType.LINKS,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('From Node', str),
             ('To Node', str),
             ('Type', str),
             ('CrestHt', float),
             ('Cd', float),
             ('Gated', str),
             ('EC', str),
             ('Cd2', float),
             ('Sur', str),
             ('Road_Width', float),
             ('Road_Surf', str))
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Orifices',
            'Links',
            GeometryType.LINKS,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('From Node', str),
             ('To Node', str),
             ('Type', str),
             ('Offset', float),
             ('Qcoeff', float),
             ('Gated', str),
             ('CloseTime', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Outlets',
            'Links',
            GeometryType.LINKS,
            SectionType.KEYWORDS,
            (('Name', str),
             ('From Node', str),
             ('To Node', str),
             ('Offset', float),
             ('Type', str),
             ),
            {
                'TABULAR/DEPTH': (('QCurve', str),),
                'TABULAR/HEAD': (('QCurve', str),),
                'FUNCTIONAL/DEPTH': (('C1', float), ('C2', float)),
                'FUNCTIONAL/HEAD': (('C1', float), ('C2', float)),
            },
            4,
            (('Gated', str),),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Losses',
            'Links',
            None,
            SectionType.NO_KEYWORDS,
            (('Link', str),
             ('Kentry', float),
             ('Kexit', float),
             ('Kavg', float),
             ('Flap', str),
             ('Seepage', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'XSections',
            'Links',
            None,
            SectionType.KEYWORDS,
            (
                ('Link', str),
                ('XsecType', str),
            ),
            {
                'Default': (('Geom1', float),
                            ('Geom2', float),
                            ('Geom3', float),
                            ('Geom4', float),
                            ('Barrels', int),
                            ('Culvert', str),
                            ),
                'CUSTOM': (('Geom1', float),
                           ('Curve', str),
                           ('Geom3', float),  # SWMM seems to require this even though it is not used/needed
                           ('Geom4', float),  # SWMM seems to require this even though it is not used/needed
                           ('Barrels', int),
                           ),
                'IRREGULAR': (('Tsect', str),
                              ),
                'STREET': (('Street', str),
                           ),
            },
            1,
        )
    )

    # Split Transects into two tables Transects and Transects_coords and switched Manning N to required for each
    transects = \
        SwmmSection(
            'Transects',
            'Links',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Xleft', float),
             ('Xright', float),
             ('Lfactor', float),
             ('Wfactor', float),
             ('Eoffset', float),
             ('Nleft', float),
             ('Nright', float),
             ('Nchanl', float),
             )
        )
    transects.custom_mapping = True
    swmm_sections_info.append(transects)

    transects_coords = \
        SwmmSection(
            'Transects_coords',
            'Links',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Elev', float),
             ('Station', float),
             )
        )
    transects_coords.custom_mapping = True
    swmm_sections_info.append(transects_coords)

    swmm_sections_info.append(
        SwmmSection(
            'Streets',
            'Links',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('Tcrown', float),
             ('Hcurb', float),
             ('Sx', float),
             ('nRoad', float),
             ('a', float),
             ('W', float),
             ('Sides', int),
             ('Tback', float),
             ('Sback', float),
             ('nBack', float),
             )
        )
    )

    # ('TRANSECTS', 'Links',
    #  (('Type',) + tuple([f'Param{x}' for x in range(1, 11)])), None),

    # columns we want (map to multiple lines potentially)
    # Name
    # Type - (new option COMBINATION) - GRATE, CURB, COMBINATION, SLOTTED, CUSTOM
    # DropGrate (T/F), Grate_Length, Grate_Width, Grate_Type, Grate_Aopen, Grate_vsplash
    # DropCurb, Curb_Length, Curb_Height, Curb_Throat
    # Slotted_Length, Slotted_width
    # Custom_curve

    inlets = SwmmSection(
        'Inlets',
        'Inlets',
        None,
        SectionType.NO_KEYWORDS,
        cols_common_start=(('Name', str),
                           ('Type', str),
                           ('Grate_Length', float),
                           ('Grate_Width', float),
                           ('Grate_Type', str),
                           ('Grate_Aopen', float),
                           ('Grate_vsplash', float),
                           ('Curb_Length', float),
                           ('Curb_Height', float),
                           ('Curb_Throat', str),
                           ('Slotted_Length', float),
                           ('Slotted_Width', float),
                           ('Custom_Curve', str),
                           )
    )
    inlets.custom_mapping = True
    swmm_sections_info.append(inlets)

    swmm_sections_info.append(
        SwmmSection(
            'Inlet_Usage',
            'Inlets',
            GeometryType.INLETS,
            SectionType.NO_KEYWORDS,
            (('Conduit', str),
             ('Inlet', str),
             ('Node', str),
             ('Number', int),
             ('PctClogged', float),
             ('Qmax', float),
             ('aLocal', float),
             ('wLocal', float),
             ('Placement', str),
             )
        )
    )

    # This is for Inlet Usage defined externally to the SWMM inp file. It is included here for use with estry_to_swmm
    swmm_sections_info.append(
        SwmmSection(
            'Inlet_Usage_ext',
            '',
            GeometryType.INLETS,
            SectionType.NO_KEYWORDS,
            (('Inlet', str),
             ('StreetXSEC', str),
             ('Elevation', float),
             ('SlopePct_Long', float),
             ('Number', int),
             ('CloggedPct', float),
             ('Qmax', float),
             ('aLocal', float),
             ('wLocal', float),
             ('Placement', str),
             ('Conn1D_2D', str),
             ('Conn_width', float),
             )
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Inflows',
            'BC',
            GeometryType.NODES,
            SectionType.KEYWORDS,
            (('Node', str),
             ('Type', str)),
            {
                # Default handles all pollutants
                'Default': (('Tseries', str),
                            ('PollutType', str),
                            ('Mfactor', float),
                            ),
                'FLOW': (('Tseries', str),
                         ('SeriesType', str),
                         ('Factor1', float),),
            },
            1,
            (('Sfactor', float),
             ('Base', float),
             ('Pat', str),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Controls',
            'Links',
            None,
            SectionType.NO_KEYWORDS,
            (('Text', str),),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'DWF',
            'Nodes',
            GeometryType.NODES,
            SectionType.NO_KEYWORDS,
            (('Node', str),
             ('Type', str),
             ('Base', float),
             ('Pat1', str),
             ('Pat2', str),
             ('Pat3', str),
             ('Pat4', str),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'RDII',
            'Hydrology',
            GeometryType.NODES,
            SectionType.NO_KEYWORDS,
            (('Node', str),
             ('UHgroup', str),
             ('SewerArea', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Hydrographs',
            'Hydrology',
            None,
            SectionType.NO_KEYWORDS,
            (('Name', str),
             ('RaingageOrMonth', str),
             ('Term', str),
             ('R', float),
             ('T', float),
             ('K', float),
             ('Dmax', float),
             ('Drec', float),
             ('D0', float),
             ),
        )
    )

    timeseries = SwmmSection(
        'Timeseries',
        'Curves',
        None,
        SectionType.NO_KEYWORDS,
        (('Name', str),
         ('Fname', str),
         ('Date', str),
         ('Time', str),
         ('Value', float),
         ),
    )
    timeseries.custom_mapping = True
    swmm_sections_info.append(timeseries)

    curves = SwmmSection(
        'Curves',
        'Curves',
        None,
        SectionType.NO_KEYWORDS,
        (('Name', str),
         ('Type', str),
         ('xval', float),
         ('yval', float),
         ),
    )
    curves.custom_mapping = True
    swmm_sections_info.append(curves)

    patterns = SwmmSection(
        'Patterns',
        'Hydrology',
        None,
        SectionType.NO_KEYWORDS,
        [('Name', str),
         ('Interval', str)] +
        [(f'Factor{x}', float) for x in range(1, 25)],
    )
    patterns.custom_mapping = True
    swmm_sections_info.append(patterns)

    swmm_sections_info.append(
        SwmmSection(
            'Report',
            'Project',
            None,
            SectionType.NO_KEYWORDS,
            (('Format', str),
             ('Value', str),
             ),
        )
    )

    # don't create tags section will add tags column to relevant tables
    swmm_sections_info.append(
        SwmmSection(
            'Tags',
            'Misc',
            None,
            SectionType.NO_KEYWORDS,
            (('Object_type', str),
             ('Object', str),
             ('Tag', str),),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Map',
            'Misc',
            None,
            SectionType.NO_KEYWORDS,
            (('Option', str),
             ('Value', str),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Symbols',
            'Misc',
            GeometryType.MISC,
            SectionType.GEOMETRY,
            (('Gage', str),
             ('X-Coord', float),
             ('Y-Coord', float),
             ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Labels',
            'Misc',
            GeometryType.MISC,
            SectionType.GEOMETRY,
            (
                ('X-Coord', float),
                ('Y-Coord', float),
                ('Label', str),
            ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Backdrop',
            'Misc',
            None,
            SectionType.NO_KEYWORDS,
            (
                ('Option', str),
                ('Value', str),
            ),
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Coordinates',
            '',
            GeometryType.NODES,
            SectionType.GEOMETRY,
            (('Node', str),
             ('X-Coord', float),
             ('Y-Coord', float),
             )
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Vertices',
            '',
            GeometryType.LINKS,
            SectionType.GEOMETRY,
            (('Link', str),
             ('X-Coord', float),
             ('Y-Coord', float),)
        )
    )

    swmm_sections_info.append(
        SwmmSection(
            'Polygons',
            '',
            GeometryType.SUBCATCHMENTS,
            SectionType.GEOMETRY,
            (('Subcatchment', str),
             ('X-Coord', float),
             ('Y-Coord', float),
             )
        )
    )

    return swmm_sections_info


if __name__ == "__main__":
    # Generate text to add sections to C++
    for name, _, column_names, _ in swmm_sections_def:
        print('m_sectionList.push_back(')
        print('\tstd::make_pair(')
        print(f'\t\tstd::string("{name}"),')
        quoted_columns = [f'"{x}"' for x in column_names]
        print(f'\t\tstd::vector<std::string>({{{",".join(quoted_columns)}}})')
        print('\t\t)')
        print('\t);')
        # print(name)
        # print(column_names)
