import numpy as np
import matplotlib.pyplot as plt
import matplotlib.pylab as pl
from matplotlib.colors import LogNorm, Normalize
import matplotlib as mpl
from astropy.io import fits, ascii
from astropy.modeling import models
from astropy import modeling
from astropy import units as u
# from kpicdrp import background, trace, extraction
import kpic_background as background
import kpic_trace as trace
import kpic_extraction as extraction
import os
from scipy import signal
import scipy.optimize as opt
import scipy
import multiprocessing as mp
from Nirspec_cmds import Spec_cmds
from Star_Tracker_cmds import Tracking_cmds
from Track_Cam_cmds import TC_cmds
from FAM_cmds import FAM_cmds
import Acquisition
import ktl
import Organization
import sys
import time
import warnings
import datetime
from time import sleep
from scipy.signal import medfilt as medfilt

'''
Required dependencies;
    kpic_background.py
    kpic_trace.py
    kpic_extraction.py
    hip81497_200602_sf2_wvsoln.txt
    2massK.txt
'''

### Get tons of these from extraction/rectification due to bad pixels
warnings.filterwarnings('ignore', category=RuntimeWarning)

# print('Current save directory:', Organization.get_path()[0])
cam   = TC_cmds()
track = Tracking_cmds()
FAM  = FAM_cmds()
spec  = Spec_cmds()
pd_sensitivity = 10
nreads = 10 #100

# connect to flip mirror keyword
serv = ktl.Service("kpic")
serv["PDPONAM"].monitor()

### These are some parameters that probably don't need to change often
N_order = 7
N_fiber = 1
nskip = 4 ### Only fit every 4 points for speed when using KPIC DRP, still give good enough trace after smoothing
itime = 1500 #30*1000
coadds = 1

def _get_cred2_flux(plot=False, nb_im=50):
    # return 1
    A = np.zeros(nb_im)
    for i in range(A.size):
        ### Get the processed cred2 image
        procim = track.Img.get_data(reform=True)
        ### Get location of psf
        goal = track.get_psf_goal()
        ### Check SHM tha tthese are right
        xcen = int(np.round(goal[1]))
        ycen = int(np.round(goal[2]))

        ### cutout - size tbd
        cutim = procim[ycen-50:ycen+51, xcen-50:xcen+51]
        if plot:
            plt.imshow(cutim, extent=[xcen-50, xcen+50, ycen-50, ycen+50])
            plt.show() ### plot to check

        totcnts = np.nansum(np.nansum(cutim))#/cutim.size
        ### Divide by itime to get flux
        A[i] = totcnts/cam.get_tint()
        # time.sleep(0.1)

    return np.mean(A)

def _acquire_fiber(fibr):
    '''
    Change to a new science fiber. If not given a valid fiber number, does nothing
    '''
    if fibr in [1,2,3,4]:
        track.set_goal("scf{}".format(fibr))
        # wait for tracking script to update goal
        while track.get_psf_goal()[3] != fibr:
            time.sleep(.1)

        start_t = time.time()
        # wait for a valid psf to be within .5 pixels in x and y of target
        while True:
            # calculate the dots to print based on how long we've been running
            dots = "."*((int(time.time()) - int(start_t))%3+1)
            # clear line
            sys.stdout.write(u"\u001b[2K")
            sys.stdout.write("\rfiber {} | acquiring".format(fibr)+dots)
            sys.stdout.flush()
            # get location of psf
            psfx, psfy = track.get_psf_cent()
            # get target location
            goal = track.get_psf_goal()

            # if we're close enough to goal, break
            if track.get_psf_parameters()[0] and abs(psfx - goal[1]) < .5 and abs(psfy - goal[2]) < .5:
                return 1
    else:
        return 0

def _rectify(cube, badpixmap, N_order, N_fiber, bkgd=np.zeros(0)):
    '''
    Flattens and rectifies a cube for peak finding
    '''
    ### Flatten and get rid of bad pixels
    deg = 4
    ### Badpixmap caussing issues, skipping for now
    # print('cube shape', cube.shape)
    # image = np.nansum(cube, axis=0) * badpixmap
    # Median instead of sum
    if bkgd.shape[0] == 0:
        image = np.nanmedian(cube, axis=0) * badpixmap
    else:
        image = (np.nanmedian(cube, axis=0) - bkgd) * badpixmap
    # plt.imshow(image, norm=LogNorm(vmin=1,vmax=1000))
    # plt.show()

    cind = int(image.shape[1]/2)
    nrows = image.shape[0]
    rowinds = np.arange(nrows)
    lind = cind-2
    rind = cind+2
    rectIm = np.empty(np.shape(image))
    rectIm[:,cind] = image[:,cind]
    # rectIm[:,cind] = np.nansum(image[:,cind-4:cind+5],axis=1) ### start from the middle, avoid edge effects
    rectCoeffs = np.empty((deg+1,image.shape[1]))
    init = np.zeros(deg+1)
    init[-2] = 1.
    rectCoeffs[:,cind] = init ### coeffs of the identity polynomial

    ### Find full peaks, then retain only the best N_orders*N_fibers
    if N_fiber==1:
        dist = 120
    else:
        dist = 15
    fullpeaks = signal.find_peaks(np.nansum(image[:,cind-4:cind+5],axis=1), prominence=1e2, distance=dist)
    peaks = fullpeaks[0]

    # plt.plot(rectIm[:,cind])
    # for peak in peaks:
    #   plt.axvline(peak)
    # plt.show()

    proms = fullpeaks[1]['prominences']
    while peaks.size > N_order*N_fiber:
        ### Toss partial traces at detector edge
        if min(peaks) < 40:
            worstpeak = np.argmin(peaks)
        else:
            worstpeak = np.argmin(proms)
        proms = np.delete(proms, worstpeak)
        peaks = np.delete(peaks, worstpeak)
    cpeaks = peaks
    ordcenters = [np.mean(cpeaks[i:i+N_fiber]) for i in range(0,N_order*N_fiber,N_fiber)]

    ### Loop over full image, in chunks
    rectfails = []
    fitinds = []
    while rind < image.shape[1] and lind > 0:
        ### Get left slice
        lcol = np.nanmedian(image[:,lind-4:lind+5], axis=1)
        lpeaks = signal.find_peaks(lcol, prominence=1e2, distance=dist)
        ### If missing peaks, skip
        if len(lpeaks[0]) < len(cpeaks):
            rectfails.append(lind)
            rectCoeffs[:,lind] = np.nan*np.ones(deg+1)
            rectIm[:,lind] = np.nan*np.ones(rowinds.size)
        else:
            peaks = lpeaks[0]
            ### If we get extra peaks, drop the weakest ones
            if len(peaks) > len(cpeaks):
                proms = lpeaks[1]['prominences']
                while len(peaks) > len(cpeaks):
                    ### Keep the N_fibers peaks closest to the center of each order, throw out rest
                    goodInds = np.empty(N_order*N_fiber)
                    for i in range(len(ordcenters)):
                        center = ordcenters[i]
                        inds = np.argsort([np.abs(peak-center) for peak in peaks])[:N_fiber]
                        goodInds[i*N_fiber:(i+1)*N_fiber] = inds
                    goodInds = np.sort(goodInds).astype(int)
                    peaks = peaks[goodInds]
                lpeaks = peaks
            else:
                lpeaks = lpeaks[0]
            ### Find polynomial to line up with reference peaks
            # print(lpeaks, cpeaks)
            lcoeffs = np.polyfit(lpeaks, cpeaks, deg=deg)
            rectCoeffs[:,lind] = lcoeffs
            lpoly = np.poly1d(lcoeffs)
            rectIm[:,lind] = np.interp(rowinds, lpoly(rowinds), lcol)

        ### Save polynomial coefficients
        fitinds.append(lind)
        lind-=4

        ### Get right slice
        rcol = np.nanmedian(image[:,rind-4:rind+5], axis=1)
        rpeaks = signal.find_peaks(rcol, prominence=1e2, distance=dist)
        ### If missing peaks, skip
        if len(rpeaks[0]) < len(cpeaks):
            rectfails.append(rind)
            rectCoeffs[:,rind] = np.nan*np.ones(deg+1)
            rectIm[:,rind] = np.nan*np.ones(rowinds.size)
        else:
            peaks = rpeaks[0]
            ### If extra peaks, drop weakest ones
            if len(peaks) > len(cpeaks):
                proms = rpeaks[1]['prominences']
                while len(peaks) > len(cpeaks):
                    ### Keep the N_fibers peaks closest to the center of each order, throw out rest
                    goodInds = np.empty(N_order*N_fiber)
                    for i in range(len(ordcenters)):
                        center = ordcenters[i]
                        inds = np.argsort([np.abs(peak-center) for peak in peaks])[:N_fiber]
                        goodInds[i*N_fiber:(i+1)*N_fiber] = inds
                    goodInds = np.sort(goodInds).astype(int)
                    peaks = peaks[goodInds]
                rpeaks = peaks
            else:
                rpeaks = rpeaks[0]
            ### Find polynomial to line uup with reference peaks
            rcoeffs = np.polyfit(rpeaks, cpeaks, deg=deg)
            rectCoeffs[:,rind] = rcoeffs
            rpoly = np.poly1d(rcoeffs)
            rectIm[:,rind] = np.interp(rowinds, rpoly(rowinds), rcol)
        ### Save polyomial coefficients
        fitinds.append(rind)
        rind+=4

    if len(rectfails) > 0:
        pass
        #print('Rectification failed in', len(rectfails), 'columns:', np.sort(rectfails))

    rectIm[np.isnan(rectIm)] = 0.0

    # plt.imshow(rectIm, norm=LogNorm(vmin=1,vmax=100))
    # plt.show()

    return rectIm, rectCoeffs, cpeaks

def _find_trace(rImage, rCoeffs, goodPeaks, N_order, N_fiber):
    '''
    Given a rectified image, rectification coefficients, peaks, and orders/fibers, finds the traces
    '''
    peaks = goodPeaks
    traces = np.empty((N_order*N_fiber, rCoeffs.shape[1]))
    for i in range(rCoeffs.shape[1]):
        ### Avoid weird things at the edges and bad pixels
        if np.any(np.isnan(rCoeffs[:,i])) or i < 20 or i > 2028:
            traces[:,i] = np.nan*np.ones(N_order*N_fiber)
            continue
        ### Undo the rectification
        for j in range(N_order*N_fiber):
            pcoeff = rCoeffs[:,i]
            pcoeff[np.abs(pcoeff)<1e-16] = 0.
            ### Solve the polynomial
            poly = np.poly1d(pcoeff)
            try:
                roots = (poly-peaks[j]).roots
            except:
                traces[j,i] = np.nan
                continue
            roots = np.real(roots[np.isreal(roots)])
            root = roots[roots>30] ### Reject bad answers near edges
            if root.size==1: traces[j,i] = float(root)
            elif root.size>1: traces[j,i] = np.min(root-peaks[j])
            else: traces[j,i] = np.nan
        ### Somehow these slip through
        if np.any(np.abs(traces[:,i]-peaks) > 100):
            traces[:,i] = np.nan*np.ones(N_order*N_fiber)
        ### Every now and fibers can switch
        for k in range(len(traces[:,i])-1):
            if traces[k,i] > traces[k+1,i]:
                traces[:,i] = np.nan*np.ones(N_order*N_fiber)
                continue
    ### Get rid of clear outliers that accumulate around edges
    traces[np.abs(traces)>2008] = np.nan
    traces[traces<40] = np.nan

    ### Polynomial nterpolate over NaNs
    for i in range(N_order*N_fiber):
        order = traces[i]
        inds = np.arange(order.size)
        ordfit = np.poly1d(np.polyfit(inds[~np.isnan(order)], order[~np.isnan(order)], deg=4))
        traces[i] = ordfit(inds)

    return traces

