
from desispec.workflow.exptable import get_exposure_table_name,get_exposure_table_path, \
                                       get_exposure_flags, get_last_step_options, get_exposure_table_column_defs, \
                                       keyval_change_reporting, deconstruct_keyval_reporting
from desispec.workflow.tableio import load_table, write_table
from desispec.workflow.utils import pathjoin
from desispec.io.util import parse_cameras, decode_camword, create_camword, parse_badamps, validate_badamps

import os
import numpy as np
from astropy.table import Table

def process_int_range_inclusive(input_string):
    """
    Given a str indicating a range of integers, this auto-detects the symbol used and returns that range as an INCLUSIVE
    numpy array of ints. Symbol can be ':', '-', or '..'.

    Args:
        input_string, str. String with integer range with the upper value being included in the output. E.g. 100:102
                           returns 100,101,102.

    Returns:
        np.array. Array of ints for the range specified in the input_string.
    """
    for symbol in [':','-','..']:
        if symbol in input_string:
            first,last = input_string.split(symbol)
            return np.arange(int(first),int(last)+1)

def parse_int_list_term(input_string, allints=None):
    """
    Given a str this determines what integer values it represents. Whether that be "all" indicating all ints in the
    table column, a range of integers specified with ':', '-', or '..', or a single integer. This should not be a list.

    Args:
        input_string, str. String with either integer range, single integer, or 'all'. 'all' requires allints
        allints, np.array. One dimensional array of all integers. Returns if 'all' is specified.

    Returns:
        out_array, np.array. Array of ints for the string specified.
    """
    if input_string.lower() == 'all' and allints is not None:
        out_array = np.asarray(allints)
    elif input_string.isnumeric():
        out_array = np.atleast_1d(int(input_string))
    elif np.any([symb in input_string for symb in [':','-','..']]):
        out_array = process_int_range_inclusive(input_string)
    else:
        raise ValueError(f"Couldn't understand input {input_string}")
    return out_array

def parse_int_list(input_string, allints=None, only_unique=True):
    """
    Given a str this determines what integer values it represents. Whether that be "all" indicating all ints in the
    table column, a range of integers specified with ':', '-', or '..', a single integer, or an indeterminant number of
    them in a comma separated list.

    Args:
        input_string, str. String with either integer range, single integer, or 'all'. It can have a combination of
                           multiple of these separated by a comma. 'all' requires allints.
        allints, np.array. One dimensional array of all integers. Returns if 'all' is specified.
        only_unique, bool. True if you want a unique set returned. Otherwise repeated entries in the input string
                           are kept.

    Returns:
        out_array, np.array. Array of ints for the string specified.
    """
    input_string = input_string.strip(' \t,')
    out_array = np.atleast_1d()
    for substr in input_string.split(","):
        out_array = np.append(out_array, parse_int_list_term(substr, allints=allints))
    if only_unique:
        out_array = np.unique(out_array)
    return out_array.astype(int)

def columns_not_to_report():
    """
    Returns list of column names that shouldn't have reporting information saved because they are user-defined values.
    """
    return ['COMMENTS', 'HEADERERR', 'BADCAMWORD', 'BADAMPS', 'LASTSTEP', 'EXPFLAG']

def columns_not_to_edit():
    """
    Defines column names that shouldn't be edited.
    """
    ## Occasionally unchanging things like NIGHT or TILEID have been missing in the headers, so we won't restrict
    ## that even though it typically shouldn't be edited if the data is there
    return ['EXPID', 'CAMWORD', 'OBSTYPE']

def columns_not_to_append():
    """
    Defines column names that shouldn't be edited.
    """
    return ['LASTSTEP', 'SURVEY', 'FA_SURV', 'FAPRGRM', 'GOALTYPE']

def validate_value(colname, value, joinsymb):
    """
    Checks that the value provided matches the syntax of the colname given. If the syntax is incorrect
    an error is raised.

    Warning: may change the value of "value" and returns it.

    Args:
        colname, str. The name of the column that is being edited.
        value, any scalar type. The value that the column's current value should be changed to.

    Returns:
        value, any scalar type. The value that the column's current value should be changed to. This is verified to
                                have the proper syntax for the colname given.

    """
    ## Match data type and convert where necessary
    if colname == 'EXPFLAG':
        ## Make sure the exposure flag is a valid one
        expflags = get_exposure_flags()
        value = value.lower().replace(' ','_')
        if value not in expflags:
            raise ValueError(f"Couldn't understand exposure flag: '{value}'. Available options are: {expflags}.")
    elif colname == 'BADAMPS':
        ## Make sure we can decode the badamp value (or easily correct it so we can decode it)
        ## This raises an error if it can't be converted to a viable list
        value = validate_badamps(value, joinsymb=joinsymb)
    elif colname == 'BADCAMWORD':
        ## Make sure we can understand the cameras given
        ## This raises an error if it cant be parsed
        value = parse_cameras(value)
    elif colname == 'LASTSTEP':
        options = get_last_step_options()
        value = value.lower()
        if value not in options:
            raise ValueError(f"Couldn't understand laststep: '{value}'. Available options are: {options}.")
    elif joinsymb in value:
        print(f"WARNING: For colname {colname} you provided a value '{value}' that contains the default"+
              f" joinsymbol='{joinsymb}'. This is allowed, but use at your own caution. Continuing...")
    elif '|' in value:
        print(f"WARNING: For colname {colname} you provided a value '{value}' that contains the default"+
              " indicator of an array string in the exposure tables (the 'pipe' i.e. '|'."+
              " This is allowed, but use at your own caution. Continuing...")
    else:
        ## Otherwise we don't have a strict syntax, so pass it
        pass
    return value

def document_in_comments(tablerow,colname,value,comment_col='HEADERERR'):
    """
    Places "reporting" string in the appropriate comment column of the exposure table to document the edits being
    made.

    Note: This alters and returns the input tablerow. How astropy handles this may vary. As of Jan 2021, I believe a copy