
'''
    PURPOSE: Provide a library of routines to be used by ST python
             applications.  Each routine is placed under a class in hopes of
             separating them into specific collections/groups.

    PROJECT: Land Satellites Data Systems Science Research and Development
             (LSRD) at the USGS EROS

    LICENSE: NASA Open Source Agreement 1.3
'''

import os
import sys
import logging
import subprocess
import datetime
from time import sleep
from io import StringIO
from osgeo import gdal, osr
import re
import numpy as np
from configparser import ConfigParser

from st_exceptions import MissingBandError

logger = logging.getLogger(__name__)
ST_NO_DATA_VALUE = 0
# Removing Collection 2 ST scale and offset:
#     - for Single Channel, we now have a maximum of 500K in the Brightness
#       temperature LUTs.  The new 373-500K values are reached in wildfires
#       and volcanoes.
#     - for Split Window, there wasn't a real limit of 373K in the algorithm
# The Collection 2 scale and offset are designed for the 373K bound but now
# we exceed that.  It is unknown what scale/offset will be used for Collection
# 3 but for now in ESPA we will use a factor of 10 scale and no offset.  This
# is easier to work with but loses more information than a scale/offset that
# fill the data type at the conversion to integer step.
DEFAULT_SCALE = 0.01
DEFAULT_OFFSET = 0.0

# Range of final ST values in Kelvin
ST_MIN = 150.0
ST_MAX = 500.0

# Saturation values
TM_ETM_HIGH_SATURATION = 255
OLI_HIGH_SATURATION = 65535

# No Data Value for ST intermediate bands. This must match the no_data_value
# used by the ASTER GED
INTERMEDIATE_NO_DATA_VALUE = -9999

# No Data Value for 8-bit unsigned emissivity source band
EMIS_SRC_NO_DATA_VALUE = 255

# Convolution kernel size
KERNEL_SIZE = 5

# Coefficient filename
COEFFICIENT_FILE = 'coefficients.json'


class Version(object):
    '''
    Description:
        Provides methods for retrieving version information.
    '''

    version = 'TBD skip_fv (Collection 2)'
    split_window_version = 'TBD skip_fv (Collection 2)'

    @staticmethod
    def version_number():
        '''
        Description:
            Returns the version number.
        '''

        return Version.version

    @staticmethod
    def version_text():
        '''
        Description:
            Returns the version information as a spelled out string.
        '''

        msg = 'Surface Temperature - Version {0}'.format(Version.version)
        return msg

    @staticmethod
    def app_version():
        '''
        Description:
            Returns the version information.
        '''

        version_text = 'st_{0}'.format(Version.version)
        return version_text

    @staticmethod
    def split_window_version_number():
        '''
        Description:
            Returns the split window version number.
        '''

        return Version.split_window_version

    @staticmethod
    def split_window_version_text():
        '''
        Description:
            Returns the split window version information as a spelled out
            string.
        '''

        msg = 'Split Window Surface Temperature - Version {0}'.format(
              Version.split_window_version)
        return msg

    @staticmethod
    def split_window_app_version():
        '''
        Description:
            Returns the Split Window version information.
        '''

        split_window_version_text = 'split_window_st_{0}'.format(
            Version.split_window_version)
        return split_window_version_text


class REANALYSIS(object):
    """Provides common REANALYSIS data related methods
    """

    @staticmethod
    def dates(espa_metadata):
        """Determines the before(time_0), after(time_1), and aquisition dates

        Args:
            espa_metadata <espa.metadata>: The metadata for the data

        Returns:
            acquisition <datetime>: Scene center date and time
            time_0 <datetime>: REANALYSIS data datetime before scene center
            time_1 <datetime>: REANALYSIS data datetime after scene center
        """

        center_time = str(espa_metadata.xml_object
                          .global_metadata.scene_center_time)

        acq_date = str(espa_metadata.xml_object
                       .global_metadata.acquisition_date)

        # Join them while dropping the last two '<number>Z'
        date_time = '-'.join([acq_date, center_time[:-2]])

        acquisition = (datetime.datetime
                       .strptime(date_time, '%Y-%m-%d-%H:%M:%S.%f'))

        '''
        Determine the 3hr increments to use from the auxiliary data
        We want the one before and after the scene acquisition time
        and convert back to formatted strings
        '''
        scene_hour = int(center_time[:2])
        time_0_hour = scene_hour - (scene_hour % 3)

        time_0 = datetime.datetime(acquisition.year,
                                   acquisition.month,
                                   acquisition.day,
                                   time_0_hour)

        time_1 = time_0 + datetime.timedelta(hours=3)

        # Round acquisition date to nearest minute.
        if acquisition.second >= 30:
            needed_seconds = datetime.timedelta(0, 60 - acquisition.second,
                                                -acquisition.microsecond)
            rounded_acquisition = acquisition + needed_seconds
        else:
            extra_seconds = datetime.timedelta(0, acquisition.second,
                                               acquisition.microsecond)
            rounded_acquisition = acquisition - extra_seconds

        return (rounded_acquisition, time_0, time_1)

    @staticmethod
    def get_reanalysis():
        """
        Description:
            Gets the string identifying the reanalysis type that is used

        Returns:
            reanalysis <str>: Formatted reanalysis string 
        """

        reanalysis_file = 'reanalysis.txt'
        if os.path.exists(reanalysis_file):
            with open(reanalysis_file, 'r') as re_fd:
                reanalysis = re_fd.readline() 
                re_fd.close()
        else:
            reanalysis = ''

        return (reanalysis)