def _extract_fluxes(datacube, bkgd_noise, badpixmap, trace_locs, trace_sigs):
    pool = mp.Pool(4)
    flux_lst = []
    for i in range(len(datacube)):
        nframes = str(len(datacube))
        txt = '%3.0f / '%(i+1) + nframes
        sys.stdout.write('\rFlux extraction, frame'+txt)
        sys.stdout.flush()
        image = datacube[i]*badpixmap
        ### Get the diwth and location for each frame
        trace_sig = trace_sigs[i,:,:,:]
        trace_loc = trace_locs[i,:,:,:]

        ### This part was needed to get the flux extraction to work, it breaks if there's only one fiber
        ### illuminated (at least I think that's what's happening). Creates a dummy second fiber.
        if trace_loc.shape[0] == 1:
            arr = 20+trace_loc
            trace_loc = np.append(trace_loc, arr,axis=0)
            trace_sig = np.append(trace_sig, trace_sig, axis=0)
        ### Background traces for DRP
        trace_loc_slit, trace_loc_dark = trace.get_background_traces(trace_loc)
        ### More DRP setup
        trace_loc_wbkg = np.concatenate([trace_loc, trace_loc_slit, trace_loc_dark], axis=0)
        trace_sigmas_wbkg = np.concatenate([trace_sig, trace_sig, trace_sig], axis=0)
        trace_flags = np.array([0, ] * trace_loc.shape[0] + [1, ] * trace_loc.shape[0] + [2, ] * trace_loc.shape[0])
        ### DRP frame processing
        fluxes, errors_extraction, errors_bkgd_only = extraction.extract_flux(image, trace_loc_wbkg, trace_sigmas_wbkg,
                                                                          output_filename=None,
                                                                          img_hdr=None,
                                                                          img_noise=bkgd_noise, fit_background=True,
                                                                          trace_flags=trace_flags, pool=pool,
                                                                          bad_pixel_fraction=0.01,
                                                                          box=True)
        flux_lst.append(fluxes)
    print('')
    int_fluxes = np.nansum(flux_lst, axis=-1)
    ### If averaging < 100 counts/pixel, more likely to run into trace fitting issues
    #if np.nanmax(int_fluxes) < 100*N_order*2048:
        #print('Warining: Low counts on spec, trace fitting may be unreliable. Check source intensity!')
    return np.asarray(flux_lst)

def _extract_fluxes_hybrid(datacube, bkgd_noise, badpixmap, trace_loc):
    '''
    Does DRP flux extraction using trace locations from the faster trace finding code
    '''
    trace_sigs = np.empty((len(datacube), N_fiber, N_order, 2048))
    if datacube.ndim == 3:
        image = np.nansum(datacube, axis=0)
    else: image = datacube

    image[np.isnan(image)] = 0.0
    image[np.isnan(image)] = 0.0
    _, _, trace_sig, _ =_fit_traces_fast(image, trace_loc, N_order, N_fiber)

    flux_lst = []
    pool = mp.Pool(4)
    for i, image in enumerate(datacube):
        nframes = str(len(datacube))
        txt = '%3.0f / '%(i+1) + nframes
        sys.stdout.write('\rExtracting frame'+txt)
        sys.stdout.flush()

        if trace_loc.shape[0] == 1:
            arr = 20+trace_loc
            trace_loc = np.append(trace_loc, arr,axis=0)
            trace_sig = np.append(trace_sig, trace_sig, axis=0)
        ### Background traces for DRP
        trace_loc_slit, trace_loc_dark = trace.get_background_traces(trace_loc)
        ### More DRP setup
        trace_loc_wbkg = np.concatenate([trace_loc, trace_loc_slit, trace_loc_dark], axis=0)
        trace_sigmas_wbkg = np.concatenate([trace_sig, trace_sig, trace_sig], axis=0)
        trace_flags = np.array([0, ] * trace_loc.shape[0] + [1, ] * trace_loc.shape[0] + [2, ] * trace_loc.shape[0])
        ### DRP frame processing
        fluxes, errors_extraction, errors_bkgd_only = extraction.extract_flux(image, trace_loc_wbkg, trace_sigmas_wbkg,
                                                                          output_filename=None,
                                                                          img_hdr=None,
                                                                          img_noise=bkgd_noise, fit_background=True,
                                                                          trace_flags=trace_flags, pool=pool,
                                                                          bad_pixel_fraction=0.01,
                                                                          box=True)
        flux_lst.append(fluxes)
    print('')
    return np.asarray(flux_lst)

def _extract_fluxes_fast(datacube, trace_loc, N_order=7, bpmap=np.ones((2048,2048))):
    trace_sigs = np.empty((len(datacube), N_fiber, N_order, 2048))
    flux_ests = np.empty((len(datacube), N_fiber, N_order, 2048))
    tot_cnts = np.empty((len(datacube), N_fiber, N_order, 2048))
    trace_locs = np.empty((len(datacube), N_fiber, N_order, 2048))

    # TO-DO: look into parallizing this to speed up
    for i, image in enumerate(datacube):
        # nframes = str(len(datacube))
        # txt = '%3.0f / '%(i+1) + nframes
        # sys.stdout.write('\rExtracting frame'+txt)
        # sys.stdout.flush()

        # apply bad pixel map
        goodim = image * bpmap
        goodim[np.isnan(goodim)] = 0.0
        trace_amp, _, trace_sig, tot_cnt =_fit_traces_fast(goodim, trace_loc, N_order, N_fiber)
        # trace_locs[i] = trace_loc
        # flux_ests[i] = trace_sig*trace_amp*np.sqrt(2*np.pi)
        # flux_ests[flux_ests>3e5] = 0.0 ### Reject hot pixel values
        # flux_ests[flux_ests<0] = 0.0 ### Reject bad pixel values\

        trace_sigs[i] = trace_sig
        tot_cnts[i] = tot_cnt

    # print('')
    return tot_cnts

def _fit_traces(datacube, badpixcube, fibers, nskip=4):
    '''
    Use the KPIC DRP to fit each trace for each image in cube. Requires
    a good guess for fiber positions. Nskip determines how many pixels to skip
    in the fitting, setting it higher speeds things up.
    '''
    # print('badpixshape', badpixcube.shape)
    # plt.imshow(badpixcube[0])
    # plt.show()
    nthreads = 4
    ### Get number of orders from fibers
    norder = len(fibers[list(fibers.keys())[0]])
    _, nx, ny = badpixcube.shape
    ### Fit each trace in the cube
    trace_list = []
    for i in range(len(datacube)):
        nframes = str(len(datacube))
        txt = '%3.0f / '%(i+1) + nframes
        sys.stdout.write('\rTrace fitting, frame'+txt)
        sys.stdout.flush()
        ### Setup for DRP tools - expecting list of images
        image = np.expand_dims(datacube[i],axis=0)
        fiber_list = trace.guess_star_fiber(image, fibers)

        ### Fit traces and smooth using KPIC DRP tools
        trace_calib, residuals = trace.fit_trace(fibers,fiber_list,image[:,:,::nskip],badpixcube[:,:,::nskip],ny,int(nx/nskip),norder,numthreads=nthreads, fitbackground=False)
        # polyfit_trace_calib, smooth_trace_calib = trace.smooth(trace_calib)

        ### Save outputs to arrays for plotting
        trace_list.append(trace_calib)
    print('')
    ### Get widths and locations
    trace_list = np.asarray(trace_list)
    # print(trace_list.shape)
    sigs = trace_list[:,:,:,:,1]
    # print(sigs.shape)
    # print(sigs[0,0,1])
    for i in range(len(sigs)):
        try:
            sigs[i] = _fourier_smooth(sigs[i], trace_list.shape[2], trace_list.shape[1])
        except:
            sigs[i] = np.nan*np.ones(np.shape(sigs[i]))
            print('Bad frame', i)

    ### Resample onto full grid if needed
    if sigs.shape[3] != 2048:
        oldshape = sigs.shape
        oldlen = sigs.shape[3]
        oldgrid = np.arange(oldlen)
        newshape = (oldshape[0], oldshape[1], oldshape[2], 2048)
        new_trace_sig = np.empty(newshape)
        newgrid = np.linspace(0, oldlen, num=2048)
        for i in range(newshape[0]):
            for j in range(newshape[1]):
                for k in range(newshape[2]):
                    new_trace_sig[i,j,k] = np.interp(newgrid, oldgrid, sigs[i,j,k])
        sigs = new_trace_sig

    return sigs

def _one_order_fit(traces, goodim, i, j):
    imslice = np.empty((9,2048))
    amps = np.empty(2048)
    for k in range(2048):
        tcen = int(np.round(traces[i,j,k],0))
        cut = np.nansum(goodim[tcen-4:tcen+5,k:k+1], axis=1)

        ### Avoid wackiness at detector edge
        if cut.size != 9 or np.abs(k-1024) > 1020:
            imslice[:,k] = np.nan*np.ones(9)
            amps[k] = np.nan
        else:
            # bkg = np.nanmedian(np.concatenate((cut[:5], cut[:-5])))
            imslice[:,k] = cut #-bkg
            ### Deal with sub-pixel phase by fitting peak
            p1 = np.polyfit(np.arange(3,6), cut[3:6], 2)
            pfit = np.poly1d(p1)
            fitprof = pfit(np.arange(3,6.1,0.1))
            amps[k] = np.nanmax(fitprof)
            # plt.plot(cut)
            # plt.plot(np.arange(3,6.1,0.1), fitprof)
            # plt.show()

    ### Because it's a sampled gaussian, amps will be underestimate on average
    ### Leads to sigs being slightly larger - underestimate resolution
    pshifts = np.abs(np.argmax(imslice, axis=0)-4)
    ### Need to figure out Lorentzian broadening here, thinking it's ~5 percent
    tots = np.nansum(imslice, axis=0)
    sigs = tots/(amps*np.sqrt(2*np.pi))
    amps[pshifts>1.5] = np.nan
    sigs[pshifts>1.5] = np.nan
    sigs[sigs>3] = np.nan

    return amps, sigs, tots

# def _fit_traces_fast_new(goodim, traces, N_order, N_fiber):
#     rawsig = np.empty((N_fiber, N_order, 2048))
#     rawamp = np.empty((N_fiber, N_order, 2048))
#     rawtot = np.empty((N_fiber, N_order, 2048))

#     if traces.ndim == 2:
#         traces = np.expand_dims(traces,axis=0)

#     for i in range(N_fiber):
#         # parallelize N_order calc
#         pool = mp.Pool(processes=N_order)
#         res_list = []
#         for j in range(N_order):
#             res_list.append(pool.apply_async(_one_order_fit, args=(traces, goodim, i, j)))

#         for j in range(N_order):
#             res = res_list[j].get()
#             rawamp[i,j] = res[0]
#             rawsig[i,j] = res[1]
#             rawtot[i,j] = res[2]

#         pool.close()
#     # print(rawsig)
#     ### Fourier smoothing - drop all the high-frequency stuff
#     # smoothamp = _fourier_smooth(rawamp, N_order, N_fiber)
#     smoothsig = _fourier_smooth(rawsig, N_order, N_fiber)
#     return rawamp, rawsig, smoothsig, rawtot

