# 
#  Copyright (C) 2010-2014,2016,2018  Smithsonian Astrophysical Observatory
#
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program; if not, write to the Free Software Foundation, Inc.,
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#


import pycrates.io as io
import pycrates as pycr
import copy
import hashlib
import os.path

class CrateDataset:

    def __init__(self, input=None, mode='rw'):
        """
        Initializes the CrateDataset object.
        """
        self.__clear__()

        if input is None:
            self.set_rw_mode(mode)
            return

        input = pycr.convert_2_str(input)
        
        # Select the backend appropriate for the input
        backend = io.select( input )
        if backend is None:
            raise AttributeError("Can not determine kernel for input: %s" % input )
        else:
            if backend._file_exists( input ) == False:
                raise IOError("File " + input + " does not exist.")

            try:
                rw_mode = backend.open( input, mode=mode )
                
            except:
                raise
            
            backend._load_dataset(self)
            self._filename = input
            self.set_rw_mode(rw_mode)
            
    
    def __clear__(self):
        """
        Clears the CrateDataset object.
        """
        self._filename = ""

        if hasattr(self, "_CrateDataset__crates"):
            self.__crates[:] =  []  # list of extensions in file
        else:
            self.__crates = []

        self.__ncrates = 0
        self.__rw_mode = ""
        self.__current_crate = -1
        self.__backend = None

        if hasattr(self, "_CrateDataset__crate_trash"):
            self.__crate_trash[:] = []
        else:
            self.__crate_trash = []


    def __del__(self):
        """
        Clears and closes the CrateDataset object.
        """
        if self.__backend is not None:
            self.__backend.close()
        self.__clear__()

    def __str__(self):
        """
        Returns a formatted string representation of the CrateDataset object.
        """
        return self.__repr__()

    def __repr__(self):
        """
        Returns a formatted string representation of the CrateDataset object.
        """
        retstr  = "   Crate Dataset:\n"
        retstr += "     File Name:         " + self._filename + "\n"
        retstr += "     Read-Write Mode:   " + str(self.get_rw_mode()) + "\n"
        retstr += "     Number of Crates:  " + str(self.__ncrates) + "\n"

        if self.__crates is not None:
            for ii in range(0, self.__ncrates):
                retstr += "       " + str(ii+1) + ")  " + self.__crates[ii].__str__() + "\n"

        return retstr

    def get_filename(self):
        """
        Returns the input file name.
        """
        return self._filename

    def get_rw_mode(self):
        """
        Returns whether the dataset is readable, writeable, or both.
        """
        return str(self.__rw_mode)

    def get_ncrates(self):
        """
        Returns the number of crates in the dataset.
        """
        return self.__ncrates

    def get_current_crate(self):
        """
        Returns the number of the dataset's current crate.
        """
        return self.__current_crate


    def _get_base_crate(self, crate):
        """
        Retrieves the underlying crate based on input number (one-based indexing) or name.
        """
        crate = pycr.convert_2_str(crate)
        if not isinstance(crate, str):
            try:
                return self.__crates[crate-1]
            except IndexError:
                raise IndexError("Index out of range")
        else:
            for item in self.__crates:
                if item.name.upper() == crate.upper():
                    return item
          
        return None


    def get_crate(self, crate):
        """
        Retrieves the crate based on input number (one-based indexing) or name.
        """
        basecrate = self._get_base_crate( crate )

        if basecrate is None:
            raise IndexError("Unable to find Crate " + crate + ".")

        tmpcrate = None

        # create a new TABLECrate or IMAGECrate with base Crate
        if basecrate.is_image():
            tmpcrate =  pycr.IMAGECrate( basecrate )
        else:
            tmpcrate =  pycr.TABLECrate( basecrate )

        tmpcrate._set_parent_dataset(self)

        # get header and data info, if not already read in
        if tmpcrate._get_key_flag() == False:
            tmpcrate._read_header()
        if tmpcrate._get_data_flag() == False:
            tmpcrate._read_data()

        # setup named Crate attributes
        if tmpcrate.is_image() and not hasattr(tmpcrate, "image"):
            try: 
                tmpcrate.__dict__["image"] = tmpcrate.get_image()
            except LookupError:
                tmpcrate.__dict__["image"] = CrateData()
                pass
        elif tmpcrate.is_image() == False:
            ncols = tmpcrate.get_ncols()
            for ii in range(ncols):
                col = tmpcrate.get_column( ii )
                if col:
                    if hasattr(tmpcrate, col.name):
                        pass
                    else:
                        tmpcrate.__dict__[ col.name ] = col
            
        return tmpcrate


    def write(self, outfile=None, clobber=False, history=True):
        """
        Writes the dataset to a file.  If no output file name is 
        given, will write file in place.
        """

        if outfile == self._filename:
            outfile = None              # <<== writing to self

        # if this dataset is not writeable, raise error
        if outfile is None and self.is_writeable() == False:
            raise IOError("File is not writeable.")

        # determine backend 
        backend = None
        if outfile is None:
            if self.__backend is None:
                raise IOError("Unable to write file without a destination.")
            else:
                backend = self.__backend
        else:
            outfile = pycr.convert_2_str(outfile)
            backend = pycr.io.select( outfile )
            backend._clobber( outfile, clobber )
            backend.create( input=outfile )

        backend.write( self, outfile=outfile, history=history)
    

    def add_crate(self, obj ):
        """
        Adds a crate to the dataset.
        """
        if isinstance(obj, pycr.Crate):
            cr = obj

        elif isinstance(obj, pycr.TABLECrate) or isinstance(obj, pycr.IMAGECrate):
            if obj._get_key_flag() == False:
                obj._read_header()
            if obj._get_data_flag() == False:
                obj._read_data()
 
            cr = obj._get_base_crate()

        else:
            raise TypeError("Input must be a Crate.")

        # if input base crate has the same name as an already existing base crate,
        # copy input crate info
        self_obj = self._get_base_crate( obj.name )

        if self_obj is not None:
            self_sig = hashlib.sha1( (self_obj.__str__()).encode('utf-8') ).hexdigest()
            obj_sig  = hashlib.sha1( (cr.__str__()).encode('utf-8') ).hexdigest()
                
            if self_sig != obj_sig:
                self.__crates.remove(self_obj)
                self.__crates.append( cr )

        else:
            self.__crates.append( cr )
            self.__ncrates += 1


    def delete_crate(self, item):
        """
        Removes a crate from the dataset based on input number (one-based indexing) or name.
        """
        crate = self._get_base_crate( item )
        
        if crate:
            self.__crate_trash.append( crate.name )
            self.__crates.remove( crate )
            self.__ncrates -= 1
        else:
            raise TypeError("Unable to find Crate " + item + ".")


    def _get_crate_trash(self):
        return self.__crate_trash[:]


    def set_ncrates(self, val):
        """
        Sets the number of crates.
        """
        self.__ncrates = val


    def set_current_crate(self, val):
        """
        Sets the number of the dataset's current crate.
        """
        self.__current_crate = val


    def set_rw_mode(self, mode):
        """
        Sets whether the dataset is readable, writeable, or both.
        """
        if not isinstance(mode, str):
            raise AttributeError("Argument must be a string")

        if ( (mode != "rw") and (mode != "r") and (mode != "w") ):
            raise AttributeError("Bad value for argument 'mode' %s"% mode)

        self.__rw_mode = mode


    def is_readable(self):
        """
        Returns True if dataset is readable, False if not.
        """
        if self.__rw_mode.find("r") >= 0:
            return True
        return False


    def is_writeable(self):
        """
        Returns True if dataset is writeable, False if not.
        """
        if self.__rw_mode.find("w") >= 0:
            return True
        return False


    def _get_backend(self):
        """
        Retrieves the pointer to the backend.
        """
        return self.__backend


    def _set_backend(self, backend):
        """
        Sets the pointer to the backend.
        """
        if isinstance(backend, pycr.io.CrateIO):
            self.__backend = backend
        else:
            raise TypeError("Input must be a CrateIO.")


    def _self_test(self, filename, debug=False):
        filename = pycr.convert_2_str(filename)
        retstat = "PASS"

        if debug:
            print("Running CrateDataset smoketest")

        try:
            crateds = CrateDataset()

            tab = pycr.TABLECrate()
            tab.name = "TAB1"

            crateds.add_crate( tab )

            crateds.write( filename, clobber=True )

            ds2 = CrateDataset( filename ) 
            
            tab2 = ds2.get_crate( tab.name )

            if tab2._number != 2:
                retstat = "FAIL"

        except:
            retstat = "FAIL"
            if debug:
                print("ERROR in CrateDataset.")
            pass

        return retstat