class ST_Geo(object):
    '''
    Description:
        Provides methods for interfacing with geographic projections.
    '''

    @staticmethod
    def update_envi_header(hdr_file_path, no_data_value):
        '''
        Description:
            Updates the specified ENVI header.  Especially the no data value,
            since it is not supported by the GDAL ENVI driver.

        Args:
            hdr_file_path <str>: Path including filename for ENVI header file
            no_data_value <float>: Value to use for fill
        '''

        hdr_text = StringIO()
        with open(hdr_file_path, 'r') as tmp_fd:
            while True:
                line = tmp_fd.readline()
                if not line:
                    break
                if (line.startswith('data ignore value') or
                        line.startswith('description')):
                    pass
                else:
                    hdr_text.write(line)

                if line.startswith('description'):
                    # This may be on multiple lines so read lines until
                    # we find the closing brace
                    if not line.strip().endswith('}'):
                        while 1:
                            next_line = tmp_fd.readline()
                            if (not next_line or
                                    next_line.strip().endswith('}')):
                                break
                    hdr_text.write('description ='
                                   ' {USGS-EROS-ESPA generated}\n')
                elif (line.startswith('data type') and
                      (no_data_value is not None)):
                    hdr_text.write('data ignore value = {0}\n'
                                   .format(no_data_value))

        # Do the actual replace here
        with open(hdr_file_path, 'w') as tmp_fd:
            tmp_fd.write(hdr_text.getvalue())


def get_satellite_sensor_code(product_id):
    """
    Derives and validates the satellite-sensor code from the product Id

    Args:
        product id: Landsat product Id

    Returns:
        <str>: Satellite sensor code
    """
    # Read the satellite sensor code from the product Id
    satellite_sensor_code = product_id[0:4]

    # Define the regex used to determine if the satellite sensor code is valid
    sat_sensor_code_regex = re.compile("LT0[45]|LE07|L[OTC]0[89]")

    # If valid return the satellite sensor code
    if re.match(sat_sensor_code_regex, satellite_sensor_code):
        return satellite_sensor_code

    raise Exception('The first four digits of the product_id, {0}, '
                    'do not match the expected Landsat Product Id '
                    'format'.format(product_id))


def get_radiometric_conversion_filename(satellite):
    """
    Gets the radiometric conversion LUT filename given the satellite

    Args:
        satellite: Satellite name, for example 'LANDSAT_9'

    Returns:
        <str>: Radiometric conversion LUT filename
    """

    if satellite == 'LANDSAT_9':
        logger.info('Using Landsat 9 Radiometric Conversion LUT')
        conversion_lut_name = 'L9_Rad_Conversion_LUT.txt'

    elif satellite == 'LANDSAT_8':
        logger.info('Using Landsat 8 Radiometric Conversion LUT')
        conversion_lut_name = 'L8_Rad_Conversion_LUT.txt'

    elif satellite == 'LANDSAT_7':
        logger.info('Using Landsat 7 Radiometric Conversion LUT')
        conversion_lut_name = 'L7_Rad_Conversion_LUT.txt'

    elif satellite == 'LANDSAT_5':
        logger.info('Using Landsat 5 Radiometric Conversion LUT')
        conversion_lut_name = 'L5_Rad_Conversion_LUT.txt'

    elif satellite == 'LANDSAT_4':
        logger.info('Using Landsat 4 Radiometric Conversion LUT')
        conversion_lut_name = 'L4_Rad_Conversion_LUT.txt'

    else:
       raise Exception('Invalid satellite {0}, '.format(satellite))

    return conversion_lut_name


def get_thermal_band_metadata(espa_metadata):
    """Finds and returns the metadata for the level 1 thermal band.
       For L7, b61 is used.  For L8 and L9, b10 is used.

    Args:
        espa_metadata <espa.metadata>: The metadata for the data

    Returns:
        band_metadata: ESPA metadata for the band
    """

    satellite = espa_metadata.xml_object.global_metadata.satellite
    for band in espa_metadata.xml_object.bands.band:
        if (band.get('product') in ('L1TP', 'L1GT', 'L1GS')):
            if satellite == 'LANDSAT_4' or satellite == 'LANDSAT_5':
                if (band.get("name") == 'b6'):
                    return band
            elif satellite == 'LANDSAT_7':
                if (band.get("name") == 'b61'):
                    return band
            else: # satellite == 'LANDSAT_8' or satellite == 'LANDSAT_9'
                if (band.get("name") == 'b10'):
                    return band

    # It will only get here if the band could not be found.
    raise MissingBandError('Missing thermal input band')