def _fit_traces_fast(goodim, traces, N_order, N_fiber):
    rawsig = np.empty((N_fiber, N_order, 2048))
    rawamp = np.empty((N_fiber, N_order, 2048))
    rawtot = np.empty((N_fiber, N_order, 2048))

    if traces.ndim == 2:
        traces = np.expand_dims(traces,axis=0)

    for i in range(N_fiber):
        for j in range(N_order):
            imslice = np.empty((9,2048))
            amps = np.empty(2048)
            for k in range(2048):
                tcen = int(np.round(traces[i,j,k],0))
                cut = np.nansum(goodim[tcen-4:tcen+5,k:k+1], axis=1)

                ### Avoid wackiness at detector edge
                if cut.size != 9 or np.abs(k-1024) > 1020:
                    imslice[:,k] = np.nan*np.ones(9)
                    amps[k] = np.nan
                else:
                    # bkg = np.nanmedian(np.concatenate((cut[:5], cut[:-5])))
                    imslice[:,k] = cut #-bkg
                    ### Deal with sub-pixel phase by fitting peak
                    p1 = np.polyfit(np.arange(3,6), cut[3:6], 2)
                    pfit = np.poly1d(p1)
                    fitprof = pfit(np.arange(3,6.1,0.1))
                    amps[k] = np.nanmax(fitprof)
                    # plt.plot(cut)
                    # plt.plot(np.arange(3,6.1,0.1), fitprof)
                    # plt.show()

            ### Because it's a sampled gaussian, amps will be underestimate on average
            ### Leads to sigs being slightly larger - underestimate resolution
            pshifts = np.abs(np.argmax(imslice, axis=0)-4)
            ### Need to figure out Lorentzian broadening here, thinking it's ~5 percent
            tots = np.nansum(imslice, axis=0)
            sigs = tots/(amps*np.sqrt(2*np.pi))
            amps[pshifts>1.5] = np.nan
            sigs[pshifts>1.5] = np.nan
            sigs[sigs>3] = np.nan
            rawamp[i,j] = amps
            rawsig[i,j] = sigs
            rawtot[i,j] = tots
    # print(rawsig)
    ### Fourier smoothing - drop all the high-frequency stuff
    # smoothamp = _fourier_smooth(rawamp, N_order, N_fiber)
    smoothsig = _fourier_smooth(rawsig, N_order, N_fiber)

    return rawamp, rawsig, smoothsig, rawtot

def _fourier_smooth(arr, N_order, N_fiber, cutoff=0.004):
    '''
    Fourier-based smoothing for widths
    '''
    arr[arr<0.5] = np.nan
    smootharr = np.empty(arr.shape)
    try:
        for i in range(N_fiber):

            for j in range(N_order):
                # res = res_list[j].get()
                # inds = res[0]
                # goodinds = res[1]
                # newvals = res[2]
                inds = np.arange(arr[i,j].size)
                vals =  arr[i,j]
                badinds = np.isnan(vals)
                goodinds = inds[~badinds]
                goodvals = vals[~badinds]
                # pdb.set_trace()
                ft = np.fft.fft(goodvals)
                freq = np.fft.fftfreq(goodinds.shape[-1])
                ### This has been tuned manually based on fringe patterns
                ### TODO - replace with a proper window to reduce ringing
                ft.imag[np.abs(freq)>cutoff] = 0.
                ft.real[np.abs(freq)>cutoff] = 0.
                newvals = np.fft.ifft(ft)
                smootharr[i,j] = np.interp(inds, goodinds, newvals)
                # plt.plot(vals)
                # plt.plot(smootharr[i,j])
                # plt.show()
        return smootharr
    except ValueError:
        print('Smoothing failed')
        return arr

def set_nspec_reads(tint):

    if 1400 <= tint < 3000:
        n_reads = 1
    elif 3000 <= tint < 4500:
        n_reads = 2
    elif 4500 <= tint < 6000:
        n_reads = 3
    elif 6000 <= tint <= 60000:
        n_reads = 4
    else:
        n_reads = 16
    # CDS is 2
    if n_reads == 1 and not ktl.read('nspec','sampmode') == 'CDS':
        ktl.write('nspec','sampmode', '2')
    # MCDS is 2
    elif n_reads > 1 and not ktl.read('nspec','sampmode') == 'MCDS':
        ktl.write('nspec','sampmode', '3')

    ktl.write('nspec','numreads', str(n_reads))
    time.sleep(0.1)

def _take_backgrounds(itime, coadds, nframes=5, retake=False, Path=None):
    '''
    Take background frames for scan
    '''
    fam_bkgd_pos = [50, 9900]

    ### Path info
    if Path is None:
        Path = Organization.get_path('Spec')
        Path = list(Path)
        Path[1]=Path[1].split('.')[0]

    # Path is actually a list, so get the string
    real_path = Path[0]
    #print(real_path)

    ### Check that backgrounds already exist, if they don't force a retake
    bkg_names = []
    for i in range(nframes):
        bkg_names.append(real_path + 'bkgd_'+str(i)+'_itime'+str(itime)+'.fits')
        if not os.path.isfile(bkg_names[-1]):
            retake = True
    track_flag=False
    if retake:
        # set nreads and sampmode
        set_nspec_reads(itime)

        ### Stop tracking and move off-slit for backgrounds
        if track.is_tracking():
            track_flag=True
            track.stop_tracking()
            # Wait for the Tracking loop to complete its last iteration
            time.sleep(2)
        ### Get initial TTM position
        TTM_CP = FAM.get_pos()
        ### Move TTM and wait for new position
        FAM.set_pos(fam_bkgd_pos, block=True)

        ### Take backgrounds
        for i in range(nframes):
            # Take backgrounds, save for DRP
            spec.TakeImage(Coadds=coadds, tint=itime, Save=True, filename='bkgd_'+str(i)+'_itime'+str(itime), checkbadframe=False)
        FAM.set_pos([TTM_CP[0], TTM_CP[1]], block=True)

    ### Return to original TTM position and restart tracking
    bkgd, smoothed_thermal_noise, badpixmap = background.make_badpixmap(bkg_names)

    if track_flag==True:
        track.start_tracking()

    return bkgd, smoothed_thermal_noise, badpixmap

# Make trace calc separate from flux extract
def _calc_trace(datacube, badpixmap, N_order, N_fiber, bkgd=np.zeros(0), plot=False):

    # datacube should have be numpy array of shape (Nframes, x, y)
    # Rectify images
    rectIm, rectCoeffs, goodPeaks = _rectify(datacube, badpixmap, N_order, N_fiber, bkgd=bkgd)
    trace_locs = _find_trace(rectIm, rectCoeffs, goodPeaks, N_order, N_fiber)

    if plot:
        collapsedim = np.nansum(datacube,axis=0)
        collapsedim[np.isnan(collapsedim)] = 0.
        plt.title('Stacked cube, with traces')
        plt.imshow(collapsedim, norm=LogNorm(vmin=1,vmax=100))
        for ordloc in trace_locs:
          plt.plot(ordloc, color='r', linestyle='--')
        plt.show()

    return trace_locs

def _total_flux_frame(this_image, badpixmap, N_order, N_fiber, bkgd=np.zeros(0), loaded_Trace=np.zeros(0), which_Order=0):
    '''
    Get the total flux on spec for a single image
    '''

    datacube = np.zeros((1, this_image.shape[0], this_image.shape[1]))

    if bkgd.shape[0] == 0:
        datacube[0] = this_image
    else:
        datacube[0] = this_image - bkgd    # remove a master bkgd

    # print('here0')

    # Trace fitting if not given one
    if loaded_Trace.shape[0] == 0:
        # Rectify images
        rectIm, rectCoeffs, goodPeaks = _rectify(datacube, badpixmap, N_order, N_fiber)
        trace_locs = _find_trace(rectIm, rectCoeffs, goodPeaks, N_order, N_fiber)
    else:
        trace_locs = loaded_Trace
        # print('Using input trace locations.')

    # Fast flux extractiong
    if N_fiber == 1:
        trace_locs = np.expand_dims(trace_locs, axis=0)
    # print('here1')

    # flux_lst has shape (Nframes, Nfibers, Norder, 2048)
    flux_lst =_extract_fluxes_fast(datacube, trace_locs, N_order=N_order, bpmap=badpixmap)

    order_flux = np.nansum(flux_lst[0][0][which_Order])

    # print('here2')

    # Get total flux in each frame
    int_flux = np.empty(flux_lst.shape[0])
    for i, frame in enumerate(flux_lst):
        int_flux[i] = np.nansum(np.nansum(frame, axis=2), axis=1)[0]

    return int_flux, order_flux

def _total_flux_framepath(frame_path, badpixmap, N_order, N_fiber, bkgd=np.zeros(0), loaded_Trace=np.zeros(0), which_Order=0):
    '''
    Get the total flux on spec for each image in the cube. Moved here since it's used a bunch
    '''

    ### Load the frame into a datacube
    hdu = fits.open(frame_path)
    this_image = hdu[0].data.T[:,::-1]
    datacube = np.zeros((1, this_image.shape[0], this_image.shape[1]))

    if bkgd.shape[0] == 0:
        datacube[0] = this_image
    else:
        datacube[0] = this_image - bkgd    # remove a master bkgd

    ### Trace fitting if not given one
    if loaded_Trace.shape[0] == 0:
        rectIm, rectCoeffs, goodPeaks = _rectify(datacube, badpixmap, N_order, N_fiber)
        trace_locs = _find_trace(rectIm, rectCoeffs, goodPeaks, N_order, N_fiber)
    else:
        trace_locs = loaded_Trace
        # print('Using input trace locations.')

    # print(trace_locs.shape)
    ### Fast flux extractiong
    if N_fiber == 1:
        trace_locs = np.expand_dims(trace_locs, axis=0)

    # flux_lst has shape (Nframes, Nfibers, Norder, 2048)
    flux_lst =_extract_fluxes_fast(datacube, trace_locs, N_order=N_order, bpmap=badpixmap)

    order_flux = np.nansum(flux_lst[0][0][which_Order])

    ### Get total flux in each frame
    int_flux = np.empty(flux_lst.shape[0])
    for i, frame in enumerate(flux_lst):
        int_flux[i] = np.nansum(np.nansum(frame, axis=2), axis=1)[0]
    ### If averaging < 100 counts/pixel, more likely to run into trace fitting issues
    #if np.nanmax(int_flux) < 100*N_order*2048:
        #print('Warining: Low counts on spec, trace fitting may be unreliable. Check source intensity!')

    return int_flux, order_flux, trace_locs

def _reduce_drp_byframe(datacube, badpixmap, smoothed_thermal_noise,  N_order, N_fiber, stack=False):
    '''
    Get trace widths and fluxes for each frame in the datacube, using KPIC DRP methods (slow)
    '''
    ### Really wants a cube for this

    # badpixmap = np.ones((2048,2048))
    badpixmap = np.ones((2048,2048))
    badpixcube = np.expand_dims(badpixmap, axis=0)
    ### Rectify frame and find trace locations
    # print('Datacube shape', datacube.shape)
    rectIm, rectCoeffs, goodPeaks = _rectify(datacube, badpixmap, N_order, N_fiber)
    ### Check rectified image if desired
    # plt.imshow(rectIm, norm=LogNorm(vmin=1, vmax=100))
    # plt.show()
    traces = _find_trace(rectIm, rectCoeffs, goodPeaks, N_order, N_fiber)
    trace_locs = np.empty((len(datacube), N_fiber, N_order, 2048))
    for i in range(len(datacube)):
        trace_locs[i] = traces

    ### Trace fitting. Currently using KPIC DRP (slow)
    ### This is for the current KPIC DRP trace fitting process
    tracebounds = []
    for order in traces:
        tracebounds.append([int(np.min(order)-20), int(np.max(order)+20)])
    ### Assign to fibers for the DRP
    fibers = {}
    for i in range(N_fiber):
        fibers[i] = tracebounds[i::N_fiber]
    ### Fit trace widths with KPIC DRP
    if not stack:
        trace_sigs = _fit_traces(datacube, badpixcube, fibers, nskip=nskip)
    else:
        trace_sigs = []
        trace_sig = _fit_traces(np.expand_dims(np.nansum(datacube,axis=0),axis=0), badpixcube, fibers, nskip=nskip)
        for i in range(len(datacube)):
            trace_sigs.append(trace_sig[0])
        trace_sigs = np.asarray(trace_sigs)
    # ## Do flux extraction with KPIC DRP tools, box option
    flux_lst = _extract_fluxes(datacube, smoothed_thermal_noise, badpixmap, trace_locs, trace_sigs)

    return trace_sigs, trace_locs, flux_lst


