from tqdm import tqdm
import numpy as np

from ..core.coreClass import *
from ..timeseries.timeseriesClass import timeSeries
import ccdprocx

from astropy.time import Time
from astropy.nddata.ccddata import CCDData
import astroalign as aa
from astropy.io import fits
from astropy.wcs import WCS

from astropy.visualization import SqrtStretch
from astropy.visualization.mpl_normalize import ImageNormalize
from astropy.stats import SigmaClip
from photutils import Background2D, MedianBackground

from photutils.aperture import CircularAperture, aperture_photometry, CircularAnnulus


__all__ = ['aicoPhot', 'dracoPhot']

class aicoPhot:

    def __init__(self):
        # TODO make observatory class
        self.temp = None
        # list of calibration frames from disk
        

    
    def calibrate(self, cr,  filter, use_med_cr_removal = False, rb = 0, use_lac_cr_removal = False, scln = False, scln_xy = [1,-1, 1,-1]):
        '''
        calibrate performs 'pre-processing' CCDData calibration to a data stack within a ceres object. This 
        involves Bias and Flatfield correction and optional median value based cosmic ray removal based on the
        ccdprocx.cosmicray_median() method.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to calibrate.
        filter: str
            String representation of the relevent filter.
        use_med_cr_removal: boolean
            controls whether to use cosmic ray removal in calibration, may affect runtime. Default is False.
        rb: int
            the rbox value for ccdprocx.cosmicray_median(). Default is 0.
        use_lac_cr_removal : boolean
            controls whether to use lacosmic ray removal in calibration, may affect runtime. Default is False
            ::NOTE:: the package ccdproc is required.
        scln: boolean
            controls whether to crop out scanlines. Default is False
        scln_xy: array
            array of position values for line cropping. [xmin, xmax, ymin, ymax]. Default is [1, -1, 1, -1]
        '''
        # for bla in series: add bias corrected = True to header
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        flat = stack.flat
        bias = Dorado.ceres[Dorado.ceres_keys[cr]].bias
        c_series = []

        print('Calibrating')
        if use_lac_cr_removal:
                try:
                    import ccdproc
                except Exception:
                    print('Failed to open ccdproc. Is it installed?')
        for im in tqdm(stack.data, colour = 'green'):
            # bar.update()
            im.data = im.data.astype('uint16') 
            flat.data = flat.data.astype('uint16') 
            bias.data = bias.data.astype('uint16') 
            im = ccdprocx.ccd_process(im, master_bias = bias, master_flat = flat)
            if scln:
                im.data = im.data[scln_xy[0]:scln_xy[1], scln_xy[2]:scln_xy[3]]
                im.mask = im.mask[scln_xy[0]:scln_xy[1], scln_xy[2]:scln_xy[3]]
                im.uncertainty = im.uncertainty[scln_xy[0]:scln_xy[1], scln_xy[2]:scln_xy[3]]
            if use_med_cr_removal:
                im = ccdprocx.cosmicray_median(im, rbox = rb)
            if use_lac_cr_removal:
                im == ccdproc.cosmicray_lacosmic(im)
            im.data = im.data.astype('uint16') 
            c_series.append(im)
        # add more flags for header
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].data = c_series
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].calibrated = True
        
    def imarith(self, cr, filter, operator, operand):
        '''
        imarith is a basic replication of the IRAF image arithmatic tool imarith.
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to perform image arithmatic on.
        filter: str
            String representation of the relevent filter.
        operator: string
            String of operator to be used for arithmatic. Supported operations are '+', '-', '/', and '*'
        '''
        # TODO should this be in stack? like have a wrapper here?
        # mod to check datatype using type()
        # mod to remove im_count and make possible to use single image NOTE might already be done
        # mod to accomodate CCDdata object NOTE might already be done
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        for i in range(len(series)):
            if (operator == '+'):
                series[i].data = series[i].data  + operand
            elif (operator == '-'):
                series[i].data = series[i].data - operand
            elif (operator == '/'):
                series[i].data = series[i].data  / operand
            elif (operator == '*'):
                series[i].data = series[i].data  * operand
        
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]] = series
        

    def getWCS(self, cr, filter, alignto = None, cache = True):
        '''
        getWCS obtains WCS information for an image either via previously solved data in the cache or
        by passing the image to astrometryNet via dorado.plate_solve().
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to get WCS information for.
        filter: str
            String representation of the relevent filter.
        alignto: int
            Index of image to use as reference. Default is stack.alignto
        cache: boolean
            Controls whether to call astrometryNet for solve or use solved result stored in cache 
            from previous run. Default is True.
        '''
        # TODO mod so cache results are target specific
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        if alignto == None:
            alignto = series.alignTo
        if cache:
            hdulist = fits.open(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits') 
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(hdulist[0].header, hdulist)
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = CCDData.read(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits')
            hdulist.close()
        else:
            toalign = series.data[alignto]
            fname, cachedir = Dorado.mkcacheObj(toalign, 'astrometryNet')
            path = [cachedir, fname]
            writearray = [cachedir, 'solved.fits']
            solved, wcs_header = Dorado.plate_solve(path, writearray = writearray)
            Dorado.delcacheObj( fname, 'astrometryNet')
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(wcs_header)
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = solved
    
    def align(self, cr, filter, alignto = None, getWCS = True, cache = False, ds = 2, ma = 5):
        '''
        align aligns a filter stack within a specified ceres object to a specified frame (default is first
        frame). align can optionally retrieve the corresponding WCS data for the aligned stack.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to align.
        filter: str
            String representation of the relevent filter.
        alignto: int
            Index of image to use as reference. Default is stack.alignto
        getWCS: boolean
            Controls whether to obtain WCS information for stack.
        cache: boolean
            Controls whether to call astrometryNet for solve or use solved result stored in cache 
            from previous run. Default is True.
        ds: int or float
            Sets the detection sigma value for astroalign.register. Default is 2.
        ma: int or float
            Sets the minimum area value for astroalign.register. Default is 5.
        '''
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        
        if alignto == None:
            alignto = series.alignTo
        else: 
            series.alignTo = alignto
        toalign = series.data[alignto]
        ## TODO :: make this use ceres.getWCS()
        if getWCS:
            if cache:
                toalign =  CCDData.read(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits', unit = Dorado.unit)
                hdulist = fits.open(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits') 
                Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(hdulist[0].header, hdulist)
                hdulist.close()
                Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = toalign
            else:
                fname, cachedir = Dorado.mkcacheObj(toalign, 'astrometryNet')
                path = [cachedir, fname]
                writearray = [cachedir, 'solved.fits']
                solved, wcs_header = Dorado.plate_solve(path, writearray = writearray)
                toalign = solved
                Dorado.delcacheObj( fname, 'astrometryNet')
                Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(wcs_header)
                Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = solved
                # delete cache object
                # save solved to target

        aa_series = []
        skipped = []

        print('Aligning')
        for image in tqdm(series.data, colour = 'green'):
            # bar.update()
            try:
                img, _ = aa.register(image.data, toalign.data, detection_sigma = ds, min_area = ma)
                aaim = image
                aaim.data = img
                aa_series.append(aaim)
            except:
                skipped.append(image)
                # print('Image skipped')
        if len(skipped) != 0:
            print(len(skipped), ' images skipped.')
            ## TODO :: need to redo times and such for less ims

        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].data = aa_series
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].aligned = True
    
    def differential_magnitude(self, flux1, flux2, mag2);
        '''
        Probably could use target class inputs and uncertainties
        '''
        mag1 = -2.5 * np.log10(flux1/flux2) + mag2
        return mag1
        
    def apPhot(self, cr, filter, toid, control_toid = None, shape = 21, unc = 0.1):
        '''
        apPhot performs basic aperture photometry based on photutils.aperture_photometry on a target
        within stack. Target photometry can optionally be compared to a control target within the stack 
        via the differential photometry method.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to perform aperture photometry on.
        filter: str
            String representation of the relevent filter.
        toid: string

        control_toid: string

        shape: int

        unc: float

        '''
        # TODO this needs a better name
        # TODO get seeing from PSF
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        # TODO if no wcs, complain alot
        w = stack.wcs
        toi = Dorado.targets[Dorado.target_keys[toid]]
        xy = w.wcs_world2pix(toi.coords.ra.deg, toi.coords.dec.deg, 1)
        ra = toi.coords.ra.deg
        dec = toi.coords.dec.deg
        pos = [(float(xy[0]), float(xy[1]))]
        aperture = CircularAperture(pos, r = shape)
        annulus_aperture = CircularAnnulus(pos, r_in = shape + 2, r_out = shape + 5)
        apers = [aperture, annulus_aperture]

        # TODO can a control be chosen? can more than one be chosen?
        if control_toid != None:
            control_toi = Dorado.targets[Dorado.target_keys[control_toid]]
            xyc = w.wcs_world2pix(control_toi.coords.ra.deg, control_toi.coords.dec.deg, 1)
            posc = [(float(xyc[0]), float(xyc[1]))]
            aperturec = CircularAperture(posc, r = shape)
            annulus_aperturec = CircularAnnulus(posc, r_in = shape + 2, r_out = shape + 5)
            apersc = [aperturec, annulus_aperturec]


        timestr = []
        exptimes = []
        ray = []
        decx = []
        x = []
        y = []
        flux = [] # NOTE this is exptime corrected
        fluxunc = []
        apsum = [] # NOTE this is raw aperture sum
        apsum_unc = []


        print('Performing photometry')
        for image in tqdm(stack.data, colour = 'green'):
            error = unc * image.data
            results = aperture_photometry(image, apers) #, error = error)
            bkg_mean = results['aperture_sum_1'] / annulus_aperture.area
            bkg_sum = bkg_mean * aperture.area
            results['flux_fit'] = results['aperture_sum_0'] - bkg_sum
            
            timestr.append(image.header['DATE-OBS']) # TODO This is most likely needed, but verify 
            exptimes.append(image.header['EXPTIME']) # TODO is this also needed
            ray.append(ra) # TODO is this really needed?
            decx.append(dec)
            x.append(results['xcenter'][0]) # TODO is this needed either?
            y.append(results['ycenter'][0])

            if control_toid != None:
                resultsc = aperture_photometry(image, apersc) # , error = error)
                bkg_meanc = resultsc['aperture_sum_1'] / annulus_aperturec.area
                bkg_sumc = bkg_meanc * aperturec.area
                resultsc['flux_fit'] = resultsc['aperture_sum_0'] - bkg_sumc

                apsum.append(results['flux_fit'][0] - resultsc['flux_fit'][0])
                flux.append((results['flux_fit'][0] - resultsc['flux_fit'][0])/image.header['EXPTIME'])
            else:
                apsum.append(results['flux_fit'][0])
                flux.append(results['flux_fit'][0]/image.header['EXPTIME'])

            fluxunc.append(1) ## TODO:: modify this to account for exposure time and control
            apsum_unc.append(1)
        times = Time(timestr)
        ts = timeSeries(times = times, flux = flux, exptimes = exptimes, x = x, y = y, 
        ra = ray, dec = decx, flux_unc = fluxunc, apsum = apsum, apsum_unc = apsum_unc)

        Dorado.targets[Dorado.target_keys[toid]].filters[filter] = len(toi.ts)
        Dorado.targets[Dorado.target_keys[toid]].ts.append(ts) 
        # TODO accomodate targets embedded in core (list of targets)
        # TODO the name for this function needs updating
        
    def mkBase(self, cr, filter, sigClip = False, minmax = False):
        '''
        mkBase stacks filter data within a ceres object to produce a base or 'average' stacked image.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to create a base stacked image for.
        filter: str
            String representation of the relevent filter.
        sigClip: boolean

        minmax: boolean

        '''
        ## TODO :: add the option to change the combination method. Right now default is 
        # sigma clipped median combination.
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        # toalign = series.data[series.alignTo]

        c = ccdprocx.Combiner(series.data)
        if minmax:
            c.minmax_clipping(min_clip = 0.1)
        if sigClip:
            c.sigma_clipping()
        base = c.median_combine()
        base.header['stacked'] = True
        base.header['numsubs'] = len(series.data)
        base.header['DATE-OBS'] = series.data[0].header['DATE-OBS']
        base.header['filter'] = series.filter

        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].base = base
        ## TODO :: sort out what is in the header of this base file.

        
    def calBase(self, cr, filter):
        '''
        calBase calibrates a base image for a stack by computing a 2D background for the image
        and removing it from the base image.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to perform base image calibration on.
        filter: str
            String representation of the relevent filter.
        '''
        # TODO this needs hella optimization and direction
        # I am 99% sure this is to remove the background gradient from the stacked base image
        img = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].base
        norm = ImageNormalize(stretch=SqrtStretch())
        sigma_clip = SigmaClip(sigma=3.)
        bkg_estimator = MedianBackground()
        bkg = Background2D(img, (50, 50), filter_size=(3, 3), sigma_clip=sigma_clip, bkg_estimator=bkg_estimator)
        base = img
        base.data = img.data / bkg.background.value
        base.data[np.isnan(base.data)] = 0
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].base = base


from astroquery.gaia import Gaia
import astropy.units as u
from astropy.table import Table
import time
from astropy.coordinates import SkyCoord
from scipy.ndimage import median_filter
import skimage.exposure as skie
from skimage.feature import blob_dog, blob_log, blob_doh
# https://scikit-image.org/docs/stable/api/skimage.feature.html#skimage.feature.blob_log
# https://scikit-image.org/docs/stable/auto_examples/features_detection/plot_blob.html
# StarSeeker is a method inspired by the above links and developed
# For the precursor to DORADO, DRACO circa 2019.
class dracoPhot:
    def __init__(self):
        #TODO make observatory class
        self.temp = None
        # list of calibration frames from disk
    def getWCS(self, cr, filter, alignto = None, cache = True):
        '''
        getWCS obtains WCS information for an image either via previously solved data in the cache or
        by passing the image to astrometryNet via dorado.plate_solve().
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to get WCS information for.
        filter: str
            String representation of the relevent filter.
        alignto: int
            Index of image to use as reference. Default is stack.alignto
        cache: boolean
            Controls whether to call astrometryNet for solve or use solved result stored in cache 
            from previous run. Default is True.
        '''
        # TODO mod so cache results are target specific
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        if alignto == None:
            alignto = series.alignTo
        if cache:
            hdulist = fits.open(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits') 
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(hdulist[0].header, hdulist)
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = CCDData.read(Dorado.dordir / 'cache' / 'astrometryNet' / 'solved.fits')
            hdulist.close()
        else:
            toalign = series.data[alignto]
            fname, cachedir = Dorado.mkcacheObj(toalign, 'astrometryNet')
            path = [cachedir, fname]
            writearray = [cachedir, 'solved.fits']
            solved, wcs_header = Dorado.plate_solve(path, writearray = writearray)
            Dorado.delcacheObj( fname, 'astrometryNet')
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].wcs = WCS(wcs_header)
            Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]].solved = solved
    
    def imarith(self, cr, filter, operator, operand):
        '''
        imarith is a basic replication of the IRAF image arithmatic tool imarith.
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to perform image arithmatic on.
        filter: str
            String representation of the relevent filter.
        operator: string
            String of operator to be used for arithmatic. Supported operations are '+', '-', '/', and '*'
        '''
        # TODO should this be in stack? like have a wrapper here?
        # mod to check datatype using type()
        # mod to remove im_count and make possible to use single image NOTE might already be done
        # mod to accomodate CCDdata object NOTE might already be done
        series = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        for i in range(len(series)):
            if (operator == '+'):
                series[i].data = series[i].data  + operand
            elif (operator == '-'):
                series[i].data = series[i].data - operand
            elif (operator == '/'):
                series[i].data = series[i].data  / operand
            elif (operator == '*'):
                series[i].data = series[i].data  * operand
        
        Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]] = series
    
    def differential_magnitude(self, flux1, flux2, mag2);
        '''
        Probably could use target class inputs and uncertainties
        '''
        mag1 = -2.5 * np.log10(flux1/flux2) + mag2
        return mag1
        
    def apPhot(self, cr, filter, toid):
        '''
        apPhot performs basic aperture photometry based on photutils.aperture_photometry on a target
        within stack. Target photometry can optionally be compared to a control target within the stack 
        via the differential photometry method.
        
        Parameters
        ----------
        cr: string
            The relevent name string of Ceres instance to perform aperture photometry on.
        filter: str
            String representation of the relevent filter.
        toid: string


        '''
        # TODO this needs a better name
        # TODO get seeing from PSF
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        # TODO if no wcs, complain alot
        w = stack.wcs
        self.get_stars(cr, filter, toid)
        projectdir = Dorado.dordir / 'data' / 'projects' / 'toid' /
        os.makedirs(projectdir, exist_ok = True)
        out_filename_prefix  = toid + '_'
        for i in range(len(stack.data)):
            im = stack.data[i]
            tstr = Time(im.header['DATE-OBS'], format='fits').mjd
            imname = tstr + '_' + str(i)
            imPhot = photo(im, self.stars, w)
            imPhot.apPhot()
            imPhot.get_zero_point()
            imPhot.mag_calibrate()
            imPhot.write(projectdir + out_filename_prefix + imname + '.fits', overwrite = True)
        # write out a summary table

    def inIm(self, tab, cr, filter, border = 0):
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        height, width = stack.data[stack.alignTo].shape
        num = len(tab)
        tab  = tab[tab['y']     >=      0 + border]
        tab  = tab[tab['y']     <= height - border]
        tab  = tab[tab['x']     >=      0 + border]
        tab  = tab[tab['x']     <=  width - border]

        print('Went from ', num , ' stars in input, to ', len(tab), ' stars in field area.')
        return tab

    def get_field(self, cr, filter, toid, limit_Mag = 16, search_bounds = [30, 20]):
        startTime = time.time()
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        w = stack.wcs
        im = stack.data[stack.alignTo]
        toi = Dorado.targets[Dorado.target_keys[toid]]
        xy = w.wcs_world2pix(toi.coords.ra.deg, toi.coords.dec.deg, 1)
        width = u.Quantity(search_bounds[0], u.arcmin)
        height = u.Quantity(search_bounds[1], u.arcmin)
        coords = SkyCoord.from_name(toi) 
        # Make the Query
        # Columns we want to keep
        columnse = [ 'phot_g_mean_mag', 'phot_bp_mean_mag', 'phot_rp_mean_mag', 'teff_val', 'dist']
        # and what we want to name those columns
        columns = ['g_mag', 'bp_mag', 'rp_mag', 'teff', 'dist']
        # Result return limit, default was 50
        Gaia.ROW_LIMIT = 4000
        re = Gaia.query_object(coordinate=coords, width=width, height=height) 
        # Parse the results
        de = [str(d) for d in re['DESIGNATION']] # This fixes the bad return from Gaia
        # Construct the star object table
        r = Table((de, re['ra'], re['dec']), names = ('DESIGNATION', 'ra', 'dec'))
        # Fill in rest of columns
        for (s, se )in zip(columns, columnse):
            r[s] = re[se]
        # Limit the results to a set magnitude
        r = r[r['rp_mag'] <= limit_Mag]
        # Get stellar object pixel position
        r['x'], r['y'] = wcs.world_to_pixel(SkyCoord(r['ra'], r['dec']))
        # This is a hardcoded scale size for plotting
        r['r'] = 28 - r['rp_mag'].value # Do we keep it?
        # Figure out which results are in the image
        r = self.inIm(r, cr, filter, 100)
        # Round of the sig figs 
        r.round(4)
        # output runtime
        executionTime = np.round((time.time() - startTime), 3)
        print('Execution time in seconds for Gaia lookup: ' + str(executionTime))
        return r

    def starSeeker(self, cr, filter):
        startTime = time.time()
        stack = Dorado.ceres[Dorado.ceres_keys[cr]].data[Dorado.ceres[Dorado.ceres_keys[cr]].filters[filter]]
        w = stack.wcs
        im = stack.data[stack.alignTo]
        data = im.data
        mf = median_filter(data, size= 15)
        datamf = data - mf
        limg = np.arcsinh(datamf)
        limg = limg / limg.max()
        low = np.percentile(limg, 1)
        high = np.percentile(limg, 99.5)
        opt_img  = skie.exposure.rescale_intensity(limg, in_range=(low,high))
        # Laplacian blob detection
        stars =  blob_log(opt_img, max_sigma=25, min_sigma = 5, num_sigma=10, threshold=.2)
        # Convert from sigma to radii in the 3rd column.
        stars[:, 2] = stars[:, 2] * np.sqrt(2)
        y, x, r = stars[:, 0], stars[:, 1], stars[:, 2]
        results = Table((x, y, r), names = ('x', 'y', 'r'))
        co = wcs.pixel_to_world(stars[:,1], s[:,0])
        results['ra']  = co.ra
        results['dec'] = co.dec
        results = self.inIm(results, cr, filter, 100)
        print('Found ', len(results), ' stars via starseeker.')
        executionTime = np.round((time.time() - startTime), 3)
        print('Execution time in seconds for starSeeker: ' + str(executionTime))
        img = im.copy()
        img.data = opt_img
        return results, img
    
    def get_stars(self, cr, filter, toid, limit_Mag = 16, search_bounds = [30, 20])
        # ask if stars should be saved with  flag
        gaia_stars = self.get_field(cr, filter, toid, limit_Mag, search_bounds)
        s3 = self.starSeeker(cr, filter)
        gaia_stars['detection_separation'] = np.zeros(len(gaia_stars))
        gaia_stars['detection_x'] = np.zeros(len(gaia_stars))
        gaia_stars['detection_y'] = np.zeros(len(gaia_stars))
        gaia_stars['detection_r'] = np.zeros(len(gaia_stars))
        matched = Table(names = gaia_stars.colnames, dtype = gaia_stars.dtype)
        for s in gaia_stars:
            sm = self.match_star(s, s3)
            matched.add_row(sm)
        self.stars = matched # should this really be an internal list
        # Or should it belong to anoter class
        projectdir = Dorado.dordir / 'data' / 'projects' / 'toid' /
        os.makedirs(projectdir, exist_ok = True)
        self.stars.write(projectdir + 'stars.fits', overwrite = True)

    def match_star(self, star, s3):
        sx, sy = star['x'], star['y']
        sr = star['r']
        separation = np.sqrt((sx - s3['x'])**2 + (sy - s3['y'])**2)
        candidate = s3[separation <= sr]
        sep = separation[separation <= sr]
        star['detection_separation'] = sep
        star['detection_x'] = candidate['x']
        star['detection_y'] = candidate['y']
        star['detection_r'] = candidate['r']
        return star