"""Added complain table

Revision ID: 485543c154fc
Revises: c6d2d23080c5
Create Date: 2022-11-03 11:14:20.799902

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '485543c154fc'
down_revision = 'c6d2d23080c5'
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('complain',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('firstname', sa.String(length=20), nullable=False),
    sa.Column('lastname', sa.String(length=200), nullable=False),
    sa.Column('complain', sa.Text(length=500), nullable=False),
    sa.Column('date_of_complain', sa.DateTime(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('complain')
    # ### end Alembic commands ###


import tweepy
import time

auth = tweepy.OAuth1UserHandler(
   '7POHEsJv6mrw668KcNTcyKmPs', '17a9opR34yX4YeJ2wnVSXkpyxev1liT8jmbPG4weMLCxDIamXn', '1597932774253809665-vX7OmjMr07p1OrMnpJgCDv3M7bP33G', 'cEnXNZjnZ1KErGsTOCYgWqjv7qo0lAzzFrrKftpkCY1zJ'
)

api = tweepy.API(auth, wait_on_rate_limit=True)

for follower in tweepy.Cursor(api.get_followers).items():
   api.create_friendship(screen_name=follower.screen_name)

search_string = "Day #100DaysOfCode"
tweets_limit = 50

for tweet in tweepy.Cursor(api.search_tweets , search_string).items(tweets_limit):
         try:
            api.create_favorite(tweet.id)
            api.retweet(tweet.id)
            print("tweet liked")
            time.sleep(10)
         except tweepy.TweepyException as e:
            print(e)
            if '429' in str(e):
               time.sleep(6000)
         except StopIteration:
            break


"""
Author: CHR-onicles©
Date: 05/12/2020