## TODO :: figure out how to handle aligning and processing planetary images and image with low star counts



from photutils.aperture import CircularAperture, aperture_photometry
def mag(flux, exp = 1, unc = None):
    mag_inst = -2.5 * np.log10(flux / exp)
    if unc:
        mag_inst_unc = unc / (flux * 2.30258509)
        return mag_inst, mag_inst_unc
    else:
        return mag_inst

class photo:
    '''
    Modify this to account for annulus
    '''
    def __init__(self, image, stars, wcs):
        self.image = image
        self.stars = stars
        self.wcs = wcs
    
    def apPhot(self):
        self.stars['aperture_sum'] = np.zeros(len(self.stars))
        self.stars['inst_mag'] = np.zeros(len(self.stars))
        for i in range(len(self.stars)):
            pos = (self.stars[i]['x'], self.stars[i]['y'])
            aperture = CircularAperture(pos, r=self.stars[i]['detection_r'])
            phot_table = aperture_photometry(self.image.data, aperture)
            self.stars[i]['aperture_sum'] = phot_table['aperture_sum']
            self.stars[i]['inst_mag'] = mag(phot_table['aperture_sum'], 20) # 20 second long exposures
    
    def get_zero_point(self):
        stars = self.stars[self.stars['inst_mag'] <= -2.3]
        self.zero_point_vals = np.polyfit(stars['inst_mag'], stars['rp_mag'], 1)
        self.zero_point = np.poly1d(self.zero_point_vals)
    
    def mag_calibrate(self):
        self.stars['fit_mag'] = self.zero_point(self.stars['inst_mag'])
    
    def write(self, filename):
        self.stars.write(filename)