DSC UG Python Challenge - Time Tracker App.
"""

from tkinter import *
from tkcalendar import *
from tkinter.filedialog import askdirectory
from tkinter import messagebox
from PIL import Image, ImageTk
from datetime import datetime as dt
import openpyxl
import os
import threading

# MAIN WINDOW CONFIGURATIONS

root = Tk()
root.title('TimeTracker')
root.iconbitmap('images/cat.ico')

SCREEN_HEIGHT = root.winfo_screenheight()
SCREEN_WIDTH = root.winfo_screenwidth()

# print(SCREEN_HEIGHT, SCREEN_WIDTH)  # For debugging
ROOT_WIDTH = 550
ROOT_HEIGHT = 640

# x and y coordinates to center app on screen
x_pos = (SCREEN_WIDTH/2) - (ROOT_WIDTH/2)
y_pos = (SCREEN_HEIGHT/2) - (ROOT_HEIGHT/2)

root.geometry(f'{ROOT_WIDTH}x{ROOT_HEIGHT}+{int(x_pos)}+{int(y_pos)}')

# Preventing user from increasing or reducing app size as most widgets are statically placed!
root.resizable(False, False)
root.configure(bg='#5fc29e')  # turquoise-ish colour used in status bar and calendar.

# GLOBAL VARIABLES------------

# Calendar stuff
cal_window = None
first_time_open = False
date_from_calendar = ''
date_from_calendar2 = ''

# Status bar text
rate_text = 'Rate: 1hr = $5.00'

# Payment calculation variable
amount_to_be_paid = 0.0
total_hours_spent = 0.0

# Excel stuff variable
excel_first_time_open = 0
alpha_current_dir = os.getcwd()
dir_location = ''
# TODO: Put coordinates of buttons in global variables for easy update of button locations.
# -----------------------------

# DEFINING BINDING EVENT FUNCTIONS

def spin_box_hover_in(event, spin_box):
    """
    Function to change the spinbox's foreground colour and status bar when hovered on.

    :param event: necessary argument in order for binding event to work.
    :param spin_box: spin box object that is passed in to be updated.
    """
    spin_box.config(fg='black')

    if spin_box == top_hour_spin_box or spin_box == m_hour_spin_box:
        status_bar_left.config(text='Set the hour...', font=('consolas', 12, 'italic'))

    elif spin_box == top_minute_spin_box or spin_box == m_minute_spin_box:
        status_bar_left.config(text='Set the minutes...', font=('consolas', 12, 'italic'))

def spin_box_hover_out(event, spin_box):
    """
    Function to change the spinbox's foreground color back to normal.

    :param event: necessary argument in order for binding event to work.
    :param spin_box: spin box object that is passed in to be updated.
    """
    spin_box.config(fg='gray')
    status_bar_left.config(text=rate_text, font=('consolas', 12))

def submit_button_hover_in(event):
    """
    Binding event function to update submit button in calendar,
    when it is hovered on.
    """
    submit_button.config(bg='white', font=('consolas', 14, 'italic'))

def submit_button_hover_out(event):
    """
    Binding event function to update submit button in calendar,
    when it is hovered out of.
    """
    submit_button.config(bg='SystemButtonFace', font=('consolas', 14))

def calculate_payment_button_hover_in(event):
    """
    Function to change colour and font style of calculate payment button and status bar
    when hovered on.
    """
    calc_payment_button.config(font=('consolas', 22))
    status_bar_left.config(text='Calculate payment based on info provided...', font=('consolas', 10, 'italic'))

def calculate_payment_button_hover_out(event):
    """
    Function to change calculate payment button's colour, font style and status bar
    back to normal when mouse pointer is no longer hovering on the button.
    """
    calc_payment_button.config(font=('consolas', 20), bg='SystemButtonFace')
    status_bar_left.config(text=rate_text, font=('consolas', 12))

def save_button_hover_in(event):
    """
    Binding event function to update save button's color, size and status bar
    when mouse pointer hovers on it.
    """
    global save_icon, save_button, sb_window
    bottom_canvas.delete(sb_window)
    save_icon = ImageTk.PhotoImage(Image.open('images/save.png').resize((70, 65), Image.ANTIALIAS))
    save_button = Button(bottom_canvas, image=save_icon, command=save_to_excel)
    sb_window = bottom_canvas.create_window(490, 160, window=save_button)
    save_button.bind('<Leave>', save_button_hover_out)

    # Update status bar
    status_bar_left.config(text='Save payment info to excel file...', font=('consolas', 11, 'italic'))

def save_button_hover_out(event):
    """
    Binding event function to return save button to normal when hovered out of.
    """
    global save_icon, save_button, sb_window
    bottom_canvas.delete(sb_window)
    save_icon = ImageTk.PhotoImage(Image.open('images/save.png').resize((65, 60), Image.ANTIALIAS))
    save_button = Button(bottom_canvas, image=save_icon, command=save_to_excel)
    sb_window = bottom_canvas.create_window(490, 160, window=save_button)
    save_button.bind('<Enter>', save_button_hover_in)

    # Update status bar again
    status_bar_left.config(text=rate_text, font=('consolas', 12))

def reset_button_hover_in(event):
    """
    Function to update reset button and status bar when hovered on.
    """
    global reset_icon, reset_button, r_window
    bottom_canvas.delete(r_window)
    reset_icon = ImageTk.PhotoImage(Image.open('images/Reset.png').resize((65, 65), Image.ANTIALIAS))
    reset_button = Button(bottom_canvas, image=reset_icon, command=reset_values)
    r_window = bottom_canvas.create_window(60, 160, window=reset_button)
    reset_button.bind('<Leave>', reset_button_hover_out)

    # Update status bar
    status_bar_left.config(text='Reset calculation...', font=('consolas', 12, 'italic'))

def reset_button_hover_out(event):
    """
    Function to update reset button and status bar back to normal when hovered out of.
    """
    global reset_icon, reset_button, r_window
    bottom_canvas.delete(r_window)
    reset_icon = ImageTk.PhotoImage(Image.open('images/Reset.png').resize((60, 60), Image.ANTIALIAS))
    reset_button = Button(bottom_canvas, image=reset_icon, command=reset_values)
    r_window = bottom_canvas.create_window(60, 160, window=reset_button)
    reset_button.bind('<Enter>', reset_button_hover_in)

    # Update status bar
    status_bar_left.config(text=rate_text, font=('consolas', 12))

def calendar_button_hover_in(event, canvas):
    """
    Function to update the calendar button when it is hovered on.
    """

    # Created 2 separate instances of the hover event because of conflict between them when they both use the same
    # resources.
    if canvas == top_canvas:  # this is top canvas
        global cl_window, calendar_button, cal_icon
        canvas.delete(cl_window)
        cal_icon = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((50, 50), Image.ANTIALIAS))
        calendar_button = Button(canvas, image=cal_icon, bg='light gray', command=lambda: open_calendar(canvas))
        cl_window = canvas.create_window(136, 100, window=calendar_button)
        calendar_button.bind('<Leave>', lambda e: calendar_button_hover_out(e, canvas))

    else:
        global cl_window2, cal_icon2, calendar_button2
        canvas.delete(cl_window2)
        cal_icon2 = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((50, 50), Image.ANTIALIAS))
        calendar_button2 = Button(canvas, image=cal_icon2, bg='light gray', command=lambda: open_calendar(canvas))
        cl_window2 = canvas.create_window(136, 100, window=calendar_button2)
        calendar_button2.bind('<Leave>', lambda e: calendar_button_hover_out(e, canvas))

    status_bar_left.config(text='Pick a date from the calendar...', font=('consolas', 12, 'italic'))

def calendar_button_hover_out(event, canvas):
    """
    Function to update the calendar button when it is hovered out of.
    """
    if canvas == top_canvas:  # top canvas
        global cl_window, calendar_button, cal_icon
        canvas.delete(cl_window)
        cal_icon = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((45, 45), Image.ANTIALIAS))
        calendar_button = Button(canvas, image=cal_icon, bg='light gray', command=lambda: open_calendar(canvas))
        cl_window = canvas.create_window(136, 100, window=calendar_button)
        calendar_button.bind('<Enter>', lambda e: calendar_button_hover_in(e, canvas))

    else:  # middle canvas
        global cl_window2, cal_icon2, calendar_button2
        canvas.delete(cl_window2)
        cal_icon2 = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((45, 45), Image.ANTIALIAS))
        calendar_button2 = Button(canvas, image=cal_icon2, bg='light gray', command=lambda: open_calendar(canvas))
        cl_window2 = canvas.create_window(136, 100, window=calendar_button2)
        calendar_button2.bind('<Enter>', lambda e: calendar_button_hover_in(e, canvas))

    status_bar_left.config(text=rate_text, font=('consolas', 12))

def confirm_button_hover_in(event, canvas):
    """
    Function to update the confirm button when it is hovered on.
    """
    if canvas == top_canvas:
        global c_icon, check_button, cf_window
        canvas.delete(cf_window)
        c_icon = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((65, 65), Image.ANTIALIAS))
        check_button = Button(canvas, image=c_icon, command=lambda: confirm_button_click(canvas))
        cf_window = canvas.create_window(490, 90, window=check_button)
        check_button.bind('<Leave>', lambda e: confirm_button_hover_out(e, canvas))

    else:  # middle canvas
        global c_icon2, check_button2, cf_window2
        canvas.delete(cf_window2)
        c_icon2 = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((65, 65), Image.ANTIALIAS))
        check_button2 = Button(canvas, image=c_icon2, command=lambda: confirm_button_click(canvas))
        cf_window2 = canvas.create_window(490, 90, window=check_button2)
        check_button2.bind('<Leave>', lambda e: confirm_button_hover_out(e, canvas))

    status_bar_left.config(text='Confirm date and time selection...', font=('consolas', 11, 'italic'))

def confirm_button_hover_out(event, canvas):
    """
    Function to update the confirm button when it is hovered out of.
    """
    if canvas == top_canvas:  # top canvas
        global c_icon, check_button, cf_window
        canvas.delete(cf_window)
        c_icon = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((60, 60), Image.ANTIALIAS))
        check_button = Button(canvas, image=c_icon, command=lambda: confirm_button_click(canvas))
        cf_window = canvas.create_window(490, 90, window=check_button)
        check_button.bind('<Enter>', lambda e: confirm_button_hover_in(e, canvas))

    else:  # middle canvas
        global c_icon2, check_button2, cf_window2
        canvas.delete(cf_window2)
        c_icon2 = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((60, 60), Image.ANTIALIAS))
        check_button2 = Button(canvas, image=c_icon2, command=lambda: confirm_button_click(canvas))
        cf_window2 = canvas.create_window(490, 90, window=check_button2)
        check_button2.bind('<Enter>', lambda e: confirm_button_hover_in(e, canvas))

    status_bar_left.config(text=rate_text, font=('consolas', 12))

def cancel_button_hover_in(event, canvas):
    """
    Function to update the cancel button when it is hovered on.
    """
    if canvas == top_canvas:
        global cancel_icon, cancel_button, cx_window
        canvas.delete(cx_window)
        cancel_icon = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((65, 65), Image.ANTIALIAS))
        cancel_button = Button(canvas, image=cancel_icon, command=lambda: cancel_button_click(canvas))
        cx_window = canvas.create_window(490, 160, window=cancel_button)
        cancel_button.bind('<Leave>', lambda e: cancel_button_hover_out(e, canvas))

    else:  # middle canvas
        global cancel_icon2, cancel_button2, cx_window2
        canvas.delete(cx_window2)
        cancel_icon2 = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((65, 65), Image.ANTIALIAS))
        cancel_button2 = Button(canvas, image=cancel_icon2, command=lambda: cancel_button_click(canvas))
        cx_window2 = canvas.create_window(490, 160, window=cancel_button2)
        cancel_button2.bind('<Leave>', lambda e: cancel_button_hover_out(e, canvas))

    status_bar_left.config(text='Cancel date and time selected...', font=('consolas', 11, 'italic'))

def cancel_button_hover_out(event, canvas):
    """
    Function to update the cancel button when it is hovered out of.
    """
    if canvas == top_canvas:
        global cancel_icon, cancel_button, cx_window
        canvas.delete(cx_window)
        cancel_icon = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((60, 60), Image.ANTIALIAS))
        cancel_button = Button(canvas, image=cancel_icon, command=lambda: cancel_button_click(canvas))
        cx_window = canvas.create_window(490, 160, window=cancel_button)
        cancel_button.bind('<Enter>', lambda e: cancel_button_hover_in(e, canvas))

    else:  # middle canvas
        global cancel_icon2, cancel_button2, cx_window2
        canvas.delete(cx_window2)
        cancel_icon2 = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((60, 60), Image.ANTIALIAS))
        cancel_button2 = Button(canvas, image=cancel_icon2, command=lambda: cancel_button_click(canvas))
        cx_window2 = canvas.create_window(490, 160, window=cancel_button2)
        cancel_button2.bind('<Enter>', lambda e: cancel_button_hover_in(e, canvas))

    status_bar_left.config(text=rate_text, font=('consolas', 12))

def time_now_button_hover_in(event, canvas):
    """
    Function to update time_now button when it is hovered on.
    """
    if canvas == top_canvas:
        global time_now_icon, time_now_button, tn_window
        canvas.delete(tn_window)
        time_now_icon = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((50, 50), Image.ANTIALIAS))
        time_now_button = Button(canvas, image=time_now_icon, command=lambda: get_current_date_time(canvas))
        tn_window = canvas.create_window(415, 100, window=time_now_button)
        time_now_button.bind('<Leave>', lambda e: time_now_button_hover_out(e, canvas))

    else:  # middle canvas
        global time_now_icon2, time_now_button2, tn_window2
        canvas.delete(tn_window2)
        time_now_icon2 = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((50, 50), Image.ANTIALIAS))
        time_now_button2 = Button(canvas, image=time_now_icon2,
                                  command=lambda: get_current_date_time(canvas))
        tn_window2 = canvas.create_window(415, 100, window=time_now_button2)
        time_now_button2.bind('<Leave>', lambda e: time_now_button_hover_out(e, canvas))

    status_bar_left.config(text='Get current date and time...', font=('consolas', 12, 'italic'))

def time_now_button_hover_out(event, canvas):
    """
    Function to update time now button when it is hovered out of.
    """
    if canvas == top_canvas:
        global time_now_icon, time_now_button, tn_window
        canvas.delete(tn_window)
        time_now_icon = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((45, 45), Image.ANTIALIAS))
        time_now_button = Button(canvas, image=time_now_icon, command=lambda: get_current_date_time(canvas))
        tn_window = canvas.create_window(415, 100, window=time_now_button)
        time_now_button.bind('<Enter>', lambda e: time_now_button_hover_in(e, canvas))

    else:  # middle canvas
        global time_now_icon2, time_now_button2, tn_window2
        canvas.delete(tn_window2)
        time_now_icon2 = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((45, 45), Image.ANTIALIAS))
        time_now_button2 = Button(canvas, image=time_now_icon2,
                                  command=lambda: get_current_date_time(canvas))
        tn_window2 = canvas.create_window(415, 100, window=time_now_button2)
        time_now_button2.bind('<Enter>', lambda e: time_now_button_hover_in(e, canvas))

    status_bar_left.config(text=rate_text, font=('consolas', 12))

# DEFINING COMMAND FUNCTIONS--------------------------------------------------------

def open_calendar(canvas):
    """
    Command function to open calendar for user to choose a date.

    :param: canvas: canvas object for the function to know which canvas called it and
                    give it specific attributes.
    """

    global first_time_open, cal_window

    def create_calendar_window():
        global first_time_open, cal_window

        # Grabbing current date from system
        year = dt.now().year
        month = dt.now().month
        day = dt.now().day

        # Calendar window configurations
        cal_window = Toplevel()
        cal_window.title('Calendar')
        cal_window.iconbitmap('images/cat.ico')

        # To keep the calendar window within the app screen (specifically top left of app)
        cal_window.geometry(f'250x270+{int(x_pos)}+{int(y_pos)}')
        cal_window.resizable(False, False)
        cal_window.configure(bg='#5fc29e')

        def get_date():
            """
            Based on the canvas argument from outer function, this function
            updates a global variable on the date got from the calendar.
            """
            global cal_window, selected_label, date_from_calendar, date_from_calendar2
            if canvas == top_canvas:
                date_from_calendar = cal.get_date()

                # rearranging of date format to: DD/MM/YY
                temp = date_from_calendar.split('/')

                # Prepending 0 to single digits
                if len(temp[0]) == 1 and len(temp[1]) == 1:
                    temp[0] = '0' + temp[0]
                    temp[1] = '0' + temp[1]
                elif len(temp[0]) == 1:
                    temp[0] = '0' + temp[0]
                elif len(temp[1]) == 1:
                    temp[1] = '0' + temp[1]

                temp2 = temp[1] + '/' + temp[0] + '/' + temp[2]
                date_from_calendar = temp2

            elif canvas == middle_canvas:
                date_from_calendar2 = cal.get_date()

                # rearranging of date format to: DD/MM/YY
                temp = date_from_calendar2.split('/')

                # Prepending 0 to single digits
                if len(temp[0]) == 1 and len(temp[1]) == 1:
                    temp[0] = '0' + temp[0]
                    temp[1] = '0' + temp[1]
                elif len(temp[0]) == 1:
                    temp[0] = '0' + temp[0]
                elif len(temp[1]) == 1:
                    temp[1] = '0' + temp[1]

                temp2 = temp[1] + '/' + temp[0] + '/' + temp[2]
                date_from_calendar2 = temp2

            selected_label = Label(cal_window, text='Date Selected!', font=('consolas', 14, 'italic'),
                                   bg='#5fc29e')
            selected_label.pack(pady=5)
            submit_button.config(state=DISABLED)

        global cal
        cal = Calendar(cal_window, selectmode='day', year=year, month=month, day=day)
        cal.pack(fill=BOTH, expand=1)

        # Submit button for calendar
        global submit_button
        submit_button = Button(cal_window, text='Submit', font=('consolas', 14), command=get_date)
        submit_button.pack(pady=(5, 0))
        submit_button.bind('<Enter>', submit_button_hover_in)
        submit_button.bind('<Leave>', submit_button_hover_out)
        first_time_open = True

    if first_time_open is False:  # Opened calendar window for the first time
        create_calendar_window()

    else:
        if cal_window.winfo_exists():  # Calendar window is still open
            first_time_open = True
        else:  # Calendar window was opened but closed.
            create_calendar_window()

def status_bar_time_update():
    """
    Function to update the time in the status bar through a thread.
    """
    cur_time = dt.now().strftime('%H:%M:%S')
    status_bar_right.config(text='|System Time(24h): ' + cur_time)
    status_bar_right.after(1000, status_bar_time_update)

def confirm_button_click(canvas):
    """
    Command function to display user's choice of date and time on the canvas provided.

    :param canvas: canvas object to know which canvas called it and attribute specific
                    configurations to it or some other global variables.
    """
    global date_window, date_window2, selected_date_label, s_time, selected_date_label2, s_time2

    def remove_user_prepended_zeros(var):
        """
        Small Helper function to remove user prepended zeros to time because app will take there of that.

        :param var: spin box value that is passed to function.
        :return: returns new var without the prepended zero.
        """
        if len(var.get()) == 2 and var.get()[0] == '0':
            var.set(var.get()[1])
            return var.get()
        else:
            return var.get()  # do nothing

    # Putting all spinbox values in local variables because
    # its easier to perform operations on those variables.
    top_hour = StringVar()
    top_hour.set(top_hour_spin_box.get())
    top_minute = StringVar()
    top_minute.set(top_minute_spin_box.get())
    m_hour = StringVar()
    m_hour.set(m_hour_spin_box.get())
    m_minute = StringVar()
    m_minute.set(m_minute_spin_box.get())

    # Check for user prepended zeros and remove them with function
    top_hour.set(remove_user_prepended_zeros(top_hour))
    top_minute.set(remove_user_prepended_zeros(top_minute))
    m_hour.set(remove_user_prepended_zeros(m_hour))
    m_minute.set(remove_user_prepended_zeros(m_minute))

    if canvas == top_canvas:

        # If user types in more than 2 numbers, generate error message
        if len(top_hour.get()) > 2 or len(top_minute.get()) > 2:
            messagebox.showerror(title='INVALID INPUT', message='TIME ENTERED IS INVALID')

        else:
            # Prepend 0 if its a single digit
            # Decided to use this and not len() because this helps filter out non-digit input from user :)
            if (0 <= int(top_hour.get()) <= 9) and (0 <= int(top_minute.get()) <= 9):
                s_time = '0' + top_hour.get() + ':' + '0' + top_minute.get()
            elif 0 <= int(top_minute.get()) <= 9:
                s_time = top_hour.get() + ':' + '0' + top_minute.get()
            elif 0 <= int(top_hour.get()) <= 9:
                s_time = '0' + top_hour.get() + ':' + top_minute.get()
            else:
                s_time = top_hour.get() + ':' + top_minute.get()
            selected_date_label.config(text='You selected: ' + date_from_calendar + ' ' + s_time)
            date_window = canvas.create_window(225, 170, window=selected_date_label)

    elif canvas == middle_canvas:

        # If user types in more than 2 numbers, generate error message
        if len(m_hour.get()) > 2 or len(m_minute.get()) > 2:
            messagebox.showerror(title='INVALID INPUT', message='TIME ENTERED IS INVALID')

        else:
            if (0 <= int(m_minute.get()) <= 9) and (0 <= int(m_hour.get()) <= 9):
                s_time2 = '0' + m_hour.get() + ':' + '0' + m_minute.get()
            elif 0 <= int(m_minute.get()) <= 9:
                s_time2 = m_hour.get() + ':' + '0' + m_minute.get()
            elif 0 <= int(m_hour.get()) <= 9:
                s_time2 = '0' + m_hour.get() + ':' + m_minute.get()
            else:
                s_time2 = m_hour.get() + ':' + m_minute.get()
            selected_date_label2.config(text='You selected: ' + date_from_calendar2 + ' ' + s_time2)
            date_window2 = canvas.create_window(225, 170, window=selected_date_label2)

def cancel_button_click(canvas):
    """
    Command function to remove label/text telling user his choice of date and time.

    :param canvas: canvas object to know which canvas called it and update specific labels.
    """
    if canvas == middle_canvas:
        canvas.delete(date_window2)

    else:  # top canvas
        canvas.delete(date_window)

def get_current_date_time(canvas):
    """
    Command function to get the system's current date and time.

    :param canvas: canvas object to know which canvas called it and update its labels.
    """
    global s_time, selected_date_label, date_window, date_window2, s_time2
    global date_from_calendar, date_from_calendar2, selected_date_label2

    if canvas == top_canvas:
        date_from_calendar = dt.now().strftime('%d/%m/%y')
        s_time = dt.now().strftime('%H:%M')
        selected_date_label.config(text='You selected: ' + date_from_calendar + ' ' + s_time)
        date_window = canvas.create_window(225, 170, window=selected_date_label)

    else:  # middle canvas
        date_from_calendar2 = dt.now().strftime('%d/%m/%y')
        s_time2 = dt.now().strftime('%H:%M')
        selected_date_label2.config(text='You selected: ' + date_from_calendar2 + ' ' + s_time2)
        date_window2 = canvas.create_window(225, 170, window=selected_date_label2)

def payment_calculation():
    """
    Function to calculate payment amount based on the date and time passed in
    by user.
    Rate: $5 for 60mins (1hr).
    """
    # Do nothing with dates for now since both dates(date started and date completed) will be the same.
    # NB: If User selects different dates, app only checks the time regardless.
    # Hopefully no one enters different dates and get some crazy output :)
    # Hmmm...naa... TODO: make sure User enters same date twice or only display one calendar button for QoL

    global amount_label, calc_payment_button, amount_to_be_paid, s_time, s_time2, total_hours_spent
    amount_label = bottom_canvas.create_text(250, 150, text='')

    # Perform operations on hours and minutes only
    # Grabbing hours and minutes
    hour_completed = int(s_time2[0:2])
    minutes_completed = int(s_time2[3:])
    hour_started = int(s_time[0:2])
    minutes_started = int(s_time[3:])

    hours_elapsed = hour_completed - hour_started
    minutes_elapsed = minutes_completed - minutes_started

    # Convert everything to minutes and perform calculations based on that
    hours_to_mins = hours_elapsed * 60
    total_minutes_elapsed = minutes_elapsed + hours_to_mins
    total_hours_spent = round(total_minutes_elapsed / 60, 2)
    amount_to_be_paid = round(float(total_minutes_elapsed * 1 / 12), 2)

    # hmm...Might generate false positive errors since user can choose different dates
    # TODO: Do the TODO just before this one
    if total_minutes_elapsed < 0:
        messagebox.showerror(title='INVALID ARGUMENT', message='INVALID INFORMATION ENTERED!')
        calc_payment_button.config(state=DISABLED)
    else:
        amount_label = bottom_canvas.create_text(270, 150, text='$' + str(amount_to_be_paid),
                                                 font=('consolas', 40, 'bold'), fill='#e8ebea')
        calc_payment_button.config(state=DISABLED)

def reset_values():
    """
    Function to reset dates, time and amount calculated.
    """
    global calc_payment_button
    bottom_canvas.delete(amount_label)
    middle_canvas.delete(date_window2)
    top_canvas.delete(date_window)
    calc_payment_button.config(state=NORMAL)

def save_to_excel():
    """
    Function to save date/time started and completed and payment calculated
    to an excel file.
    """
    global excel_first_time_open, amount_to_be_paid, dir_location, total_hours_spent
    program_dir = alpha_current_dir

    def check_total_hours_spent():
        """
        Small Helper function to check if the total hours spent is
         and bring up an error box.
        """
        if total_hours_spent < 0:
            messagebox.showerror(title='INVALID ARGUMENT', message='INVALID INFORMATION ENTERED!')
            return False
        else:
            return True

    def create_new_file():
        """
        Function to create new excel file and input information from app.
        """
        workbook = openpyxl.Workbook()
        sheet = workbook['Sheet']

        # Column headers
        sheet['A1'] = 'Date Started'
        sheet['B1'] = 'Time Started'
        sheet['C1'] = 'Date Completed'
        sheet['D1'] = 'Time Completed'
        sheet['E1'] = 'Total Hours Spent'
        sheet['F1'] = 'Calculated Payment'
        sheet['G1'] = 'Time Info was Saved'

        # Increase width for better view
        for cols in ['A', 'B', 'C', 'D', 'E', 'F', 'G']:
            sheet.column_dimensions[cols].width = 20

        # This is the first time so we can write specifically to certain cells
        sheet['A2'] = date_from_calendar
        sheet['B2'] = s_time
        sheet['C2'] = date_from_calendar2
        sheet['D2'] = s_time2
        sheet['E2'] = total_hours_spent
        sheet['F2'] = '$' + str(amount_to_be_paid)
        sheet['G2'] = dt.now().strftime('%d/%m/%Y, %H:%M:%S')

        # Save excel file and close it
        workbook.save('payment_history.xlsx')
        workbook.close()

        # Change back to program's directory
        os.chdir(program_dir)

        # Message box for successful entry to NEW file
        messagebox.showinfo(title='Save To Excel File', message='Information saved successfully to NEW excel file!')

    def edit_existing_file():
        """
        Function to edit already existing excel file and input info from app.
        """
        workbook = openpyxl.load_workbook('payment_history.xlsx')
        sheet = workbook['Sheet']

        found_available_cell = False
        row_num = 0
        for row in sheet.iter_rows(min_row=1, max_row=200, min_col=1, max_col=6):
            row_num += 1
            for count, cell in enumerate(row):

                # if all cells in a row are empty, then that row is available for writing
                if cell.value is None and cell == row[-1]:
                    sheet.cell(row=row_num, column=1).value = date_from_calendar
                    sheet.cell(row=row_num, column=2).value = s_time
                    sheet.cell(row=row_num, column=3).value = date_from_calendar2
                    sheet.cell(row=row_num, column=4).value = s_time2
                    sheet.cell(row=row_num, column=5).value = total_hours_spent
                    sheet.cell(row=row_num, column=6).value = '$' + str(amount_to_be_paid)
                    sheet.cell(row=row_num, column=7).value = dt.now().strftime('%d/%m/%Y, %H:%M:%S')
                    found_available_cell = True
                    break
                elif cell.value is not None:  # if any cell contains information, break
                    break
            if found_available_cell is True:
                break

        # Save and close excel file
        workbook.save('payment_history.xlsx')
        workbook.close()

        # Change back to program's directory
        os.chdir(program_dir)

        # Message box for successful entry to EXISTING file
        messagebox.showinfo(title='Save To Excel File', message='Information saved successfully to EXISTING excel file!')

    # Main Excel Logic
    if excel_first_time_open == 0:  # Save button has not been clicked already
        if check_total_hours_spent() is True:
            dir_location = askdirectory(initialdir='.\\', title='Select Directory To Save To')

            # Change location to the user-chosen one
            os.chdir(dir_location)

            # Search for already existing file
            for item in os.listdir():
                if item != 'payment_history.xlsx' and item == os.listdir()[-1]:
                    create_new_file()

                elif item == 'payment_history.xlsx':
                    edit_existing_file()
                    break
        else:
            pass  # Show error box and do nothing
    else:
        if check_total_hours_spent() is True:
            # Change location to the user-chosen one
            os.chdir(dir_location)
            edit_existing_file()
        else:
            pass  # Show error box and do nothing

    excel_first_time_open += 1

# TOP CANVAS---------------------------------------------------------------------------------------

top_canvas = Canvas(root, width=ROOT_WIDTH, height=200, borderwidth=0)
top_canvas.grid(row=0, column=0, sticky=W + E, columnspan=2)
img1 = ImageTk.PhotoImage(Image.open('images/clock_dark.jfif'), Image.ANTIALIAS)
top_canvas.create_image(0, 0, image=img1, anchor=NW)
top_canvas.create_text(270, 30, text='Date/Time Started', font=('android 7', 25))

# Top Canvas Hour Spin Box
top_hour_spin_box = Spinbox(top_canvas, from_=0, to=23, width=2, font=('consolas', 20), fg='gray')
top_canvas.create_window(290, 100, window=top_hour_spin_box)
top_hour_spin_box.bind('<Enter>', lambda event: spin_box_hover_in(event, top_hour_spin_box))
top_hour_spin_box.bind('<Leave>', lambda event: spin_box_hover_out(event, top_hour_spin_box))

# Semicolon separating minute from hours
top_canvas.create_text(323, 100, text=':', font=('android 7', 25, 'bold'), fill='white')

# Top Canvas Minute Spin Box
top_minute_spin_box = Spinbox(top_canvas, from_=0, to=59, width=2, font=('consolas', 20), fg='gray')
top_canvas.create_window(355, 100, window=top_minute_spin_box)
top_minute_spin_box.bind('<Enter>', lambda event: spin_box_hover_in(event, top_minute_spin_box))
top_minute_spin_box.bind('<Leave>', lambda event: spin_box_hover_out(event, top_minute_spin_box))

# Time text
top_canvas.create_text(225, 100, text='Time:', font=('consolas', 20))

# Date text
top_canvas.create_text(70, 100, text='Date:', font=('consolas', 20))

# Calendar button for top canvas
t_cal_icon = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((45, 45), Image.ANTIALIAS))
t_calendar_button = Button(top_canvas, image=t_cal_icon, bg='light gray', command=lambda: open_calendar(top_canvas))
cl_window = top_canvas.create_window(136, 100, window=t_calendar_button)
t_calendar_button.bind('<Enter>', lambda event: calendar_button_hover_in(event, top_canvas))
t_calendar_button.bind('<Leave>', lambda event: calendar_button_hover_out(event, top_canvas))

# Selected date and time text
s_time = ''
selected_date_label = Label(top_canvas, text='You selected: ' + date_from_calendar + ' ' + s_time,
                            font=('consolas', 20, 'italic', 'bold'))

# Checkmark button
c_icon = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((60, 60), Image.ANTIALIAS))
check_button = Button(top_canvas, image=c_icon, command=lambda: confirm_button_click(top_canvas))
cf_window = top_canvas.create_window(490, 90, window=check_button)
check_button.bind('<Enter>', lambda event: confirm_button_hover_in(event, top_canvas))
check_button.bind('<Leave>', lambda event: confirm_button_hover_out(event, top_canvas))

# Cancel button
cancel_icon = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((60, 60), Image.ANTIALIAS))
cancel_button = Button(top_canvas, image=cancel_icon, command=lambda: cancel_button_click(top_canvas))
cx_window = top_canvas.create_window(490, 160, window=cancel_button)
cancel_button.bind('<Enter>', lambda event: cancel_button_hover_in(event, top_canvas))
cancel_button.bind('<Leave>', lambda event: cancel_button_hover_out(event, top_canvas))

# Get Time Now button
time_now_icon = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((45, 45), Image.ANTIALIAS))
time_now_button = Button(top_canvas, image=time_now_icon, command=lambda: get_current_date_time(top_canvas))
tn_window = top_canvas.create_window(415, 100, window=time_now_button)
time_now_button.bind('<Enter>', lambda event: time_now_button_hover_in(event, top_canvas))
time_now_button.bind('<Leave>', lambda event: time_now_button_hover_out(event, top_canvas))

# MIDDLE CANVAS---------------------------------------------------------------------------------------

middle_canvas = Canvas(root, width=ROOT_WIDTH, height=200, borderwidth=0)
middle_canvas.grid(row=1, column=0, columnspan=2, sticky=W + E)
img2 = ImageTk.PhotoImage(Image.open('images/time_dark.jfif').resize((600, 400)), Image.ANTIALIAS)
middle_canvas.create_image(0, 0, image=img2, anchor=NW)
middle_canvas.create_text(280, 26, text='Date/Time Completed', font=('android 7', 25), fill='#e8ebea')

# Middle Canvas Hour Spin Box
m_hour_spin_box = Spinbox(middle_canvas, from_=0, to=23, width=2, font=('consolas', 20), fg='gray')
m_hour_spin_box.bind('<Enter>', lambda event: spin_box_hover_in(event, m_hour_spin_box))
m_hour_spin_box.bind('<Leave>', lambda event: spin_box_hover_out(event, m_hour_spin_box))
middle_canvas.create_window(290, 100, window=m_hour_spin_box)

# Semicolon separating minute from hours
middle_canvas.create_text(323, 100, text=':', font=('android 7', 25, 'bold'), fill='#e8ebea')

# Middle Canvas Minute Spin Box
m_minute_spin_box = Spinbox(middle_canvas, from_=0, to=59, width=2, font=('consolas', 20), fg='gray')
middle_canvas.create_window(355, 100, window=m_minute_spin_box)
m_minute_spin_box.bind('<Enter>', lambda event: spin_box_hover_in(event, m_minute_spin_box))
m_minute_spin_box.bind('<Leave>', lambda event: spin_box_hover_out(event, m_minute_spin_box))

# Time text
middle_canvas.create_text(225, 100, text='Time:', font=('consolas', 20), fill='#e8ebea')
# this hex colour is used because of the background picture of the canvas.

# Date text
middle_canvas.create_text(70, 100, text='Date:', font=('consolas', 20), fill='#e8ebea')

# Calendar button for middle canvas
m_cal_icon = ImageTk.PhotoImage(Image.open('images/cal2_icon.png').resize((45, 45), Image.ANTIALIAS))
m_calendar_button = Button(middle_canvas, image=m_cal_icon, bg='light gray',
                           command=lambda: open_calendar(middle_canvas))
cl_window2 = middle_canvas.create_window(136, 100, window=m_calendar_button)
m_calendar_button.bind('<Enter>', lambda event: calendar_button_hover_in(event, middle_canvas))
m_calendar_button.bind('<Leave>', lambda event: calendar_button_hover_out(event, middle_canvas))

# Selected date and time text
s_time2 = ''
selected_date_label2 = Label(middle_canvas, text='You selected: ' + date_from_calendar2 + ' ' + s_time2,
                             font=('consolas', 20, 'italic', 'bold'))

# Checkmark icon
c_icon2 = ImageTk.PhotoImage(Image.open('images/check_icon.png').resize((60, 60), Image.ANTIALIAS))
check_button2 = Button(middle_canvas, image=c_icon2, command=lambda: confirm_button_click(middle_canvas))
cf_window2 = middle_canvas.create_window(490, 90, window=check_button2)
check_button2.bind('<Enter>', lambda event: confirm_button_hover_in(event, middle_canvas))
check_button2.bind('<Leave>', lambda event: confirm_button_hover_out(event, middle_canvas))

# Cancel icon
cancel_icon2 = ImageTk.PhotoImage(Image.open('images/remove_icon.jpg').resize((60, 60), Image.ANTIALIAS))
cancel_button2 = Button(middle_canvas, image=cancel_icon2, command=lambda: cancel_button_click(middle_canvas))
cx_window2 = middle_canvas.create_window(490, 160, window=cancel_button2)
cancel_button2.bind('<Enter>', lambda event: cancel_button_hover_in(event, middle_canvas))
cancel_button2.bind('<Leave>', lambda event: cancel_button_hover_out(event, middle_canvas))

# Get Time Now button
time_now_icon2 = ImageTk.PhotoImage(Image.open('images/t_now.png').resize((45, 45), Image.ANTIALIAS))
time_now_button2 = Button(middle_canvas, image=time_now_icon2, command=lambda: get_current_date_time(middle_canvas))
tn_window2 = middle_canvas.create_window(415, 100, window=time_now_button2)
time_now_button2.bind('<Enter>', lambda event: time_now_button_hover_in(event, middle_canvas))
time_now_button2.bind('<Leave>', lambda event: time_now_button_hover_out(event, middle_canvas))

# BOTTOM CANVAS-----------------------------------------------------------------------------------

bottom_canvas = Canvas(root, width=ROOT_WIDTH, height=200, borderwidth=0)
bottom_canvas.grid(row=2, column=0, columnspan=2, sticky=W + E)
img3 = ImageTk.PhotoImage(Image.open('images/money_dark.jfif').resize((600, 400)), Image.ANTIALIAS)
bottom_canvas.create_image(0, 0, image=img3, anchor=NW)
bottom_canvas.create_text(280, 30, text='Payment', font=('android 7', 25), fill='#e8ebea')

# Calculate Payment Button
calc_payment_button = Button(bottom_canvas, text='Calculate Payment', font=('consolas', 20),
                             command=payment_calculation)
bottom_canvas.create_window(275, 100, window=calc_payment_button)
calc_payment_button.bind('<Enter>', calculate_payment_button_hover_in)
calc_payment_button.bind('<Leave>', calculate_payment_button_hover_out)

# Reset Button
reset_icon = ImageTk.PhotoImage(Image.open('images/Reset.png').resize((60, 60), Image.ANTIALIAS))
reset_button = Button(bottom_canvas, image=reset_icon, command=reset_values)
r_window = bottom_canvas.create_window(60, 160, window=reset_button)
reset_button.bind('<Enter>', reset_button_hover_in)
reset_button.bind('<Leave>', reset_button_hover_out)

# Save to Excel Button
save_icon = ImageTk.PhotoImage(Image.open('images/save.png').resize((65, 60), Image.ANTIALIAS))
save_button = Button(bottom_canvas, image=save_icon, command=save_to_excel)
sb_window = bottom_canvas.create_window(490, 160, window=save_button)
save_button.bind('<Enter>', save_button_hover_in)
save_button.bind('<Leave>', save_button_hover_out)

# STATUS BAR--------------------------------------------------------------------------------
global left_status_text, date_and_time_text, status_bar

# Left Status bar text
status_bar_left = Label(root, text=rate_text, font=('consolas', 12), bg='#5fc29e', anchor=W)
status_bar_left.grid(row=3, column=0, sticky=W)

# Right status bar text
status_bar_right = Label(root, text='', font=('consolas', 12), bg='#5fc29e', anchor=E)
status_bar_right.grid(row=3, column=1, sticky=E)

# Thread for status bar time
threading.Thread(target=status_bar_time_update).start()

# MAIN--------------------
if __name__ == '__main__':
    root.mainloop()


from django.shortcuts import render
from .models import Order, OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import get_object_or_404, redirect
from .render import render_to_pdf
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect
from django.template.loader import render_to_string
from shop.models import Product
from django.contrib.auth.decorators import login_required
import datetime
from datetime import date
from django.contrib import messages
from django.contrib.auth.models import User
from django.db import connection
from django.db.models import Q

@login_required(login_url="/account/signin/")
def order_create(request):
    cart = Cart(request)
    form = OrderCreateForm(request.POST or None)
    received_amount = request.POST.get('montant_recu')

    if request.method == 'POST':
        if form.is_valid():
            # order = form.save()
            total_cost = 0
            order = form.save(commit=False)
            if cart.coupon:
                order.coupon = cart.coupon
                order.discount = cart.coupon.discount
            order.utilisateur = request.user
            order.save()
            for item in cart:
                article = Product.objects.filter(libelle=item['product'])
                # Verifer si le montant saisi est inférieur au montant à payer
                if float(received_amount) < cart.get_total_price_after_discount():
                    messages.error(request, "Le montant " + received_amount + " est inférieur au montant à payer")
                    return redirect('orders:order_create')
                # Validation de la commande
                else:
                    for product in article:
                        prod = product.libelle
                        qte=product.quantite
                        finalQte=qte-item['quantity']
                        product.quantite=finalQte
                        article.update(quantite=finalQte)
                        # Vérification de la quantité en stock :
                        if product.quantite <= 10:
                            messages.warning(request,
                                          "Le seuil en stock du produit " + product.libelle + " est "
                                          + str(product.quantite) + " veuillez en rajouter! ")
                        OrderItem.objects.create(order=order,
                                     product=item['product'],
                                     price=item['price'],
                                     tva=item['tax_rate'],
                                     quantity=item['quantity'])
            # clear the cart
            cart.clear()
            return render(request,
                            'orders/order/created.html',
                            {'order': order,'refund':received_amount, 'cart':cart})
    else:
        form = OrderCreateForm()
    return render(request,
                'orders/order/create.html',
                {'cart': cart, 'form': form,'refund':received_amount})

@staff_member_required
def admin_order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    return render(request,
                  'admin/orders/order/detail.html',
                  {'order': order})

@staff_member_required
def admin_order_pdf(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    utilisateur=request.user
    pdf = render_to_pdf('orders/order/pdf.html', {'order': order, 'user':utilisateur})
    if pdf:
        response= HttpResponse(pdf,content_type='application/pdf')
        response['Content-Disposition'] = 'filename=\
                "order_{}.pdf"'.format(order.id)
        return response
    else:
        return HttpResponse("Fichier n'existe pas")

@login_required(login_url='/account/signin')
def order_list(request):
    order_list= Order.objects.all().order_by('-date_creation')
    return render(request,
                  'orders/order/order_list.html',
                  {'order_list':order_list})

@login_required(login_url='/account/signin')
def order_detail(request, id):
    order = get_object_or_404(Order, id=id)
    return render(request,
                  'orders/order/detail.html',
                  {'order': order})

@login_required(login_url='/account/signin')
def order_pdf(request, id):
    order = get_object_or_404(Order, id=id)
    utilisateur=request.user
    pdf = render_to_pdf('orders/order/pdf.html', {'order': order, 'user':utilisateur})
    if pdf:
        response= HttpResponse(pdf,content_type='application/pdf')
        response['Content-Disposition'] = 'filename=\
                "order_{}.pdf"'.format(order.id)
        return response
    else:
        return HttpResponse("Fichier n'existe pas")

@login_required(login_url="/account/signin/")
def daily_report(request):
    template = 'shop/product/list.html'
    orders=Order.objects.filter(utilisateur_id=request.user.id , date_creation= date.today().strftime("%Y-%m-%d"))
    daily_orders=Order.objects.filter(date_creation= date.today().strftime("%Y-%m-%d"))
    total_amount=0
    daily_totalAmount=0
    for order in orders:
        if order.utilisateur:
            total_amount=total_amount + order.get_total_cost()
    for daily_order in daily_orders:
        if daily_order.date_creation:
            daily_totalAmount=daily_totalAmount+daily_order.get_total_cost()
    print(daily_totalAmount)
    return render(request,
                  template,{'totalAmountPerDay':total_amount, 'daily_Sales':daily_totalAmount})
@staff_member_required(login_url="/account/signin")
def most_saled_products(request):
    template = 'orders/order/reports.html'
    list_product = []
    totol_quantity=0
    all_ord_items = OrderItem.objects.all()
    for element in all_ord_items:
        totol_quantity=totol_quantity+element.quantity
    print(totol_quantity)
    items = OrderItem.objects.all().distinct().order_by('-quantity')[:10]

    return render(request,
                  template,
                  {'data':items,
                   'total_quantity':totol_quantity})

@staff_member_required(login_url="/account/signin")
def most_valued_order(request):
    somme_benefice=0
    template = 'orders/order/important_sold.html'
    product = Product.objects.filter(order_items__in=OrderItem.objects.all())
    produit=Product.objects.all()
    top_sold = product.distinct().order_by('-prix')[:10]
    print(top_sold)
    for item in top_sold:
        if item.pk:
            somme_benefice = somme_benefice + item.get_cost_margin()
    return render(request,
                  template,
                  {'top_sold':top_sold,
                   'benefice':somme_benefice
                   })

@staff_member_required(login_url="/account/signin")
def chart(request, pk=None):

    order_list=[i['quantity'] for i in OrderItem.objects.all().values('quantity')]
    # print(order_list)

    context={'order_list':order_list}
    return render(request,
                  'orders/order/chart.html',
                  context)

@staff_member_required(login_url="/account/signin")
def monthly_report(request):
    if request.method == 'GET':
        # from_date=datetime.datetime.strptime(request.GET.get('datedebut',None),'%Y-%m-%d')
        from_date = request.GET.get('datedebut', None)
        to_date = request.GET.get('datefin',None)
        utilisateur = request.GET.get('user', None)
        orderList=Order.objects.filter(Q(date_creation__range=(from_date,to_date))).order_by('-id')
        # print(orderList)
        #|Order.objects.filter(Q(utilisateur=utilisateur))
    return  render(request,
                   'orders/order/monthly_report.html', {'orderList':orderList})


from bs4 import BeautifulSoup
import re
import requests

def get_joke():
    url = "https://icanhazdadjoke.com/"
    reading = requests.get(url)
    soup = BeautifulSoup(reading.text, "lxml")
    # print(reading.text)
    spans = soup.find_all('div', {'class': "card-content"})
    # print(spans)
    return spans[0].find("p").get_text()
    # re.sub('[^a-zA-Z]+', '', spans[0].find("p").get_text())
    # for s in spans:
    #    print(s.get_text())

def main():
    print(get_joke())

if __name__ == "__main__":
    main()


from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Ridge
from sklearn.tree import DecisionTreeRegressor
from sklearn import linear_model
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestRegressor
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.naive_bayes import GaussianNB
from sklearn import svm
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn import tree
import pickle
import json
import re
from datetime import datetime
from pathlib import Path
from sklearn.metrics import accuracy_score
from collections import OrderedDict
dummy=pd.DataFrame()
X_train=pd.DataFrame()
y_train=pd.DataFrame()
X_test=pd.DataFrame()
y_test=pd.DataFrame()
label_encoder = LabelEncoder()
label_encoder1=LabelEncoder()
label_encoderx=LabelEncoder()
filepath1=[]
feature_count=""
sel_x=()
test_df=pd.DataFrame()
target_df=pd.DataFrame()
comman_list=[]
tt=[]
RANDOM_STATE=10
def val(n):
    if n == 0:
        print("entered into if")
        json_fail={"status":"false","msg":"0 rows after preprocessing ,choose target and x features carefully"}
        result_json=json.dumps(json_fail)
        print(result_json)
        return result_json
    else:
        return 'continue'
def hint(df,x,y):
    record=list(df.iloc[0])
    ex_dict=OrderedDict()
    for i in range(len(record)):
        ex_dict[x[i]]=str(record[i])
    print(ex_dict)
    ex_dict.pop(y, None)
    print(ex_dict)
    return ex_dict
def preprocess_dropna2(filepath,ex,x,y,algo,tt_split,mid,algo_params,email,label_encoder_file_path):
    global dummy,filepath1,feature_count,sel_x,target_df,comman_list,tt
    tt=tt_split
    print('came in',label_encoder_file_path)
    sel_x=x
    #print(sel_x)
    feature_count=int(len(x))
    #print(feature_count)
    filepath1=filepath
    if ex in ['.csv']:
        try:
            df_preprocess=pd.read_csv(filepath,encoding='raw_unicode_escape')
        except UnicodeDecodeError:
            failure={"status":"false","msg":"uploaded file cannot be used for modelling pls upload proper file"}
            success_json=json.dumps(failure)
            return success_json

        list_x=x
       # print(list_x)
        list_y=str(y)
        target_df=pd.DataFrame({'target':df_preprocess[list_y]})
        comman_list=x
        comman_list.append(list_y)
        print(comman_list)
        required_df=df_preprocess[comman_list]
        required_df=required_df.dropna(axis=0)
        shape=required_df.shape
        print(shape)
        zero=shape[0]
        print(zero)
        res=val(zero)
        if res!='continue':
            return res

       # print(res)
        #return res
        else:
            ex_dict=hint(required_df,x,y)
            dummy=dummy.append(required_df)
            visual()
            print('yet to encode')
            encoding(required_df,filepath,label_encoder_file_path)
        #normalize()
        #print(list_x)
        #print(list_y)
            print('encoding done')
            x1=dummy.iloc[:,0:feature_count]
            y1=dummy.iloc[:,-1]
            print(x1)
            data_spilt(x1,y1)
            result_json=model(algo,ex_dict,mid,algo_params,email)
            return result_json
    else:
        df_preprocess=pd.read_excel(filepath,encoding='latin')
        list_x=x
        list_y=y
        comman_list=list_x
        comman_list.append(list_y)
        print(comman_list)
        required_df=df_preprocess[comman_list]
        required_df=required_df.dropna()
        print(required_df.shape)
        shape=required_df.shape
        zero=shape[0]
        res=val(zero)
        if res!='continue':
            return res
        else:
            ex_dict=hint(required_df,x,y)
            dummy=dummy.append(required_df)
            visual()
            encoding(required_df,filepath,label_encoder_file_path)
            #normalize()
            print(target_df)
            x1=dummy.iloc[:,0:feature_count]
            y1=dummy.iloc[:,-1]
            data_spilt(x1,y1)
            result_json=model(algo,ex_dict,mid,algo_params,email)
            return result_json
def visual():
    print(dummy)
def encoder_using_path(le,df_x):
    for i in df_x.select_dtypes(['object']):
        if i in le:
            temp =le.get(i)
            # my_dict2= {y:x for x,y in temp.items()}
            # label[i]=dict(my_dict2)
            df_x[i] = df_x[i].map(temp)
            print("encoded" ,i)
    return df_x
def encoding(required_df,filepath,label_encoder_file_path):
    global dummy
    print(label_encoder_file_path)
    with open(label_encoder_file_path, 'rb') as file:
        le=pickle.load(file)
    req_df=encoder_using_path(le,required_df)
    dummy=req_df
def normalize():
    global dummy
    scaler = StandardScaler()
    scaled_features=scaler.fit_transform(dummy)
    scaled_features_df = pd.DataFrame(scaled_features, index=dummy.index, columns=dummy.columns)
    dummy =scaled_features_df
    visual()
def data_spilt(x,y):
    print("test split",x)
    global X_train,X_test,y_train,y_test,tt
    if "80% train and 20% test" in tt:
        X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.20)
        print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
    elif "70% train and 30% test" in tt:
        X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.30)
        print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
    elif "50% train and 50% test" in tt:
        X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.50)
        print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
def algo_select(li):
    global X_train,y_train,X_test,y_test
    # for i in li:
    #     if "DecisionTreeClassifier" in i:
    #         para=[]
    #         para_value=[]
    #         for j in li:
    #             par=li[j]
    #             for p in par:
    #                 para_value.append(par.get(p))
    #             clf = tree.DecisionTreeClassifier(p=v)
    #             clf = clf.fit(X_train,y_train)
    #             pre=clf.predict(X_test)
    #             acc=accuracy_score(y_test,pre)
    #             print("training done")
    #             return acc*100
    for u in li:
        algo_tune=u
        if "DecisionTreeClassifier" in algo_tune:
            var=[]
            keys=[]
            for key,value in li[u].items():
                if value !="":
                    var.append(key)
                    keys.append(value)
            print(var,keys)
            if "max_depth" in var:
                indi=var.index('max_depth')
                max_depths=int(keys[indi])
            else:
                max_depths=None
            if "max_features" in var:
                indi1=var.index('max_features')
                max_featuress=keys[indi1]
            else:
                max_featuress=None
            if "min_samples_leaf" in var:
                indi2=var.index('min_samples_leaf')
                min_samples_leafs=int(keys[indi2])
            else:
                min_samples_leafs=1
            if "criterion" in var:
                indi3=var.index('criterion')
                criterions=keys[indi3]
            else:
                criterions='gini'
            if "min_samples_split" in var:
                indi4=var.index('min_samples_split')
                min_samples_splits=int(keys[indi4])
            else:
                min_samples_splits=2
            clf = tree.DecisionTreeClassifier(max_depth=max_depths,max_features=max_featuress,min_samples_leaf=min_samples_leafs,criterion=criterions,min_samples_split=min_samples_splits,random_state=RANDOM_STATE)
            clf = clf.fit(X_train,y_train)
            pre=clf.predict(X_test)
            acc=accuracy_score(y_test,pre)
            print("training done",acc)
            return acc
        if "RandomForestClassifier" in algo_tune:
            var1=[]
            keys1=[]
            for key,value in li[u].items():
                if value !="":
                    var1.append(key)
                    keys1.append(value)
            if "max_depth" in var1:
                indi8=var1.index('max_depth')
                max_depths=keys1[indi8]
            else:
                max_depths=None
            if "n_estimators" in var1:
                indi5=var1.index('n_estimators')
                n_estimatorss=int(keys1[indi5])
            else:
                n_estimatorss=10
            if "min_samples_leaf" in var1:
                indi6=var1.index('min_samples_leaf')
                min_samples_leafs=int(keys1[indi6])
            else:
                min_samples_leafs=1
            if "min_samples_split" in var1:
                indi7=var1.index('min_samples_split')
                min_samples_splits=int(keys1[indi7])
            else:
                min_samples_splits=2
            if "criterion" in var1:
                indi9=var1.index('criterion')
                criterions=keys1[indi9]
            else:
                criterions='gini'
            if "max_features" in var1:
                indi10=var1.index('max_features')
                max_featuress=keys1[indi10]
            else:
                max_featuress=None
            clf1=RandomForestClassifier(n_estimators=n_estimatorss,max_depth=max_depths,min_samples_split=min_samples_splits,min_samples_leaf=min_samples_leafs,max_features=max_featuress,criterion=criterions,random_state=RANDOM_STATE)
            clf1 = clf1.fit(X_train,y_train)
            pre=clf1.predict(X_test)
            acc=accuracy_score(y_test,pre)
            print(acc)
            print("training done")
            return acc

        if "svm-rbf" in algo_tune:
            var3=[]
            keys3=[]
            for key,value in li[u].items():
                if value !="":
                    var3.append(key)
                    keys3.append(value)
            if "C" in var3:
                indi11=var3.index('C')
                c=int(keys3[indi11])
            else:
                c=1.0
            if "gamma" in var3:
                indi12=var3.index('gamma')
                gammas=keys3[indi12]
            else:
                gammas='auto'
            clf1=svm.SVC(kernel='rbf', gamma=gammas, C=c,random_state=RANDOM_STATE)
            clf1 = clf1.fit(X_train,y_train)
            pre=clf1.predict(X_test)
            acc=accuracy_score(y_test,pre)
            print(acc)
            print("training done")
            return acc
        if "svm-linear" in algo_tune:
            var4=[]
            keys4=[]
            for key,value in li[u].items():
                if value !="":
                    var4.append(key)
                    keys4.append(value)
            if "C" in var4:
                indi13=var4.index('C')
                c=int(keys4[indi13])
            else:
                c=1.0
            if 'class_weight' in var4:
                indi14=var4.index('class_weight')
                class_weights=keys4[indi14]
            else:
                class_weights=None
            if "multi_class" in var4:
                indi15=var4.index('multi_class')
                multi_classs=keys4[indi15]
            else:
                multi_classs='ovr'
            clf1=svm.LinearSVC(C=c,class_weight=class_weights,multi_class=multi_classs,random_state=RANDOM_STATE)
            clf1 = clf1.fit(X_train,y_train)
            pre=clf1.predict(X_test)
            acc=accuracy_score(y_test,pre)
            print(acc)
            print("training done")
            return acc
        if "KNeighborsClassifier" in algo_tune:
            var5=[]
            keys5=[]
            for key,value in li[u].items():
                if value !="":
                    var5.append(key)
                    keys5.append(value)
            if 'n_neighbors' in var5:
                indi16=var5.index('n_neighbors')
                n_neighborss=int(keys5[indi16])
            else:
                n_neighborss=5
           # if 'class_weight' in var5:
           #     indi17=var5.index('class_weight')
           #     class_weights=keys5[indi17]
           # else:
           #     class_weights=None
            if 'algorithm' in var5:
                indi18=var5.index("algorithm")
                algorithms=keys5[indi18]
            else:
                algorithms='ovr'
            if 'p' in var5:
                indi19=var5.index('p')
                ps=int(keys5[indi19])
            else:
                ps=2
            clf1=KNeighborsClassifier(n_neighbors=n_neighborss,algorithm=algorithms,p=ps)
            clf1 = clf1.fit(X_train,y_train)
            pre=clf1.predict(X_test)
            acc=accuracy_score(y_test,pre)
            print(acc)
            print("training done")
            return acc
def model(algo,ex_dict,mid,algo_params,email):
    global X_train,X_test,y_train,y_test,filepath1,feature_count,sel_x,dummy,tt
    print(algo_params)
    li={}
    acc={}
    for i in algo_params:
        li[i]=algo_params[i]
        if "classification" in algo:
            acc[i]=algo_select(li)
            li.clear()
        else:
            acc[i]=algo_selectr(li)
            li.clear()
    print(acc)
    ss={"modelId":mid,"algo":algo,"accuracy":acc,"email":email,"status":"True"}
    success_json=json.dumps(ss)
    return success_json