# class tessPhot:
    # def __init__(self):
    #     self.temp = None

    # def dorphot(self, cere, filter, aperture_shape, annulus_shape):
    #     stack = cere.data[cere.filters[filter]]
        
    #     if stack.ts == []:
    #         #tess_bjds, make sure to read them in as time objects for cere
    #         stack.ts = timeSeries()
    #     ts = stack.ts

    #     perture = RectangularAperture((5,4), 3,3) 
    #     annulus_aperture = RectangularAnnulus((5,4), 4, 5, 5) 
    #     # Combine the aperture
    #     aps = [aperture , annulus_aperture]

    #     for s in tqdm(range(stack.length), colour = 'green'):
    #         # Place the aperature on the current stamp and perform aperture photometry
    #         phot_table = aperture_photometry(calibrated_fluxes[s,:,:], aps)
    #         # Compute the background values from the annulus photometry
    #         bkg_sum = (phot_table['aperture_sum_1'] / annulus_aperture.area) * aperture.area
    #         # Subtract the background from main aperture
    #         phot_table['residual_aperture_sum'] = phot_table['aperture_sum_0'] - bkg_sum
    #         # Store the aperture results 
    #         aperture_sums.append(float(phot_table['aperture_sum_0']) - float(bkg_sum))
    #         # Store the flux error
    #         err.append(np.mean(flux_err[s,:,:]))
